Coverage for /usr/local/lib/python3.11/dist-packages/grond/apps/grond.py: 52%
724 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-06-12 14:01 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-06-12 14:01 +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 '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 help='set logger level to '
215 '"critical", "error", "warning", "info", or "debug". '
216 'Default is "info".')
218 parser.add_option(
219 '--status', dest='status',
220 type='choice', choices=['state', 'quiet'],
221 help='status output selection (choices: state, quiet, default: '
222 'state)')
224 parser.add_option(
225 '--parallel', dest='nparallel', type=int,
226 help='set number of events to process in parallel, '
227 'if set to more than one, --status=quiet is implied.')
229 parser.add_option(
230 '--threads', dest='nthreads', type=int,
231 help='set number of threads per process (default: 1). '
232 'Set to 0 to use all available cores.')
234 parser.add_option(
235 '--docs',
236 dest='rst_docs',
237 action='store_true')
240def print_docs(command, parser):
242 from optparse import IndentedHelpFormatter
244 class DocsFormatter(IndentedHelpFormatter):
246 def format_heading(self, heading):
247 return '%s\n%s\n\n' % (heading, '.'*len(heading))
249 def format_usage(self, usage):
250 lines = usage.splitlines()
251 return self.format_heading('Usage') + \
252 '.. code-block:: none\n\n%s' % '\n'.join(
253 ' '+line.strip() for line in lines)
255 def format_option(self, option):
256 if not option.help:
257 return ''
259 result = []
260 opts = self.option_strings[option]
261 result.append('\n.. describe:: %s\n\n' % opts)
263 help_text = self.expand_default(option)
264 result.append(' %s\n\n' % help_text)
266 return ''.join(result)
268 parser.formatter = DocsFormatter()
269 parser.formatter.set_parser(parser)
271 def format_help(parser):
272 formatter = parser.formatter
273 result = []
275 result.append(parser.format_description(formatter) + "\n")
277 if parser.usage:
278 result.append(parser.get_usage() + "\n")
280 result.append('\n')
282 result.append(parser.format_option_help(formatter))
284 result.append('\n')
286 result.append(parser.format_epilog(formatter))
287 return "".join(result)
289 print(command)
290 print('-' * len(command))
291 print()
292 print('.. program:: %s' % program_name)
293 print()
294 print('.. option:: %s' % command)
295 print()
296 print(format_help(parser))
299def process_common_options(command, parser, options):
300 from grond.config import get_global_config
302 gconf = get_global_config()
303 gconf.override_with_cli_arguments(options)
305 util.setup_logging(program_name, gconf.loglevel)
306 if options.rst_docs:
307 print_docs(command, parser)
308 exit(0)
311def cl_parse(command, args, setup=None, details=None):
312 usage = subcommand_usages[command]
313 descr = subcommand_descriptions[command]
315 if isinstance(usage, str):
316 usage = [usage]
318 susage = '%s %s' % (program_name, usage[0])
319 for s in usage[1:]:
320 susage += '\n%s%s %s' % (' '*7, program_name, s)
322 description = descr[0].upper() + descr[1:] + '.'
324 if details:
325 description = description + '\n\n%s' % details
327 parser = OptionParser(usage=susage, description=description)
329 if setup:
330 setup(parser)
332 add_common_options(parser)
333 (options, args) = parser.parse_args(args)
334 process_common_options(command, parser, options)
335 return parser, options, args
338def die(message, err='', prelude=''):
339 if prelude:
340 prelude = prelude + '\n'
342 if err:
343 err = '\n' + err
345 sys.exit('%s%s failed: %s%s' % (prelude, program_name, message, err))
348def help_and_die(parser, message):
349 sio = StringIO()
350 parser.print_help(sio)
351 die(message, prelude=sio.getvalue())
354def multiple_choice(option, opt_str, value, parser, choices):
355 options = value.split(',')
356 for opt in options:
357 if opt not in choices:
358 raise OptionValueError('Invalid option %s - valid options are: %s'
359 % (opt, ', '.join(choices)))
360 setattr(parser.values, option.dest, options)
363def magnitude_range(option, opt_str, value, parser):
364 mag_range = value.split('-')
365 if len(mag_range) != 2:
366 raise OptionValueError(
367 'Invalid magnitude %s - valid range is e.g. 6-7.' % value)
368 try:
369 mag_range = tuple(map(float, mag_range))
370 except ValueError:
371 raise OptionValueError('Magnitudes must be numbers.')
373 if mag_range[0] > mag_range[1]:
374 raise OptionValueError('Minimum magnitude must be larger than'
375 ' maximum magnitude.')
376 setattr(parser.values, option.dest, mag_range)
379def command_scenario(args):
381 STORE_STATIC = 'crust2_ib_static'
382 STORE_WAVEFORMS = 'crust2_ib'
384 def setup(parser):
385 parser.add_option(
386 '--targets', action='callback', dest='targets', type=str,
387 callback=multiple_choice, callback_kwargs={
388 'choices': ('waveforms', 'gnss', 'insar')
389 },
390 default='waveforms',
391 help='forward modelling targets for the scenario. Select from:'
392 ' waveforms, gnss and insar. '
393 '(default: --targets=%default,'
394 ' multiple selection by --targets=waveforms,gnss,insar)')
395 parser.add_option(
396 '--problem', dest='problem', default='cmt',
397 type='choice', choices=['cmt', 'rectangular'],
398 help='problem to generate: \'dc\' (double couple)'
399 ' or \'rectangular\' (rectangular finite fault)'
400 ' (default: \'%default\')')
401 parser.add_option(
402 '--magnitude-range', dest='magnitude_range', type=str,
403 action='callback', callback=magnitude_range, default=[6.0, 7.0],
404 help='Magnitude range min_mag-max_mag (default: %default)')
405 parser.add_option(
406 '--nstations', dest='nstations', type=int, default=20,
407 help='number of seismic stations to create (default: %default)')
408 parser.add_option(
409 '--gnss_nstations', dest='gnss_nstations', type=int, default=20,
410 help='number of GNSS campaign stations to create'
411 ' (default: %default)')
412 parser.add_option(
413 '--nevents', dest='nevents', type=int, default=1,
414 help='number of events to create (default: %default)')
415 parser.add_option(
416 '--lat', dest='lat', type=float, default=41.0,
417 help='center latitude of the scenario (default: %default)')
418 parser.add_option(
419 '--lon', dest='lon', type=float, default=33.3,
420 help='center latitude of the scenario (default: %default)')
421 parser.add_option(
422 '--radius', dest='radius', type=float, default=100.,
423 help='radius of the scenario in [km] (default: %default)')
424 parser.add_option(
425 '--source-radius', dest='source_radius', type=float, default=10.,
426 help='radius of the source area in [km] (default: %default)')
427 parser.add_option(
428 '--stations-paths', dest='stations_paths', type=str, default=None,
429 help='paths to a Pyrocko station file, seperated by \',\''
430 '(default: %default)')
431 parser.add_option(
432 '--stationxml-paths', dest='stationxml_paths', type=str,
433 default=None,
434 help='paths to StationXML files, seperated by \',\''
435 '(default: %default)')
436 parser.add_option(
437 '--gf-waveforms', dest='store_waveforms', type=str,
438 default=STORE_WAVEFORMS,
439 help='Green\'s function store for waveform modelling, '
440 '(default: %default)')
441 parser.add_option(
442 '--gf-static', dest='store_statics', type=str,
443 default=STORE_STATIC,
444 help='Green\'s function store for static modelling, '
445 '(default: %default)')
446 parser.add_option(
447 '--force', dest='force', action='store_true',
448 help='overwrite existing project folder.')
449 parser.add_option(
450 '--gf-store-superdirs',
451 dest='gf_store_superdirs',
452 help='Comma-separated list of directories containing GF stores')
453 parser.add_option(
454 '--no-map',
455 dest='make_map',
456 default=True,
457 action='store_false',
458 help='suppress generation of map')
459 parser.add_option(
460 '--rebuild',
461 dest='rebuild', action='store_true', default=False,
462 help='Rebuild a manually configured grond scenario')
464 parser, options, args = cl_parse('scenario', args, setup)
466 gf_store_superdirs = None
467 if options.gf_store_superdirs:
468 gf_store_superdirs = options.gf_store_superdirs.split(',')
470 if len(args) == 1:
471 project_dir = args[0]
472 else:
473 parser.print_help()
474 sys.exit(1)
476 from grond import scenario as grond_scenario
478 try:
479 scenario = grond_scenario.GrondScenario(
480 project_dir,
481 center_lat=options.lat, center_lon=options.lon,
482 radius=options.radius*km)
484 scenario.rebuild = options.rebuild
485 if options.rebuild:
486 options.force = True
488 if 'waveforms' in options.targets:
489 if options.stationxml_paths:
490 options.stationxml_paths = [
491 op.abspath(path) for path in
492 options.stationxml_paths.split(',')]
494 if options.stations_paths:
495 options.stations_paths = [
496 op.abspath(path) for path in
497 options.stations_paths.split(',')]
499 obs = grond_scenario.WaveformObservation(
500 nstations=options.nstations,
501 store_id=options.store_waveforms,
502 stations_paths=options.stations_paths,
503 stationxml_paths=options.stationxml_paths)
504 scenario.add_observation(obs)
506 if 'insar' in options.targets:
507 obs = grond_scenario.InSARObservation(
508 store_id=options.store_statics)
509 scenario.add_observation(obs)
511 if 'gnss' in options.targets:
512 obs = grond_scenario.GNSSCampaignObservation(
513 nstations=options.gnss_nstations,
514 store_id=options.store_statics)
515 scenario.add_observation(obs)
517 if options.problem == 'cmt':
518 problem = grond_scenario.DCSourceProblem(
519 nevents=options.nevents,
520 radius=options.source_radius*km,
521 magnitude_min=options.magnitude_range[0],
522 magnitude_max=options.magnitude_range[1])
523 elif options.problem == 'rectangular':
524 problem = grond_scenario.RectangularSourceProblem(
525 nevents=options.nevents)
526 scenario.set_problem(problem)
528 scenario.build(
529 force=options.force,
530 interactive=True,
531 gf_store_superdirs=gf_store_superdirs,
532 make_map=options.make_map)
534 logger.info(CLIHints('scenario',
535 config=scenario.get_grond_config_path(),
536 project_dir=project_dir))
538 except grond.GrondError as e:
539 die(str(e))
542def command_init(args):
544 from .cmd_init import GrondInit
546 grond_init = GrondInit()
548 def print_section(entries):
549 if len(entries) == 0:
550 return '\tNone available.'
552 padding = max([len(n) for n in entries.keys()])
553 rstr = []
554 lcat = None
555 for name, desc in entries.items():
557 cat = name.split('_')[0]
558 if lcat is not None and lcat != cat:
559 rstr.append('')
560 lcat = cat
562 rstr.append(' {c.BOLD}{name:<{padding}}{c.END} : {desc}'.format(
563 name=name, desc=desc, padding=padding, c=Color))
564 return '\n'.join(rstr)
566 help_text = '''Available configuration examples for Grond.
568{c.BOLD}Example Projects{c.END}
570 Deploy a full project structure into a directory.
572 usage: grond init <example> <projectdir>
574 where <example> is any of the following:
576{examples_list}
578{c.BOLD}Config Sections{c.END}
580 Print out configuration snippets for various components.
582 usage: grond init <section>
584 where <section> is any of the following:
586{sections_list}
587'''.format(c=Color,
588 examples_list=print_section(grond_init.get_examples()),
589 sections_list=print_section(grond_init.get_sections()))
591 def setup(parser):
592 parser.add_option(
593 '--force', dest='force', action='store_true')
595 parser, options, args = cl_parse(
596 'init', args, setup,
597 'Use grond init list to show available examples.')
599 if len(args) not in (1, 2):
600 help_and_die(parser, '1 or 2 arguments required')
602 if args[0] == 'list':
603 print(help_text)
604 return
606 if args[0].startswith('example_'):
607 if len(args) == 1:
608 config = grond_init.get_content_example(args[0])
609 if not config:
610 help_and_die(parser, 'Unknown example: %s' % args[0])
612 sys.stdout.write(config+'\n\n')
614 logger.info('Hint: To create a project, use: grond init <example> '
615 '<projectdir>')
617 elif op.exists(op.abspath(args[1])) and not options.force:
618 help_and_die(
619 parser,
620 'Directory "%s" already exists! Use --force to overwrite.'
621 % args[1])
622 else:
623 try:
624 grond_init.init_example(args[0], args[1], force=options.force)
625 except OSError as e:
626 print(str(e))
628 else:
629 sec = grond_init.get_content_snippet(args[0])
630 if not sec:
631 help_and_die(parser, 'Unknown snippet: %s' % args[0])
633 sys.stdout.write(sec)
636def command_init_old(args):
638 from . import cmd_init as init
640 def setup(parser):
641 parser.add_option(
642 '--targets', action='callback', dest='targets', type=str,
643 callback=multiple_choice, callback_kwargs={
644 'choices': ('waveforms', 'gnss', 'insar', 'all')
645 },
646 default='waveforms',
647 help='select from:'
648 ' waveforms, gnss and insar. '
649 '(default: --targets=%default,'
650 ' multiple selection by --targets=waveforms,gnss,insar)')
651 parser.add_option(
652 '--problem', dest='problem',
653 type='choice', choices=['cmt', 'rectangular'],
654 help='problem to generate: \'dc\' (double couple)'
655 ' or\'rectangular\' (rectangular finite fault)'
656 ' (default: \'%default\')')
657 parser.add_option(
658 '--force', dest='force', action='store_true',
659 help='overwrite existing project folder')
661 parser, options, args = cl_parse('init', args, setup)
663 try:
664 project = init.GrondProject()
666 if 'all' in options.targets:
667 targets = ['waveforms', 'gnss', 'insar']
668 else:
669 targets = options.targets
671 if not options.problem:
672 if 'insar' in targets or 'gnss' in targets:
673 problem = 'rectangular'
674 else:
675 problem = 'cmt'
676 else:
677 problem = options.problem
679 if problem == 'rectangular':
680 project.set_rectangular_source()
681 elif problem == 'cmt':
682 project.set_cmt_source()
684 if 'waveforms' in targets:
685 project.add_waveforms()
687 if 'insar' in targets:
688 project.add_insar()
690 if 'gnss' in targets:
691 project.add_gnss()
693 if len(args) == 1:
694 project_dir = args[0]
695 project.build(project_dir, options.force)
696 logger.info(CLIHints(
697 'init', project_dir=project_dir,
698 config=op.join(project_dir, 'config', 'config.gronf')))
699 else:
700 sys.stdout.write(project.dump())
702 except grond.GrondError as e:
703 die(str(e))
706def command_events(args):
707 def setup(parser):
708 pass
710 parser, options, args = cl_parse('events', args, setup)
711 if len(args) != 1:
712 help_and_die(parser, 'missing arguments')
714 config_path = args[0]
715 try:
716 config = grond.read_config(config_path)
718 for event_name in grond.get_event_names(config):
719 print(event_name)
721 except grond.GrondError as e:
722 die(str(e))
725def command_check(args):
727 from grond.environment import Environment
729 def setup(parser):
730 parser.add_option(
731 '--target-ids', dest='target_string_ids', metavar='TARGET_IDS',
732 help='process only selected targets. TARGET_IDS is a '
733 'comma-separated list of target IDs. Target IDs have the '
734 'form SUPERGROUP.GROUP.NETWORK.STATION.LOCATION.CHANNEL.')
736 parser.add_option(
737 '--waveforms', dest='show_waveforms', action='store_true',
738 help='show raw, restituted, projected, and processed waveforms')
740 parser.add_option(
741 '--nrandom', dest='n_random_synthetics', metavar='N', type=int,
742 default=10,
743 help='set number of random synthetics to forward model (default: '
744 '10). If set to zero, create synthetics for the reference '
745 'solution.')
747 parser.add_option(
748 '--save-stations-used', dest='stations_used_path',
749 metavar='FILENAME',
750 help='aggregate all stations used by the setup into a file')
752 parser, options, args = cl_parse('check', args, setup)
753 if len(args) < 1:
754 help_and_die(parser, 'missing arguments')
756 try:
757 env = Environment(args)
758 config = env.get_config()
760 target_string_ids = None
761 if options.target_string_ids:
762 target_string_ids = options.target_string_ids.split(',')
764 grond.check(
765 config,
766 event_names=env.get_selected_event_names(),
767 target_string_ids=target_string_ids,
768 show_waveforms=options.show_waveforms,
769 n_random_synthetics=options.n_random_synthetics,
770 stations_used_path=options.stations_used_path)
772 logger.info(CLIHints('check', config=env.get_config_path()))
774 except grond.GrondError as e:
775 die(str(e))
778def command_go(args):
780 from grond.environment import Environment
782 def setup(parser):
783 parser.add_option(
784 '--force', dest='force', action='store_true',
785 help='overwrite existing run directory')
786 parser.add_option(
787 '--preserve', dest='preserve', action='store_true',
788 help='preserve old rundir')
790 parser, options, args = cl_parse('go', args, setup)
792 try:
793 env = Environment(args)
795 grond.go(
796 env,
797 force=options.force,
798 preserve=options.preserve)
800 if len(env.get_selected_event_names()) == 1:
801 logger.info(CLIHints(
802 'go', rundir=env.get_rundir_path()))
804 except grond.GrondError as e:
805 die(str(e))
808def command_forward(args):
810 from grond.environment import Environment
812 def setup(parser):
813 parser.add_option(
814 '--show', dest='show', metavar='WHAT',
815 default='filtered',
816 choices=('filtered', 'processed'),
817 help='select whether to show only "filtered" or fully "processed" '
818 '(i.e. tapered) waveforms (default "%default").')
820 parser, options, args = cl_parse('forward', args, setup)
821 if len(args) < 1:
822 help_and_die(parser, 'missing arguments')
824 try:
825 env = Environment(args)
826 grond.forward(env, show=options.show)
827 except grond.GrondError as e:
828 die(str(e))
831def command_harvest(args):
832 def setup(parser):
833 parser.add_option(
834 '--force', dest='force', action='store_true',
835 help='overwrite existing harvest directory')
836 parser.add_option(
837 '--neach', dest='neach', type=int, default=10,
838 help='take NEACH best samples from each chain (default: %default)')
839 parser.add_option(
840 '--weed', dest='weed', type=int, default=0,
841 help='weed out bootstrap samples with bad global performance. '
842 '0: no weeding (default), '
843 '1: only bootstrap chains where all NEACH best samples '
844 'global misfit is less than the global average misfit of all '
845 'NEACH best in all chains plus one standard deviation are '
846 'included in the harvest ensemble, '
847 '2: same as 1 but additionally individual samples are '
848 'removed if their global misfit is greater than the global '
849 'average misfit of all NEACH best in all chains, '
850 '3: harvesting is done on the global chain only, bootstrap '
851 'chains are excluded')
852 parser.add_option(
853 '--export-fits', dest='export_fits', default='',
854 help='additionally export details about the fit of individual '
855 'targets. "best" - export fits of best model, "mean" - '
856 'export fits of ensemble mean model, "ensemble" - export '
857 'fits of all models in harvest ensemble.')
859 parser, options, args = cl_parse('harvest', args, setup)
860 if len(args) < 1:
861 help_and_die(parser, 'no rundir')
863 export_fits = []
864 if options.export_fits.strip():
865 export_fits = [x.strip() for x in options.export_fits.split(',')]
867 for run_path in args:
868 try:
869 grond.harvest(
870 run_path,
871 force=options.force,
872 nbest=options.neach,
873 weed=options.weed,
874 export_fits=export_fits)
876 except grond.DirectoryAlreadyExists as e:
877 die(str(e) + '\n Use --force to overwrite.')
879 except grond.GrondError as e:
880 die(str(e))
883def command_cluster(args):
884 from grond import Clustering
885 from grond.clustering import metrics, methods, read_config, write_config
887 def setup(parser):
888 parser.add_option(
889 '--metric', dest='metric', metavar='METRIC',
890 default='kagan_angle',
891 choices=metrics.metrics,
892 help='metric to measure model distances. Choices: [%s]. Default: '
893 'kagan_angle' % ', '.join(metrics.metrics))
895 parser.add_option(
896 '--write-config',
897 dest='write_config',
898 metavar='FILE',
899 help='write configuration (or default configuration) to FILE')
901 method = args[0] if args else ''
902 try:
903 parser, options, args = cl_parse(
904 'cluster', args[1:], setup=Clustering.cli_setup(method, setup),
905 details='Available clustering methods: [%s]. Use '
906 '"grond cluster <method> --help" to get list of method '
907 'dependent options.' % ', '.join(methods))
909 if method not in Clustering.name_to_class and not op.exists(method):
910 help_and_die(
911 parser,
912 'no such clustering method: %s' % method if method else
913 'no clustering method specified')
915 if op.exists(method):
916 clustering = read_config(method)
917 else:
918 clustering = Clustering.cli_instantiate(method, options)
920 if options.write_config:
921 write_config(clustering, options.write_config)
922 else:
923 if len(args) != 1:
924 help_and_die(parser, 'no rundir')
925 run_path, = args
927 grond.cluster(run_path, clustering, metric=options.metric)
929 except grond.GrondError as e:
930 die(str(e))
933def command_plot(args):
935 def setup(parser):
936 parser.add_option(
937 '--show', dest='show', action='store_true',
938 help='show plot for interactive inspection')
940 details = ''
942 parser, options, args = cl_parse('plot', args, setup, details)
944 if not options.show:
945 import matplotlib
946 matplotlib.use('Agg')
948 from grond.environment import Environment
950 if len(args) not in (1, 2, 3):
951 help_and_die(parser, '1, 2 or 3 arguments required')
953 if len(args) > 1:
954 env = Environment(args[1:])
955 else:
956 env = None
958 from grond import plot
959 if args[0] == 'list':
961 def get_doc_title(doc):
962 for ln in doc.split('\n'):
963 ln = ln.strip()
964 if ln != '':
965 ln = ln.strip('.')
966 return ln
967 return 'Undocumented.'
969 if env:
970 plot_classes = env.get_plot_classes()
971 else:
972 plot_classes = plot.get_all_plot_classes()
974 plot_names, plot_doc = zip(*[(pc.name, pc.__doc__)
975 for pc in plot_classes])
977 plot_descs = [get_doc_title(doc) for doc in plot_doc]
978 left_spaces = max([len(pn) for pn in plot_names])
980 for name, desc in zip(plot_names, plot_descs):
981 print('{name:<{ls}} - {desc}'.format(
982 ls=left_spaces, name=name, desc=desc))
984 elif args[0] == 'config':
985 plot_config_collection = plot.get_plot_config_collection(env)
986 print(plot_config_collection)
988 elif args[0] == 'all':
989 if env is None:
990 help_and_die(parser, 'two or three arguments required')
991 plot_names = plot.get_plot_names(env)
992 plot.make_plots(env, plot_names=plot_names, show=options.show)
994 elif op.exists(args[0]):
995 if env is None:
996 help_and_die(parser, 'two or three arguments required')
997 plots = plot.PlotConfigCollection.load(args[0])
998 plot.make_plots(env, plots, show=options.show)
1000 else:
1001 if env is None:
1002 help_and_die(parser, 'two or three arguments required')
1003 plot_names = [name.strip() for name in args[0].split(',')]
1004 plot.make_plots(env, plot_names=plot_names, show=options.show)
1007def command_movie(args):
1009 import matplotlib
1010 matplotlib.use('Agg')
1012 def setup(parser):
1013 pass
1015 parser, options, args = cl_parse('movie', args, setup)
1017 if len(args) != 4:
1018 help_and_die(parser, 'four arguments required')
1020 run_path, xpar_name, ypar_name, movie_filename_template = args
1022 from grond import plot
1024 movie_filename = movie_filename_template % {
1025 'xpar': xpar_name,
1026 'ypar': ypar_name}
1028 try:
1029 plot.make_movie(run_path, xpar_name, ypar_name, movie_filename)
1031 except grond.GrondError as e:
1032 die(str(e))
1035def command_export(args):
1037 def setup(parser):
1038 parser.add_option(
1039 '--type', dest='type', metavar='TYPE',
1040 choices=('event', 'event-yaml', 'source', 'vector'),
1041 help='select type of objects to be exported. Choices: '
1042 '"event" (default), "event-yaml", "source", "vector".')
1044 parser.add_option(
1045 '--parameters', dest='parameters', metavar='PLIST',
1046 help='select parameters to be exported. PLIST is a '
1047 'comma-separated list where each entry has the form '
1048 '"<parameter>[.<measure>]". Available measures: "best", '
1049 '"mean", "std", "minimum", "percentile16", "median", '
1050 '"percentile84", "maximum".')
1052 parser.add_option(
1053 '--selection', dest='selection', metavar='EXPRESSION',
1054 help='only export data for runs which match EXPRESSION. '
1055 'Example expression: "tags_contains:excellent,good"')
1057 parser.add_option(
1058 '--output', dest='filename', metavar='FILE',
1059 help='write output to FILE')
1061 parser.add_option(
1062 '--effective-lat-lon', dest='effective_lat_lon',
1063 action='store_true',
1064 help='convert north_shift/east_shift offsets to true lat/lon '
1065 'coordinates (when outputting event objects).')
1067 parser, options, args = cl_parse('export', args, setup)
1068 if len(args) < 2:
1069 help_and_die(parser, 'arguments required')
1071 what = args[0]
1073 dirnames = args[1:]
1075 what_choices = ('best', 'mean', 'ensemble', 'stats')
1077 if what not in what_choices:
1078 help_and_die(
1079 parser,
1080 'invalid choice: %s (choose from %s)' % (
1081 repr(what), ', '.join(repr(x) for x in what_choices)))
1083 if options.parameters:
1084 pnames = options.parameters.split(',')
1085 else:
1086 pnames = None
1088 try:
1089 grond.export(
1090 what,
1091 dirnames,
1092 filename=options.filename,
1093 type=options.type,
1094 pnames=pnames,
1095 selection=options.selection,
1096 effective_lat_lon=options.effective_lat_lon)
1098 except grond.GrondError as e:
1099 die(str(e))
1102def command_tag(args):
1104 def setup(parser):
1105 parser.add_option(
1106 '-d', '--dir-names',
1107 dest='show_dirnames',
1108 action='store_true',
1109 help='show directory names instead of run names')
1111 parser, options, args = cl_parse('tag', args, setup)
1112 if len(args) < 2:
1113 help_and_die(parser, 'two or more arguments required')
1115 action = args.pop(0)
1117 if action not in ('add', 'remove', 'list'):
1118 help_and_die(parser, 'invalid action: %s' % action)
1120 if action in ('add', 'remove'):
1121 if len(args) < 2:
1122 help_and_die(parser, 'three or more arguments required')
1124 tag = args.pop(0)
1126 rundirs = args
1128 if action == 'list':
1129 rundirs = args
1131 from grond.environment import Environment
1133 errors = False
1134 for rundir in rundirs:
1135 try:
1136 env = Environment([rundir])
1137 if options.show_dirnames:
1138 name = rundir
1139 else:
1140 name = env.get_problem().name
1142 info = env.get_run_info()
1143 if action == 'add':
1144 info.add_tag(tag)
1145 env.set_run_info(info)
1146 elif action == 'remove':
1147 info.remove_tag(tag)
1148 env.set_run_info(info)
1149 elif action == 'list':
1150 print('%-60s : %s' % (
1151 name,
1152 ', '.join(info.tags)))
1154 except grond.GrondError as e:
1155 errors = True
1156 logger.error(e)
1158 if errors:
1159 die('Errors occurred, see log messages above.')
1162def make_report(env_args, event_name, conf, update_without_plotting):
1163 from grond.environment import Environment
1164 from grond.report import report
1165 try:
1166 env = Environment(env_args)
1167 if event_name:
1168 env.set_current_event_name(event_name)
1170 report(
1171 env, conf,
1172 update_without_plotting=update_without_plotting,
1173 make_index=False,
1174 make_archive=False)
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 '--archive',
1252 dest='make_archive',
1253 default=None,
1254 action='store_true',
1255 help='create archive file, even if `make_archive` in the report '
1256 'configuration file is set to `false`. The config file '
1257 'default creates no archive.')
1258 parser.add_option(
1259 '--no-archive',
1260 dest='make_archive',
1261 default=None,
1262 action='store_false',
1263 help='do not create archive file, even if `make_archive` in the '
1264 'report configuration file is set to `true`. The config file '
1265 'default creates no archive.')
1267 parser, options, args = cl_parse('report', args, setup)
1269 s_conf = ''
1270 if options.config:
1271 try:
1272 conf = read_config(options.config)
1273 except grond.GrondError as e:
1274 die(str(e))
1276 s_conf = ' --config="%s"' % options.config
1277 else:
1278 from grond import plot
1279 conf = ReportConfig(
1280 plot_config_collection=plot.get_plot_config_collection())
1281 conf.set_basepath('.')
1283 if options.write_config:
1284 try:
1285 write_config(conf, options.write_config)
1286 sys.exit(0)
1288 except grond.GrondError as e:
1289 die(str(e))
1291 # commandline options that can override config values
1292 if options.make_archive is not None:
1293 conf.make_archive = options.make_archive
1295 if len(args) == 1 and op.exists(op.join(args[0], 'index.html')):
1296 conf.report_base_path = conf.rel_path(args[0])
1297 s_conf = ' %s' % args[0]
1298 args = []
1300 report_base_path = conf.expand_path(conf.report_base_path)
1302 if options.index_only:
1303 report_index(conf)
1304 report_archive(conf)
1305 args = []
1307 entries_generated = False
1309 payload = []
1310 if args and all(op.isdir(rundir) for rundir in args):
1311 rundirs = args
1312 all_failed = True
1313 for rundir in rundirs:
1314 payload.append((
1315 [rundir], None, conf, options.update_without_plotting))
1317 elif args:
1318 try:
1319 env = Environment(args)
1320 for event_name in env.get_selected_event_names():
1321 payload.append((
1322 args, event_name, conf, options.update_without_plotting))
1324 except grond.GrondError as e:
1325 die(str(e))
1327 if payload:
1328 entries_generated = []
1329 for result in parimap.parimap(
1330 make_report, *zip(*payload), nprocs=options.nparallel):
1332 entries_generated.append(result)
1334 all_failed = not any(entries_generated)
1335 entries_generated = any(entries_generated)
1337 if all_failed:
1338 die('no report entries generated')
1340 report_index(conf)
1341 report_archive(conf)
1343 if options.serve or options.serve_external:
1344 if options.serve_external:
1345 host = 'default'
1346 else:
1347 host = options.host
1349 addr = serve_ip(host), options.port
1351 serve_report(
1352 addr,
1353 report_config=conf,
1354 fixed_port=options.fixed_port or options.serve_external,
1355 open=options.open)
1357 elif options.open:
1358 import webbrowser
1359 url = 'file://%s/index.html' % op.abspath(report_base_path)
1360 webbrowser.open(url)
1362 else:
1363 if not entries_generated and not options.index_only:
1364 logger.info('Nothing to do, see: grond report --help')
1366 if entries_generated and not (options.serve or options.serve_external):
1367 logger.info(CLIHints('report', config=s_conf))
1370def command_qc_polarization(args):
1372 def setup(parser):
1373 parser.add_option(
1374 '--time-factor-pre', dest='time_factor_pre', type=float,
1375 metavar='NUMBER',
1376 default=0.5,
1377 help='set duration to extract before synthetic P phase arrival, '
1378 'relative to 1/fmin. fmin is taken from the selected target '
1379 'group in the config file (default=%default)')
1380 parser.add_option(
1381 '--time-factor-post', dest='time_factor_post', type=float,
1382 metavar='NUMBER',
1383 default=0.5,
1384 help='set duration to extract after synthetic P phase arrival, '
1385 'relative to 1/fmin. fmin is taken from the selected target '
1386 'group in the config file (default=%default)')
1387 parser.add_option(
1388 '--distance-min', dest='distance_min', type=float,
1389 metavar='NUMBER',
1390 help='minimum event-station distance [m]')
1391 parser.add_option(
1392 '--distance-max', dest='distance_max', type=float,
1393 metavar='NUMBER',
1394 help='maximum event-station distance [m]')
1395 parser.add_option(
1396 '--depth-min', dest='depth_min', type=float,
1397 metavar='NUMBER',
1398 help='minimum station depth [m]')
1399 parser.add_option(
1400 '--depth-max', dest='depth_max', type=float,
1401 metavar='NUMBER',
1402 help='maximum station depth [m]')
1403 parser.add_option(
1404 '--picks', dest='picks_filename',
1405 metavar='FILENAME',
1406 help='add file with P picks in Snuffler marker format')
1407 parser.add_option(
1408 '--save', dest='output_filename',
1409 metavar='FILENAME.FORMAT',
1410 help='save output to file FILENAME.FORMAT')
1411 parser.add_option(
1412 '--dpi', dest='output_dpi', type=float, default=120.,
1413 metavar='NUMBER',
1414 help='DPI setting for raster formats (default=120)')
1416 parser, options, args = cl_parse('qc-polarization', args, setup)
1417 if len(args) != 3:
1418 help_and_die(parser, 'missing arguments')
1420 if options.output_filename:
1421 import matplotlib
1422 matplotlib.use('Agg')
1424 import grond.qc
1426 config_path, event_name, target_group_path = args
1428 try:
1429 config = grond.read_config(config_path)
1430 except grond.GrondError as e:
1431 die(str(e))
1433 ds = config.get_dataset(event_name)
1435 engine = config.engine_config.get_engine()
1437 nsl_to_time = None
1438 if options.picks_filename:
1439 markers = marker.load_markers(options.picks_filename)
1440 marker.associate_phases_to_events(markers)
1442 nsl_to_time = {}
1443 for m in markers:
1444 if isinstance(m, marker.PhaseMarker):
1445 ev = m.get_event()
1446 if ev is not None and ev.name == event_name:
1447 nsl_to_time[m.one_nslc()[:3]] = m.tmin
1449 if not nsl_to_time:
1450 help_and_die(
1451 parser,
1452 'no markers associated with event "%s" found in file "%s"' % (
1453 event_name, options.picks_filename))
1455 target_group_paths_avail = []
1456 for target_group in config.target_groups:
1457 name = target_group.path
1458 if name == target_group_path:
1459 imc = target_group.misfit_config
1460 fmin = imc.fmin
1461 fmax = imc.fmax
1462 ffactor = imc.ffactor
1464 store = engine.get_store(target_group.store_id)
1465 timing = '{cake:P|cake:p|cake:P\\|cake:p\\}'
1467 grond.qc.polarization(
1468 ds, store, timing, fmin=fmin, fmax=fmax, ffactor=ffactor,
1469 time_factor_pre=options.time_factor_pre,
1470 time_factor_post=options.time_factor_post,
1471 distance_min=options.distance_min,
1472 distance_max=options.distance_max,
1473 depth_min=options.depth_min,
1474 depth_max=options.depth_max,
1475 nsl_to_time=nsl_to_time,
1476 output_filename=options.output_filename,
1477 output_dpi=options.output_dpi)
1479 return
1481 target_group_paths_avail.append(name)
1483 die('no target group with path "%s" found. Available: %s' % (
1484 target_group_path, ', '.join(target_group_paths_avail)))
1487def command_upgrade_config(args):
1488 def setup(parser):
1489 parser.add_option(
1490 '--diff', dest='diff', action='store_true',
1491 help='create diff between normalized old and new versions')
1493 parser, options, args = cl_parse('upgrade-config', args, setup)
1494 if len(args) != 1:
1495 help_and_die(parser, 'missing argument <configfile>')
1497 from grond import upgrade
1498 upgrade.upgrade_config_file(args[0], diff=options.diff)
1501def command_diff(args):
1502 def setup(parser):
1503 pass
1505 parser, options, args = cl_parse('diff', args, setup)
1506 if len(args) != 2:
1507 help_and_die(parser, 'requires exactly two arguments')
1509 from grond.config import diff_configs
1510 diff_configs(*args)
1513def command_version(args):
1514 def setup(parser):
1515 parser.add_option(
1516 '--short', dest='short', action='store_true',
1517 help='only print Grond\'s version number')
1518 parser.add_option(
1519 '--failsafe', dest='failsafe', action='store_true',
1520 help='do not get irritated when some dependencies are missing')
1522 parser, options, args = cl_parse('version', args, setup)
1524 if options.short:
1525 print(grond.__version__)
1526 return
1528 elif not options.failsafe:
1529 from grond import info
1530 print(info.version_info())
1531 return
1533 print("grond: %s" % grond.__version__)
1535 try:
1536 import pyrocko
1537 print('pyrocko: %s' % pyrocko.long_version)
1538 except ImportError:
1539 print('pyrocko: N/A')
1541 try:
1542 import numpy
1543 print('numpy: %s' % numpy.__version__)
1544 except ImportError:
1545 print('numpy: N/A')
1547 try:
1548 import scipy
1549 print('scipy: %s' % scipy.__version__)
1550 except ImportError:
1551 print('scipy: N/A')
1553 try:
1554 import matplotlib
1555 print('matplotlib: %s' % matplotlib.__version__)
1556 except ImportError:
1557 print('matplotlib: N/A')
1559 try:
1560 from pyrocko.gui.qt_compat import Qt
1561 print('PyQt: %s' % Qt.PYQT_VERSION_STR)
1562 print('Qt: %s' % Qt.QT_VERSION_STR)
1563 except ImportError:
1564 print('PyQt: N/A')
1565 print('Qt: N/A')
1567 import sys
1568 print('python: %s.%s.%s' % sys.version_info[:3])
1570 if not options.failsafe:
1571 die('fell back to failsafe version printing')
1574if __name__ == '__main__':
1575 main()