Coverage for /usr/local/lib/python3.11/dist-packages/grond/apps/grond.py: 53%
744 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-25 10:12 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-25 10:12 +0000
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 'continue': 'continue a Grond optimisation in case of an'
50 ' interruption or more iterations are needed',
51 'forward': 'run forward modelling',
52 'harvest': 'manually run harvesting',
53 'cluster': 'run cluster analysis on result ensemble',
54 'plot': 'plot optimisation result',
55 'movie': 'visualize optimiser evolution',
56 'export': 'export results',
57 'tag': 'add user-defined label to run directories',
58 'report': 'create result report',
59 'diff': 'compare two configs or other normalized Grond YAML files',
60 'qc-polarization': 'check sensor orientations with polarization analysis',
61 'upgrade-config': 'upgrade config file to the latest version of Grond',
62 'version': 'print version number of Grond and its main dependencies',
63}
65subcommand_usages = {
66 'init': (
67 'init list [options]',
68 'init <example> [options]',
69 'init <example> <projectdir> [options]'),
70 'scenario': 'scenario [options] <projectdir>',
71 'events': 'events <configfile>',
72 'check': 'check <configfile> <eventnames> ... [options]',
73 'go': 'go <configfile> <eventnames> ... [options]',
74 'continue': 'continue <configfile> <eventnames> ... [options]',
75 'forward': (
76 'forward <rundir> [options]',
77 'forward <configfile> <eventnames> ... [options]'),
78 'harvest': 'harvest <rundir> [options]',
79 'cluster': (
80 'cluster <method> <rundir> [options]',
81 'cluster <clusteringconfigfile> <rundir> [options]'),
82 'plot': (
83 'plot <plotnames> ( <rundir> | <configfile> <eventname> ) [options]',
84 'plot all ( <rundir> | <configfile> <eventname> ) [options]',
85 'plot <plotconfigfile> ( <rundir> | <configfile> <eventname> ) [options]', # noqa
86 'plot list ( <rundir> | <configfile> <eventname> ) [options]',
87 'plot config ( <rundir> | <configfile> <eventname> ) [options]'),
88 'movie': 'movie <rundir> <xpar> <ypar> <filetemplate> [options]',
89 'export': 'export (best|mean|ensemble|stats) <rundirs> ... [options]',
90 'tag': (
91 'tag add <tag> <rundir>',
92 'tag remove <tag> <rundir>',
93 'tag list <rundir>'),
94 'report': (
95 'report <rundir> ... [options]',
96 'report <configfile> <eventnames> ...'),
97 'diff': 'diff <left_path> <right_path>',
98 'qc-polarization': 'qc-polarization <configfile> <eventname> '
99 '<target_group_path> [options]',
100 'upgrade-config': 'upgrade-config <configfile>',
101 'version': 'version',
102}
104subcommands = subcommand_descriptions.keys()
106program_name = 'grond'
108usage_tdata = d2u(subcommand_descriptions)
109usage_tdata['program_name'] = program_name
110usage_tdata['version_number'] = grond.__version__
113usage = '''%(program_name)s <subcommand> [options] [--] <arguments> ...
115Grond is a probabilistic earthquake source inversion framework.
117This is Grond version %(version_number)s.
119Subcommands:
121 scenario %(scenario)s
122 init %(init)s
123 events %(events)s
124 check %(check)s
125 go %(go)s
126 continue %(continue)s
127 forward %(forward)s
128 harvest %(harvest)s
129 cluster %(cluster)s
130 plot %(plot)s
131 movie %(movie)s
132 export %(export)s
133 tag %(tag)s
134 report %(report)s
135 diff %(diff)s
136 qc-polarization %(qc_polarization)s
137 upgrade-config %(upgrade_config)s
138 version %(version)s
140To get further help and a list of available options for any subcommand run:
142 %(program_name)s <subcommand> --help
144What do you want to bust today?!
145''' % usage_tdata
148class CLIHints(object):
149 init = '''
150We created a folder structure in {project_dir}.
151Check out the YAML configuration in {config} and start the optimisation by:
153 grond go {config}
154'''
155 scenario = '''
156To start the scenario's optimisation, change to folder
158 cd {project_dir}
160Check out the YAML configuration in {config} and start the optimisation by:
162 grond go {config}
163'''
164 report = '''
165To open the report in your web browser, run
167 grond report -s --open {config}
168'''
169 check = '''
170To start the optimisation, run
172 grond go {config}
173'''
174 go = '''
175To look at the results, run
177 grond report -so {rundir}
178'''
180 def __new__(cls, command, **kwargs):
181 return '{c.BOLD}Hint{c.END}\n'.format(c=Color) +\
182 getattr(cls, command).format(**kwargs)
185def main(args=None):
186 if not args:
187 args = sys.argv
189 args = list(args)
190 if len(args) < 2:
191 sys.exit('Usage: %s' % usage)
193 args.pop(0)
194 command = args.pop(0)
196 if command in subcommands:
197 globals()['command_' + d2u(command)](args)
199 elif command in ('--help', '-h', 'help'):
200 if command == 'help' and args:
201 acommand = args[0]
202 if acommand in subcommands:
203 globals()['command_' + acommand](['--help'])
205 sys.exit('Usage: %s' % usage)
207 else:
208 die('No such subcommand: %s' % command)
211def add_common_options(parser):
212 parser.add_option(
213 '--loglevel',
214 action='store',
215 dest='loglevel',
216 type='choice',
217 choices=('critical', 'error', 'warning', 'info', 'debug'),
218 default='info',
219 help='set logger level to '
220 '"critical", "error", "warning", "info", or "debug". '
221 'Default is "%default".')
223 parser.add_option(
224 '--docs',
225 dest='rst_docs',
226 action='store_true')
229def print_docs(command, parser):
231 from optparse import IndentedHelpFormatter
233 class DocsFormatter(IndentedHelpFormatter):
235 def format_heading(self, heading):
236 return '%s\n%s\n\n' % (heading, '.'*len(heading))
238 def format_usage(self, usage):
239 lines = usage.splitlines()
240 return self.format_heading('Usage') + \
241 '.. code-block:: none\n\n%s' % '\n'.join(
242 ' '+line.strip() for line in lines)
244 def format_option(self, option):
245 if not option.help:
246 return ''
248 result = []
249 opts = self.option_strings[option]
250 result.append('\n.. describe:: %s\n\n' % opts)
252 help_text = self.expand_default(option)
253 result.append(' %s\n\n' % help_text)
255 return ''.join(result)
257 parser.formatter = DocsFormatter()
258 parser.formatter.set_parser(parser)
260 def format_help(parser):
261 formatter = parser.formatter
262 result = []
264 result.append(parser.format_description(formatter) + "\n")
266 if parser.usage:
267 result.append(parser.get_usage() + "\n")
269 result.append('\n')
271 result.append(parser.format_option_help(formatter))
273 result.append('\n')
275 result.append(parser.format_epilog(formatter))
276 return "".join(result)
278 print(command)
279 print('-' * len(command))
280 print()
281 print('.. program:: %s' % program_name)
282 print()
283 print('.. option:: %s' % command)
284 print()
285 print(format_help(parser))
288def process_common_options(command, parser, options):
289 util.setup_logging(program_name, options.loglevel)
290 if options.rst_docs:
291 print_docs(command, parser)
292 exit(0)
295def cl_parse(command, args, setup=None, details=None):
296 usage = subcommand_usages[command]
297 descr = subcommand_descriptions[command]
299 if isinstance(usage, str):
300 usage = [usage]
302 susage = '%s %s' % (program_name, usage[0])
303 for s in usage[1:]:
304 susage += '\n%s%s %s' % (' '*7, program_name, s)
306 description = descr[0].upper() + descr[1:] + '.'
308 if details:
309 description = description + '\n\n%s' % details
311 parser = OptionParser(usage=susage, description=description)
313 if setup:
314 setup(parser)
316 add_common_options(parser)
317 (options, args) = parser.parse_args(args)
318 process_common_options(command, parser, options)
319 return parser, options, args
322def die(message, err='', prelude=''):
323 if prelude:
324 prelude = prelude + '\n'
326 if err:
327 err = '\n' + err
329 sys.exit('%s%s failed: %s%s' % (prelude, program_name, message, err))
332def help_and_die(parser, message):
333 sio = StringIO()
334 parser.print_help(sio)
335 die(message, prelude=sio.getvalue())
338def multiple_choice(option, opt_str, value, parser, choices):
339 options = value.split(',')
340 for opt in options:
341 if opt not in choices:
342 raise OptionValueError('Invalid option %s - valid options are: %s'
343 % (opt, ', '.join(choices)))
344 setattr(parser.values, option.dest, options)
347def magnitude_range(option, opt_str, value, parser):
348 mag_range = value.split('-')
349 if len(mag_range) != 2:
350 raise OptionValueError(
351 'Invalid magnitude %s - valid range is e.g. 6-7.' % value)
352 try:
353 mag_range = tuple(map(float, mag_range))
354 except ValueError:
355 raise OptionValueError('Magnitudes must be numbers.')
357 if mag_range[0] > mag_range[1]:
358 raise OptionValueError('Minimum magnitude must be larger than'
359 ' maximum magnitude.')
360 setattr(parser.values, option.dest, mag_range)
363def command_scenario(args):
365 STORE_STATIC = 'crust2_ib_static'
366 STORE_WAVEFORMS = 'crust2_ib'
368 def setup(parser):
369 parser.add_option(
370 '--targets', action='callback', dest='targets', type=str,
371 callback=multiple_choice, callback_kwargs={
372 'choices': ('waveforms', 'gnss', 'insar')
373 },
374 default='waveforms',
375 help='forward modelling targets for the scenario. Select from:'
376 ' waveforms, gnss and insar. Multiple selection by '
377 '--targets=waveforms,gnss,insar'
378 '(default: --targets=%default)')
379 parser.add_option(
380 '--problem', dest='problem', default='cmt',
381 type='choice', choices=['cmt', 'rectangular', 'dynamic_rupture'],
382 help='problem to generate: \'dc\' (double couple), '
383 '\'rectangular\' (rectangular finite fault) or '
384 '\'dynamic_rupture\' for a dynamic rupture scenario.'
385 ' (default: \'%default\')')
386 parser.add_option(
387 '--magnitude-range', dest='magnitude_range', type=str,
388 action='callback', callback=magnitude_range, default=[6.0, 7.0],
389 help='Magnitude range min_mag-max_mag (default: %default)')
390 parser.add_option(
391 '--nstations', dest='nstations', type=int, default=20,
392 help='number of seismic stations to create (default: %default)')
393 parser.add_option(
394 '--gnss_nstations', dest='gnss_nstations', type=int, default=20,
395 help='number of GNSS campaign stations to create'
396 ' (default: %default)')
397 parser.add_option(
398 '--nevents', dest='nevents', type=int, default=1,
399 help='number of events to create (default: %default)')
400 parser.add_option(
401 '--lat', dest='lat', type=float, default=41.0,
402 help='center latitude of the scenario (default: %default)')
403 parser.add_option(
404 '--lon', dest='lon', type=float, default=33.3,
405 help='center latitude of the scenario (default: %default)')
406 parser.add_option(
407 '--radius', dest='radius', type=float, default=100.,
408 help='radius of the scenario in [km] (default: %default)')
409 parser.add_option(
410 '--source-radius', dest='source_radius', type=float, default=10.,
411 help='radius of the source area in [km] (default: %default)')
412 parser.add_option(
413 '--stations-paths', dest='stations_paths', type=str, default=None,
414 help='paths to a Pyrocko station file, seperated by \',\''
415 '(default: %default)')
416 parser.add_option(
417 '--stationxml-paths', dest='stationxml_paths', type=str,
418 default=None,
419 help='paths to StationXML files, seperated by \',\''
420 '(default: %default)')
421 parser.add_option(
422 '--gf-waveforms', dest='store_waveforms', type=str,
423 default=STORE_WAVEFORMS,
424 help='Green\'s function store for waveform modelling, '
425 '(default: %default)')
426 parser.add_option(
427 '--gf-static', dest='store_statics', type=str,
428 default=STORE_STATIC,
429 help='Green\'s function store for static modelling, '
430 '(default: %default)')
431 parser.add_option(
432 '--force', dest='force', action='store_true',
433 help='overwrite existing project folder.')
434 parser.add_option(
435 '--gf-store-superdirs',
436 dest='gf_store_superdirs',
437 help='Comma-separated list of directories containing GF stores')
438 parser.add_option(
439 '--no-map',
440 dest='make_map',
441 default=True,
442 action='store_false',
443 help='suppress generation of map')
444 parser.add_option(
445 '--rebuild',
446 dest='rebuild', action='store_true', default=False,
447 help='Rebuild a manually configured grond scenario')
449 parser, options, args = cl_parse('scenario', args, setup)
451 gf_store_superdirs = None
452 if options.gf_store_superdirs:
453 gf_store_superdirs = options.gf_store_superdirs.split(',')
455 if len(args) == 1:
456 project_dir = args[0]
457 else:
458 parser.print_help()
459 sys.exit(1)
461 from grond import scenario as grond_scenario
463 try:
464 scenario = grond_scenario.GrondScenario(
465 project_dir,
466 center_lat=options.lat, center_lon=options.lon,
467 radius=options.radius*km)
469 scenario.rebuild = options.rebuild
470 if options.rebuild:
471 options.force = True
473 if 'waveforms' in options.targets:
474 if options.stationxml_paths:
475 options.stationxml_paths = [
476 op.abspath(path) for path in
477 options.stationxml_paths.split(',')]
479 if options.stations_paths:
480 options.stations_paths = [
481 op.abspath(path) for path in
482 options.stations_paths.split(',')]
484 obs = grond_scenario.WaveformObservation(
485 nstations=options.nstations,
486 store_id=options.store_waveforms,
487 stations_paths=options.stations_paths,
488 stationxml_paths=options.stationxml_paths)
489 scenario.add_observation(obs)
491 if 'insar' in options.targets:
492 obs = grond_scenario.InSARObservation(
493 store_id=options.store_statics)
494 scenario.add_observation(obs)
496 if 'gnss' in options.targets:
497 obs = grond_scenario.GNSSCampaignObservation(
498 nstations=options.gnss_nstations,
499 store_id=options.store_statics)
500 scenario.add_observation(obs)
502 src_args = dict(
503 nevents=options.nevents,
504 magnitude_min=options.magnitude_range[0],
505 magnitude_max=options.magnitude_range[1])
506 if options.problem == 'cmt':
507 problem = grond_scenario.DCSourceProblem(**src_args)
508 elif options.problem == 'rectangular':
509 problem = grond_scenario.RectangularSourceProblem(**src_args)
510 elif options.problem == 'dynamic_rupture':
511 problem = grond_scenario.PseudoDynamicRuptureProblem(**src_args)
512 scenario.set_problem(problem)
514 scenario.build(
515 force=options.force,
516 interactive=True,
517 gf_store_superdirs=gf_store_superdirs,
518 make_map=options.make_map)
520 logger.info(CLIHints('scenario',
521 config=scenario.get_grond_config_path(),
522 project_dir=project_dir))
524 except grond.GrondError as e:
525 die(str(e))
528def command_init(args):
530 from .cmd_init import GrondInit
532 grond_init = GrondInit()
534 def print_section(entries):
535 if len(entries) == 0:
536 return '\tNone available.'
538 padding = max([len(n) for n in entries.keys()])
539 rstr = []
540 lcat = None
541 for name, desc in entries.items():
543 cat = name.split('_')[0]
544 if lcat is not None and lcat != cat:
545 rstr.append('')
546 lcat = cat
548 rstr.append(' {c.BOLD}{name:<{padding}}{c.END} : {desc}'.format(
549 name=name, desc=desc, padding=padding, c=Color))
550 return '\n'.join(rstr)
552 help_text = '''Available configuration examples for Grond.
554{c.BOLD}Example Projects{c.END}
556 Deploy a full project structure into a directory.
558 usage: grond init <example> <projectdir>
560 where <example> is any of the following:
562{examples_list}
564{c.BOLD}Config Sections{c.END}
566 Print out configuration snippets for various components.
568 usage: grond init <section>
570 where <section> is any of the following:
572{sections_list}
573'''.format(c=Color,
574 examples_list=print_section(grond_init.get_examples()),
575 sections_list=print_section(grond_init.get_sections()))
577 def setup(parser):
578 parser.add_option(
579 '--force', dest='force', action='store_true')
581 parser, options, args = cl_parse(
582 'init', args, setup,
583 'Use grond init list to show available examples.')
585 if len(args) not in (1, 2):
586 help_and_die(parser, '1 or 2 arguments required')
588 if args[0] == 'list':
589 print(help_text)
590 return
592 if args[0].startswith('example_'):
593 if len(args) == 1:
594 config = grond_init.get_content_example(args[0])
595 if not config:
596 help_and_die(parser, 'Unknown example: %s' % args[0])
598 sys.stdout.write(config+'\n\n')
600 logger.info('Hint: To create a project, use: grond init <example> '
601 '<projectdir>')
603 elif op.exists(op.abspath(args[1])) and not options.force:
604 help_and_die(
605 parser,
606 'Directory "%s" already exists! Use --force to overwrite.'
607 % args[1])
608 else:
609 try:
610 grond_init.init_example(args[0], args[1], force=options.force)
611 except OSError as e:
612 print(str(e))
614 else:
615 sec = grond_init.get_content_snippet(args[0])
616 if not sec:
617 help_and_die(parser, 'Unknown snippet: %s' % args[0])
619 sys.stdout.write(sec)
622def command_init_old(args):
624 from . import cmd_init as init
626 def setup(parser):
627 parser.add_option(
628 '--targets', action='callback', dest='targets', type=str,
629 callback=multiple_choice, callback_kwargs={
630 'choices': ('waveforms', 'gnss', 'insar', 'all')
631 },
632 default='waveforms',
633 help='select from:'
634 ' waveforms, gnss and insar. '
635 '(default: --targets=%default,'
636 ' multiple selection by --targets=waveforms,gnss,insar)')
637 parser.add_option(
638 '--problem', dest='problem',
639 type='choice', choices=['cmt', 'rectangular'],
640 help='problem to generate: \'dc\' (double couple)'
641 ' or\'rectangular\' (rectangular finite fault)'
642 ' (default: \'%default\')')
643 parser.add_option(
644 '--force', dest='force', action='store_true',
645 help='overwrite existing project folder')
647 parser, options, args = cl_parse('init', args, setup)
649 try:
650 project = init.GrondProject()
652 if 'all' in options.targets:
653 targets = ['waveforms', 'gnss', 'insar']
654 else:
655 targets = options.targets
657 if not options.problem:
658 if 'insar' in targets or 'gnss' in targets:
659 problem = 'rectangular'
660 else:
661 problem = 'cmt'
662 else:
663 problem = options.problem
665 if problem == 'rectangular':
666 project.set_rectangular_source()
667 elif problem == 'cmt':
668 project.set_cmt_source()
670 if 'waveforms' in targets:
671 project.add_waveforms()
673 if 'insar' in targets:
674 project.add_insar()
676 if 'gnss' in targets:
677 project.add_gnss()
679 if len(args) == 1:
680 project_dir = args[0]
681 project.build(project_dir, options.force)
682 logger.info(CLIHints(
683 'init', project_dir=project_dir,
684 config=op.join(project_dir, 'config', 'config.gronf')))
685 else:
686 sys.stdout.write(project.dump())
688 except grond.GrondError as e:
689 die(str(e))
692def command_events(args):
693 def setup(parser):
694 pass
696 parser, options, args = cl_parse('events', args, setup)
697 if len(args) != 1:
698 help_and_die(parser, 'missing arguments')
700 config_path = args[0]
701 try:
702 config = grond.read_config(config_path)
704 for event_name in grond.get_event_names(config):
705 print(event_name)
707 except grond.GrondError as e:
708 die(str(e))
711def command_check(args):
713 from grond.environment import Environment
715 def setup(parser):
716 parser.add_option(
717 '--target-ids', dest='target_string_ids', metavar='TARGET_IDS',
718 help='process only selected targets. TARGET_IDS is a '
719 'comma-separated list of target IDs. Target IDs have the '
720 'form SUPERGROUP.GROUP.NETWORK.STATION.LOCATION.CHANNEL.')
722 parser.add_option(
723 '--waveforms', dest='show_waveforms', action='store_true',
724 help='show raw, restituted, projected, and processed waveforms')
726 parser.add_option(
727 '--nrandom', dest='n_random_synthetics', metavar='N', type=int,
728 default=10,
729 help='set number of random synthetics to forward model (default: '
730 '10). If set to zero, create synthetics for the reference '
731 'solution.')
733 parser.add_option(
734 '--save-stations-used', dest='stations_used_path',
735 metavar='FILENAME',
736 help='aggregate all stations used by the setup into a file')
738 parser, options, args = cl_parse('check', args, setup)
739 if len(args) < 1:
740 help_and_die(parser, 'missing arguments')
742 try:
743 env = Environment(args)
744 config = env.get_config()
746 target_string_ids = None
747 if options.target_string_ids:
748 target_string_ids = options.target_string_ids.split(',')
750 grond.check(
751 config,
752 event_names=env.get_selected_event_names(),
753 target_string_ids=target_string_ids,
754 show_waveforms=options.show_waveforms,
755 n_random_synthetics=options.n_random_synthetics,
756 stations_used_path=options.stations_used_path)
758 logger.info(CLIHints('check', config=env.get_config_path()))
760 except grond.GrondError as e:
761 die(str(e))
764def command_go(args):
766 from grond.environment import Environment
768 def setup(parser):
769 parser.add_option(
770 '--force', dest='force', action='store_true',
771 help='overwrite existing run directory')
772 parser.add_option(
773 '--preserve', dest='preserve', action='store_true',
774 help='preserve old rundir')
775 parser.add_option(
776 '--status', dest='status', default='state',
777 type='choice', choices=['state', 'quiet'],
778 help='status output selection (choices: state, quiet, default: '
779 'state)')
780 parser.add_option(
781 '--parallel', dest='nparallel', type=int, default=1,
782 help='set number of events to process in parallel, '
783 'if set to more than one, --status=quiet is implied.')
784 parser.add_option(
785 '--threads', dest='nthreads', type=int, default=1,
786 help='set number of threads per process (default: 1). '
787 'Set to 0 to use all available cores.')
789 parser, options, args = cl_parse('go', args, setup)
791 try:
792 env = Environment(args)
794 status = options.status
795 if options.nparallel != 1:
796 status = 'quiet'
798 grond.go(
799 env,
800 force=options.force,
801 preserve=options.preserve,
802 status=status,
803 nparallel=options.nparallel,
804 nthreads=options.nthreads)
805 if len(env.get_selected_event_names()) == 1:
806 logger.info(CLIHints(
807 'go', rundir=env.get_rundir_path()))
809 except grond.GrondError as e:
810 die(str(e))
813def command_continue(args):
815 from grond.environment import Environment
817 def setup(parser):
818 parser.add_option(
819 '--no-preserve', dest='no_preserve', action='store_true',
820 help='do not preserve old rundir')
821 parser.add_option(
822 '--status', dest='status', default='state',
823 type='choice', choices=['state', 'quiet'],
824 help='status output selection (choices: state, quiet, default: '
825 'state)')
826 parser.add_option(
827 '--parallel', dest='nparallel', type=int, default=1,
828 help='set number of events to process in parallel, '
829 'if set to more than one, --status=quiet is implied.')
830 parser.add_option(
831 '--threads', dest='nthreads', type=int, default=1,
832 help='set number of threads per process (default: 1). '
833 'Set to 0 to use all available cores.')
835 parser, options, args = cl_parse('continue', args, setup)
837 try:
838 env = Environment(args)
840 status = options.status
841 if options.nparallel != 1:
842 status = 'quiet'
844 grond.continue_run(
845 env,
846 preserve=~bool(options.no_preserve),
847 status=status,
848 nparallel=options.nparallel,
849 nthreads=options.nthreads)
851 except grond.GrondError as e:
852 die(str(e))
855def command_forward(args):
857 from grond.environment import Environment
859 def setup(parser):
860 parser.add_option(
861 '--show', dest='show', metavar='WHAT',
862 default='filtered',
863 choices=('filtered', 'processed'),
864 help='select whether to show only "filtered" or fully "processed" '
865 '(i.e. tapered) waveforms (default "%default").')
867 parser, options, args = cl_parse('forward', args, setup)
868 if len(args) < 1:
869 help_and_die(parser, 'missing arguments')
871 try:
872 env = Environment(args)
873 grond.forward(env, show=options.show)
874 except grond.GrondError as e:
875 die(str(e))
878def command_harvest(args):
879 def setup(parser):
880 parser.add_option(
881 '--force', dest='force', action='store_true',
882 help='overwrite existing harvest directory')
883 parser.add_option(
884 '--neach', dest='neach', type=int, default=10,
885 help='take NEACH best samples from each chain (default: %default)')
886 parser.add_option(
887 '--weed', dest='weed', type=int, default=0,
888 help='weed out bootstrap samples with bad global performance. '
889 '0: no weeding (default), '
890 '1: only bootstrap chains where all NEACH best samples '
891 'global misfit is less than the global average misfit of all '
892 'NEACH best in all chains plus one standard deviation are '
893 'included in the harvest ensemble, '
894 '2: same as 1 but additionally individual samples are '
895 'removed if their global misfit is greater than the global '
896 'average misfit of all NEACH best in all chains, '
897 '3: harvesting is done on the global chain only, bootstrap '
898 'chains are excluded')
899 parser.add_option(
900 '--export-fits', dest='export_fits', default='',
901 help='additionally export details about the fit of individual '
902 'targets. "best" - export fits of best model, "mean" - '
903 'export fits of ensemble mean model, "ensemble" - export '
904 'fits of all models in harvest ensemble.')
906 parser, options, args = cl_parse('harvest', args, setup)
907 if len(args) < 1:
908 help_and_die(parser, 'no rundir')
910 export_fits = []
911 if options.export_fits.strip():
912 export_fits = [x.strip() for x in options.export_fits.split(',')]
914 for run_path in args:
915 try:
916 grond.harvest(
917 run_path,
918 force=options.force,
919 nbest=options.neach,
920 weed=options.weed,
921 export_fits=export_fits)
923 except grond.DirectoryAlreadyExists as e:
924 die(str(e) + '\n Use --force to overwrite.')
926 except grond.GrondError as e:
927 die(str(e))
930def command_cluster(args):
931 from grond import Clustering
932 from grond.clustering import metrics, methods, read_config, write_config
934 def setup(parser):
935 parser.add_option(
936 '--metric', dest='metric', metavar='METRIC',
937 default='kagan_angle',
938 choices=metrics.metrics,
939 help='metric to measure model distances. Choices: [%s]. Default: '
940 'kagan_angle' % ', '.join(metrics.metrics))
942 parser.add_option(
943 '--write-config',
944 dest='write_config',
945 metavar='FILE',
946 help='write configuration (or default configuration) to FILE')
948 method = args[0] if args else ''
949 try:
950 parser, options, args = cl_parse(
951 'cluster', args[1:], setup=Clustering.cli_setup(method, setup),
952 details='Available clustering methods: [%s]. Use '
953 '"grond cluster <method> --help" to get list of method '
954 'dependent options.' % ', '.join(methods))
956 if method not in Clustering.name_to_class and not op.exists(method):
957 help_and_die(
958 parser,
959 'no such clustering method: %s' % method if method else
960 'no clustering method specified')
962 if op.exists(method):
963 clustering = read_config(method)
964 else:
965 clustering = Clustering.cli_instantiate(method, options)
967 if options.write_config:
968 write_config(clustering, options.write_config)
969 else:
970 if len(args) != 1:
971 help_and_die(parser, 'no rundir')
972 run_path, = args
974 grond.cluster(run_path, clustering, metric=options.metric)
976 except grond.GrondError as e:
977 die(str(e))
980def command_plot(args):
982 def setup(parser):
983 parser.add_option(
984 '--show', dest='show', action='store_true',
985 help='show plot for interactive inspection')
987 details = ''
989 parser, options, args = cl_parse('plot', args, setup, details)
991 if not options.show:
992 import matplotlib
993 matplotlib.use('Agg')
995 from grond.environment import Environment
997 if len(args) not in (1, 2, 3):
998 help_and_die(parser, '1, 2 or 3 arguments required')
1000 if len(args) > 1:
1001 env = Environment(args[1:])
1002 else:
1003 env = None
1005 from grond import plot
1006 if args[0] == 'list':
1008 def get_doc_title(doc):
1009 for ln in doc.split('\n'):
1010 ln = ln.strip()
1011 if ln != '':
1012 ln = ln.strip('.')
1013 return ln
1014 return 'Undocumented.'
1016 if env:
1017 plot_classes = env.get_plot_classes()
1018 else:
1019 plot_classes = plot.get_all_plot_classes()
1021 plot_names, plot_doc = zip(*[(pc.name, pc.__doc__)
1022 for pc in plot_classes])
1024 plot_descs = [get_doc_title(doc) for doc in plot_doc]
1025 left_spaces = max([len(pn) for pn in plot_names])
1027 for name, desc in zip(plot_names, plot_descs):
1028 print('{name:<{ls}} - {desc}'.format(
1029 ls=left_spaces, name=name, desc=desc))
1031 elif args[0] == 'config':
1032 plot_config_collection = plot.get_plot_config_collection(env)
1033 print(plot_config_collection)
1035 elif args[0] == 'all':
1036 if env is None:
1037 help_and_die(parser, 'two or three arguments required')
1038 plot_names = plot.get_plot_names(env)
1039 plot.make_plots(env, plot_names=plot_names, show=options.show)
1041 elif op.exists(args[0]):
1042 if env is None:
1043 help_and_die(parser, 'two or three arguments required')
1044 plots = plot.PlotConfigCollection.load(args[0])
1045 plot.make_plots(env, plots, show=options.show)
1047 else:
1048 if env is None:
1049 help_and_die(parser, 'two or three arguments required')
1050 plot_names = [name.strip() for name in args[0].split(',')]
1051 plot.make_plots(env, plot_names=plot_names, show=options.show)
1054def command_movie(args):
1056 import matplotlib
1057 matplotlib.use('Agg')
1059 def setup(parser):
1060 pass
1062 parser, options, args = cl_parse('movie', args, setup)
1064 if len(args) != 4:
1065 help_and_die(parser, 'four arguments required')
1067 run_path, xpar_name, ypar_name, movie_filename_template = args
1069 from grond import plot
1071 movie_filename = movie_filename_template % {
1072 'xpar': xpar_name,
1073 'ypar': ypar_name}
1075 try:
1076 plot.make_movie(run_path, xpar_name, ypar_name, movie_filename)
1078 except grond.GrondError as e:
1079 die(str(e))
1082def command_export(args):
1084 def setup(parser):
1085 parser.add_option(
1086 '--type', dest='type', metavar='TYPE',
1087 choices=('event', 'event-yaml', 'source', 'vector'),
1088 help='select type of objects to be exported. Choices: '
1089 '"event" (default), "event-yaml", "source", "vector".')
1091 parser.add_option(
1092 '--parameters', dest='parameters', metavar='PLIST',
1093 help='select parameters to be exported. PLIST is a '
1094 'comma-separated list where each entry has the form '
1095 '"<parameter>[.<measure>]". Available measures: "best", '
1096 '"mean", "std", "minimum", "percentile16", "median", '
1097 '"percentile84", "maximum".')
1099 parser.add_option(
1100 '--selection', dest='selection', metavar='EXPRESSION',
1101 help='only export data for runs which match EXPRESSION. '
1102 'Example expression: "tags_contains:excellent,good"')
1104 parser.add_option(
1105 '--output', dest='filename', metavar='FILE',
1106 help='write output to FILE')
1108 parser.add_option(
1109 '--effective-lat-lon', dest='effective_lat_lon',
1110 action='store_true',
1111 help='convert north_shift/east_shift offsets to true lat/lon '
1112 'coordinates (when outputting event objects).')
1114 parser, options, args = cl_parse('export', args, setup)
1115 if len(args) < 2:
1116 help_and_die(parser, 'arguments required')
1118 what = args[0]
1120 dirnames = args[1:]
1122 what_choices = ('best', 'mean', 'ensemble', 'stats')
1124 if what not in what_choices:
1125 help_and_die(
1126 parser,
1127 'invalid choice: %s (choose from %s)' % (
1128 repr(what), ', '.join(repr(x) for x in what_choices)))
1130 if options.parameters:
1131 pnames = options.parameters.split(',')
1132 else:
1133 pnames = None
1135 try:
1136 grond.export(
1137 what,
1138 dirnames,
1139 filename=options.filename,
1140 type=options.type,
1141 pnames=pnames,
1142 selection=options.selection,
1143 effective_lat_lon=options.effective_lat_lon)
1145 except grond.GrondError as e:
1146 die(str(e))
1149def command_tag(args):
1151 def setup(parser):
1152 parser.add_option(
1153 '-d', '--dir-names',
1154 dest='show_dirnames',
1155 action='store_true',
1156 help='show directory names instead of run names')
1158 parser, options, args = cl_parse('tag', args, setup)
1159 if len(args) < 2:
1160 help_and_die(parser, 'two or more arguments required')
1162 action = args.pop(0)
1164 if action not in ('add', 'remove', 'list'):
1165 help_and_die(parser, 'invalid action: %s' % action)
1167 if action in ('add', 'remove'):
1168 if len(args) < 2:
1169 help_and_die(parser, 'three or more arguments required')
1171 tag = args.pop(0)
1173 rundirs = args
1175 if action == 'list':
1176 rundirs = args
1178 from grond.environment import Environment
1180 errors = False
1181 for rundir in rundirs:
1182 try:
1183 env = Environment([rundir])
1184 if options.show_dirnames:
1185 name = rundir
1186 else:
1187 name = env.get_problem().name
1189 info = env.get_run_info()
1190 if action == 'add':
1191 info.add_tag(tag)
1192 env.set_run_info(info)
1193 elif action == 'remove':
1194 info.remove_tag(tag)
1195 env.set_run_info(info)
1196 elif action == 'list':
1197 print('%-60s : %s' % (
1198 name,
1199 ', '.join(info.tags)))
1201 except grond.GrondError as e:
1202 errors = True
1203 logger.error(e)
1205 if errors:
1206 die('Errors occurred, see log messages above.')
1209def make_report(env_args, event_name, conf, update_without_plotting, nthreads):
1210 from grond.environment import Environment
1211 from grond.report import report
1212 try:
1213 env = Environment(env_args)
1214 if event_name:
1215 env.set_current_event_name(event_name)
1217 report(
1218 env, conf,
1219 update_without_plotting=update_without_plotting,
1220 make_index=False,
1221 make_archive=False,
1222 nthreads=nthreads)
1224 return True
1226 except grond.GrondError as e:
1227 logger.error(str(e))
1228 return False
1231def command_report(args):
1233 import matplotlib
1234 matplotlib.use('Agg')
1236 from pyrocko import parimap
1238 from grond.environment import Environment
1239 from grond.report import \
1240 report_index, report_archive, serve_ip, serve_report, read_config, \
1241 write_config, ReportConfig
1243 def setup(parser):
1244 parser.add_option(
1245 '--index-only',
1246 dest='index_only',
1247 action='store_true',
1248 help='create index only')
1249 parser.add_option(
1250 '--serve', '-s',
1251 dest='serve',
1252 action='store_true',
1253 help='start http service')
1254 parser.add_option(
1255 '--serve-external', '-S',
1256 dest='serve_external',
1257 action='store_true',
1258 help='shortcut for --serve --host=default --fixed-port')
1259 parser.add_option(
1260 '--host',
1261 dest='host',
1262 default='localhost',
1263 help='<ip> to start the http server on. Special values for '
1264 '<ip>: "*" binds to all available interfaces, "default" '
1265 'to default external interface, "localhost" to "127.0.0.1".')
1266 parser.add_option(
1267 '--port',
1268 dest='port',
1269 type=int,
1270 default=8383,
1271 help='set default http server port. Will count up if port is '
1272 'already in use unless --fixed-port is given.')
1273 parser.add_option(
1274 '--fixed-port',
1275 dest='fixed_port',
1276 action='store_true',
1277 help='fail if port is already in use')
1278 parser.add_option(
1279 '--open', '-o',
1280 dest='open',
1281 action='store_true',
1282 help='open report in browser')
1283 parser.add_option(
1284 '--config',
1285 dest='config',
1286 metavar='FILE',
1287 help='report configuration file to use')
1288 parser.add_option(
1289 '--write-config',
1290 dest='write_config',
1291 metavar='FILE',
1292 help='write configuration (or default configuration) to FILE')
1293 parser.add_option(
1294 '--update-without-plotting',
1295 dest='update_without_plotting',
1296 action='store_true',
1297 help='quick-and-dirty update parameter files without plotting')
1298 parser.add_option(
1299 '--parallel', dest='nparallel', type=int, default=1,
1300 help='set number of runs to process in parallel, '
1301 'If set to more than one, --status=quiet is implied.')
1302 parser.add_option(
1303 '--threads', dest='nthreads', type=int, default=1,
1304 help='set number of threads per process (default: 1).'
1305 'Set to 0 to use all available cores.')
1306 parser.add_option(
1307 '--no-archive',
1308 dest='no_archive',
1309 action='store_true',
1310 help='don\'t create archive file.')
1312 parser, options, args = cl_parse('report', args, setup)
1314 s_conf = ''
1315 if options.config:
1316 try:
1317 conf = read_config(options.config)
1318 except grond.GrondError as e:
1319 die(str(e))
1321 s_conf = ' --config="%s"' % options.config
1322 else:
1323 from grond import plot
1324 conf = ReportConfig(
1325 plot_config_collection=plot.get_plot_config_collection())
1326 conf.set_basepath('.')
1328 if options.write_config:
1329 try:
1330 write_config(conf, options.write_config)
1331 sys.exit(0)
1333 except grond.GrondError as e:
1334 die(str(e))
1336 # commandline options that can override config values
1337 if options.no_archive:
1338 conf.make_archive = False
1340 if len(args) == 1 and op.exists(op.join(args[0], 'index.html')):
1341 conf.report_base_path = conf.rel_path(args[0])
1342 s_conf = ' %s' % args[0]
1343 args = []
1345 report_base_path = conf.expand_path(conf.report_base_path)
1347 if options.index_only:
1348 report_index(conf)
1349 report_archive(conf)
1350 args = []
1352 entries_generated = False
1354 payload = []
1355 if args and all(op.isdir(rundir) for rundir in args):
1356 rundirs = args
1357 all_failed = True
1358 for rundir in rundirs:
1359 payload.append((
1360 [rundir], None, conf, options.update_without_plotting,
1361 options.nthreads))
1363 elif args:
1364 try:
1365 env = Environment(args)
1366 for event_name in env.get_selected_event_names():
1367 payload.append((
1368 args, event_name, conf, options.update_without_plotting,
1369 options.nthreads))
1371 except grond.GrondError as e:
1372 die(str(e))
1374 if payload:
1375 entries_generated = []
1376 for result in parimap.parimap(
1377 make_report, *zip(*payload), nprocs=options.nparallel):
1379 entries_generated.append(result)
1381 all_failed = not any(entries_generated)
1382 entries_generated = any(entries_generated)
1384 if all_failed:
1385 die('no report entries generated')
1387 report_index(conf)
1388 report_archive(conf)
1390 if options.serve or options.serve_external:
1391 if options.serve_external:
1392 host = 'default'
1393 else:
1394 host = options.host
1396 addr = serve_ip(host), options.port
1398 serve_report(
1399 addr,
1400 report_config=conf,
1401 fixed_port=options.fixed_port or options.serve_external,
1402 open=options.open)
1404 elif options.open:
1405 import webbrowser
1406 url = 'file://%s/index.html' % op.abspath(report_base_path)
1407 webbrowser.open(url)
1409 else:
1410 if not entries_generated and not options.index_only:
1411 logger.info('Nothing to do, see: grond report --help')
1413 if entries_generated and not (options.serve or options.serve_external):
1414 logger.info(CLIHints('report', config=s_conf))
1417def command_qc_polarization(args):
1419 def setup(parser):
1420 parser.add_option(
1421 '--time-factor-pre', dest='time_factor_pre', type=float,
1422 metavar='NUMBER',
1423 default=0.5,
1424 help='set duration to extract before synthetic P phase arrival, '
1425 'relative to 1/fmin. fmin is taken from the selected target '
1426 'group in the config file (default=%default)')
1427 parser.add_option(
1428 '--time-factor-post', dest='time_factor_post', type=float,
1429 metavar='NUMBER',
1430 default=0.5,
1431 help='set duration to extract after synthetic P phase arrival, '
1432 'relative to 1/fmin. fmin is taken from the selected target '
1433 'group in the config file (default=%default)')
1434 parser.add_option(
1435 '--distance-min', dest='distance_min', type=float,
1436 metavar='NUMBER',
1437 help='minimum event-station distance [m]')
1438 parser.add_option(
1439 '--distance-max', dest='distance_max', type=float,
1440 metavar='NUMBER',
1441 help='maximum event-station distance [m]')
1442 parser.add_option(
1443 '--depth-min', dest='depth_min', type=float,
1444 metavar='NUMBER',
1445 help='minimum station depth [m]')
1446 parser.add_option(
1447 '--depth-max', dest='depth_max', type=float,
1448 metavar='NUMBER',
1449 help='maximum station depth [m]')
1450 parser.add_option(
1451 '--picks', dest='picks_filename',
1452 metavar='FILENAME',
1453 help='add file with P picks in Snuffler marker format')
1454 parser.add_option(
1455 '--save', dest='output_filename',
1456 metavar='FILENAME.FORMAT',
1457 help='save output to file FILENAME.FORMAT')
1458 parser.add_option(
1459 '--dpi', dest='output_dpi', type=float, default=120.,
1460 metavar='NUMBER',
1461 help='DPI setting for raster formats (default=120)')
1463 parser, options, args = cl_parse('qc-polarization', args, setup)
1464 if len(args) != 3:
1465 help_and_die(parser, 'missing arguments')
1467 if options.output_filename:
1468 import matplotlib
1469 matplotlib.use('Agg')
1471 import grond.qc
1473 config_path, event_name, target_group_path = args
1475 try:
1476 config = grond.read_config(config_path)
1477 except grond.GrondError as e:
1478 die(str(e))
1480 ds = config.get_dataset(event_name)
1482 engine = config.engine_config.get_engine()
1484 nsl_to_time = None
1485 if options.picks_filename:
1486 markers = marker.load_markers(options.picks_filename)
1487 marker.associate_phases_to_events(markers)
1489 nsl_to_time = {}
1490 for m in markers:
1491 if isinstance(m, marker.PhaseMarker):
1492 ev = m.get_event()
1493 if ev is not None and ev.name == event_name:
1494 nsl_to_time[m.one_nslc()[:3]] = m.tmin
1496 if not nsl_to_time:
1497 help_and_die(
1498 parser,
1499 'no markers associated with event "%s" found in file "%s"' % (
1500 event_name, options.picks_filename))
1502 target_group_paths_avail = []
1503 for target_group in config.target_groups:
1504 name = target_group.path
1505 if name == target_group_path:
1506 imc = target_group.misfit_config
1507 fmin = imc.fmin
1508 fmax = imc.fmax
1509 ffactor = imc.ffactor
1511 store = engine.get_store(target_group.store_id)
1512 timing = '{cake:P|cake:p|cake:P\\|cake:p\\}'
1514 grond.qc.polarization(
1515 ds, store, timing, fmin=fmin, fmax=fmax, ffactor=ffactor,
1516 time_factor_pre=options.time_factor_pre,
1517 time_factor_post=options.time_factor_post,
1518 distance_min=options.distance_min,
1519 distance_max=options.distance_max,
1520 depth_min=options.depth_min,
1521 depth_max=options.depth_max,
1522 nsl_to_time=nsl_to_time,
1523 output_filename=options.output_filename,
1524 output_dpi=options.output_dpi)
1526 return
1528 target_group_paths_avail.append(name)
1530 die('no target group with path "%s" found. Available: %s' % (
1531 target_group_path, ', '.join(target_group_paths_avail)))
1534def command_upgrade_config(args):
1535 def setup(parser):
1536 parser.add_option(
1537 '--diff', dest='diff', action='store_true',
1538 help='create diff between normalized old and new versions')
1540 parser, options, args = cl_parse('upgrade-config', args, setup)
1541 if len(args) != 1:
1542 help_and_die(parser, 'missing argument <configfile>')
1544 from grond import upgrade
1545 upgrade.upgrade_config_file(args[0], diff=options.diff)
1548def command_diff(args):
1549 def setup(parser):
1550 pass
1552 parser, options, args = cl_parse('diff', args, setup)
1553 if len(args) != 2:
1554 help_and_die(parser, 'requires exactly two arguments')
1556 from grond.config import diff_configs
1557 diff_configs(*args)
1560def command_version(args):
1561 def setup(parser):
1562 parser.add_option(
1563 '--short', dest='short', action='store_true',
1564 help='only print Grond\'s version number')
1565 parser.add_option(
1566 '--failsafe', dest='failsafe', action='store_true',
1567 help='do not get irritated when some dependencies are missing')
1569 parser, options, args = cl_parse('version', args, setup)
1571 if options.short:
1572 print(grond.__version__)
1573 return
1575 elif not options.failsafe:
1576 from grond import info
1577 print(info.version_info())
1578 return
1580 print("grond: %s" % grond.__version__)
1582 try:
1583 import pyrocko
1584 print('pyrocko: %s' % pyrocko.long_version)
1585 except ImportError:
1586 print('pyrocko: N/A')
1588 try:
1589 import numpy
1590 print('numpy: %s' % numpy.__version__)
1591 except ImportError:
1592 print('numpy: N/A')
1594 try:
1595 import scipy
1596 print('scipy: %s' % scipy.__version__)
1597 except ImportError:
1598 print('scipy: N/A')
1600 try:
1601 import matplotlib
1602 print('matplotlib: %s' % matplotlib.__version__)
1603 except ImportError:
1604 print('matplotlib: N/A')
1606 try:
1607 from pyrocko.gui.qt_compat import Qt
1608 print('PyQt: %s' % Qt.PYQT_VERSION_STR)
1609 print('Qt: %s' % Qt.QT_VERSION_STR)
1610 except ImportError:
1611 print('PyQt: N/A')
1612 print('Qt: N/A')
1614 import sys
1615 print('python: %s.%s.%s' % sys.version_info[:3])
1617 if not options.failsafe:
1618 die('fell back to failsafe version printing')
1621if __name__ == '__main__':
1622 main()