Coverage for /usr/local/lib/python3.11/dist-packages/grond/apps/grond.py: 53%
732 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-11-27 15:15 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-11-27 15:15 +0000
1# https://pyrocko.org/grond - GPLv3
2#
3# The Grond Developers, 21st Century
4import sys
5import os.path as op
6import logging
7from optparse import OptionParser, OptionValueError, IndentedHelpFormatter
8from io import StringIO
10from pyrocko import util, marker
11import grond
13logger = logging.getLogger('grond.main')
14km = 1e3
17class GrondHelpFormatter(IndentedHelpFormatter):
19 def _format_text(self, text):
20 """
21 Format a paragraph of free-form text for inclusion in the
22 help output at the current indentation level.
23 """
24 import textwrap
26 def fill(text, *args, **kwargs):
27 return '\n\n'.join(
28 textwrap.fill(part, *args, **kwargs)
29 for part in text.split('\n\n'))
31 text_width = max(self.width - self.current_indent, 11)
32 indent = " "*self.current_indent
33 return fill(
34 text,
35 text_width,
36 initial_indent=indent,
37 subsequent_indent=indent)
40class Color:
41 PURPLE = '\033[95m'
42 CYAN = '\033[96m'
43 DARKCYAN = '\033[36m'
44 BLUE = '\033[94m'
45 GREEN = '\033[92m'
46 YELLOW = '\033[93m'
47 RED = '\033[91m'
48 BOLD = '\033[1m'
49 UNDERLINE = '\033[4m'
50 END = '\033[0m'
53def d2u(d):
54 if isinstance(d, dict):
55 return dict((k.replace('-', '_'), v) for (k, v) in d.items())
56 else:
57 return d.replace('-', '_')
60subcommand_descriptions = {
61 'init': 'initialise new project structure or print configuration',
62 'scenario': 'create a forward-modelled scenario project',
63 'events': 'print available event names for given configuration',
64 'check': 'check data and configuration',
65 'go': 'run Grond optimisation',
66 'forward': 'run forward modelling',
67 'harvest': 'manually run harvesting',
68 'cluster': 'run cluster analysis on result ensemble',
69 'plot': 'plot optimisation result',
70 'movie': 'visualize optimiser evolution',
71 'export': 'export results',
72 'tag': 'add user-defined label to run directories',
73 'report': 'create result report',
74 'diff': 'compare two configs or other normalized Grond YAML files',
75 'qc-polarization': 'check sensor orientations with polarization analysis',
76 'upgrade-config': 'upgrade config file to the latest version of Grond',
77 'version': 'print version number of Grond and its main dependencies',
78}
80subcommand_usages = {
81 'init': (
82 'init list [options]',
83 'init <example> [options]',
84 'init <example> <projectdir> [options]'),
85 'scenario': 'scenario [options] <projectdir>',
86 'events': 'events <configfile>',
87 'check': 'check <configfile> <eventnames> ... [options]',
88 'go': 'go <configfile> <eventnames> ... [options]',
89 'forward': (
90 'forward <rundir> [options]',
91 'forward <configfile> <eventnames> ... [options]'),
92 'harvest': 'harvest <rundir> [options]',
93 'cluster': (
94 'cluster <method> <rundir> [options]',
95 'cluster <clusteringconfigfile> <rundir> [options]'),
96 'plot': (
97 'plot <plotnames> ( <rundir> | <configfile> <eventname> ) [options]',
98 'plot all ( <rundir> | <configfile> <eventname> ) [options]',
99 'plot <plotconfigfile> ( <rundir> | <configfile> <eventname> ) [options]', # noqa
100 'plot list ( <rundir> | <configfile> <eventname> ) [options]',
101 'plot config ( <rundir> | <configfile> <eventname> ) [options]'),
102 'movie': 'movie <rundir> <xpar> <ypar> <filetemplate> [options]',
103 'export': 'export (best|mean|ensemble|stats) <rundirs> ... [options]',
104 'tag': (
105 'tag add <tag> <rundir>',
106 'tag remove <tag> <rundir>',
107 'tag list <rundir>'),
108 'report': (
109 'report <rundir> ... [options]',
110 'report <configfile> <eventnames> ...'),
111 'diff': 'diff <left_path> <right_path>',
112 'qc-polarization': 'qc-polarization <configfile> <eventname> '
113 '<target_group_path> [options]',
114 'upgrade-config': 'upgrade-config <configfile>',
115 'version': 'version',
116}
118subcommands = subcommand_descriptions.keys()
120program_name = 'grond'
122usage_tdata = d2u(subcommand_descriptions)
123usage_tdata['program_name'] = program_name
124usage_tdata['version_number'] = grond.__version__
127usage = '''%(program_name)s <subcommand> [options] [--] <arguments> ...
129Grond is a probabilistic earthquake source inversion framework.
131This is Grond version %(version_number)s.
133Subcommands:
135 scenario %(scenario)s
136 init %(init)s
137 events %(events)s
138 check %(check)s
139 go %(go)s
140 forward %(forward)s
141 harvest %(harvest)s
142 cluster %(cluster)s
143 plot %(plot)s
144 movie %(movie)s
145 export %(export)s
146 tag %(tag)s
147 report %(report)s
148 diff %(diff)s
149 qc-polarization %(qc_polarization)s
150 upgrade-config %(upgrade_config)s
151 version %(version)s
153To get further help and a list of available options for any subcommand run:
155 %(program_name)s <subcommand> --help
157What do you want to bust today?!
158''' % usage_tdata
161class CLIHints(object):
162 init = '''
163We created a folder structure in {project_dir}.
164Check out the YAML configuration in {config} and start the optimisation by:
166 grond go {config}
167'''
168 scenario = '''
169To start the scenario's optimisation, change to folder
171 cd {project_dir}
173Check out the YAML configuration in {config} and start the optimisation by:
175 grond go {config}
176'''
177 report = '''
178To open the report in your web browser, run
180 grond report -s --open {config}
181'''
182 check = '''
183To start the optimisation, run
185 grond go {config}
186'''
187 go = '''
188To look at the results, run
190 grond report -so {rundir}
191'''
193 def __new__(cls, command, **kwargs):
194 return '{c.BOLD}Hint{c.END}\n'.format(c=Color) +\
195 getattr(cls, command).format(**kwargs)
198def main(args=None):
199 if not args:
200 args = sys.argv
202 args = list(args)
203 if len(args) < 2:
204 sys.exit('Usage: %s' % usage)
206 args.pop(0)
207 command = args.pop(0)
209 if command in subcommands:
210 globals()['command_' + d2u(command)](args)
212 elif command in ('--help', '-h', 'help'):
213 if command == 'help' and args:
214 acommand = args[0]
215 if acommand in subcommands:
216 globals()['command_' + acommand](['--help'])
218 sys.exit('Usage: %s' % usage)
220 else:
221 die('No such subcommand: %s' % command)
224def add_common_options(parser):
225 parser.add_option(
226 '--loglevel',
227 action='store',
228 dest='loglevel',
229 type='choice',
230 choices=('critical', 'error', 'warning', 'info', 'debug'),
231 help='set logger level to '
232 '"critical", "error", "warning", "info", or "debug". '
233 'Default is "info".')
235 parser.add_option(
236 '--status', dest='status',
237 type='choice', choices=['state', 'quiet'],
238 help='status output selection (choices: state, quiet, default: '
239 'state)')
241 parser.add_option(
242 '--parallel', dest='nparallel', type=int,
243 help='set number of events to process in parallel, '
244 'if set to more than one, --status=quiet is implied.')
246 parser.add_option(
247 '--threads', dest='nthreads', type=int,
248 help='set number of threads per process (default: 1). '
249 'Set to 0 to use all available cores.')
251 parser.add_option(
252 '--docs',
253 dest='rst_docs',
254 action='store_true')
257def print_docs(command, parser):
259 from optparse import IndentedHelpFormatter
261 class DocsFormatter(IndentedHelpFormatter):
263 def format_heading(self, heading):
264 return '%s\n%s\n\n' % (heading, '.'*len(heading))
266 def format_usage(self, usage):
267 lines = usage.splitlines()
268 return self.format_heading('Usage') + \
269 '.. code-block:: none\n\n%s' % '\n'.join(
270 ' '+line.strip() for line in lines)
272 def format_option(self, option):
273 if not option.help:
274 return ''
276 result = []
277 opts = self.option_strings[option]
278 result.append('\n.. describe:: %s\n\n' % opts)
280 help_text = self.expand_default(option)
281 result.append(' %s\n\n' % help_text)
283 return ''.join(result)
285 parser.formatter = DocsFormatter()
286 parser.formatter.set_parser(parser)
288 def format_help(parser):
289 formatter = parser.formatter
290 result = []
292 result.append(parser.format_description(formatter) + "\n")
294 if parser.usage:
295 result.append(parser.get_usage() + "\n")
297 result.append('\n')
299 result.append(parser.format_option_help(formatter))
301 result.append('\n')
303 result.append(parser.format_epilog(formatter))
304 return "".join(result)
306 print(command)
307 print('-' * len(command))
308 print()
309 print('.. program:: %s' % program_name)
310 print()
311 print('.. option:: %s' % command)
312 print()
313 print(format_help(parser))
316def process_common_options(command, parser, options):
317 from grond.config import get_global_config
319 gconf = get_global_config()
320 gconf.override_with_cli_arguments(options)
322 util.setup_logging(program_name, gconf.loglevel)
323 if options.rst_docs:
324 print_docs(command, parser)
325 exit(0)
328def cl_parse(command, args, setup=None, details=None):
329 usage = subcommand_usages[command]
330 descr = subcommand_descriptions[command]
332 if isinstance(usage, str):
333 usage = [usage]
335 susage = '%s %s' % (program_name, usage[0])
336 for s in usage[1:]:
337 susage += '\n%s%s %s' % (' '*7, program_name, s)
339 description = descr[0].upper() + descr[1:] + '.'
341 if details:
342 description = description + '\n\n%s' % details
344 parser = OptionParser(
345 usage=susage,
346 description=description,
347 formatter=GrondHelpFormatter())
349 if setup:
350 setup(parser)
352 add_common_options(parser)
353 (options, args) = parser.parse_args(args)
354 process_common_options(command, parser, options)
355 return parser, options, args
358def die(message, err='', prelude=''):
359 if prelude:
360 prelude = prelude + '\n'
362 if err:
363 err = '\n' + err
365 sys.exit('%s%s failed: %s%s' % (prelude, program_name, message, err))
368def help_and_die(parser, message):
369 sio = StringIO()
370 parser.print_help(sio)
371 die(message, prelude=sio.getvalue())
374def multiple_choice(option, opt_str, value, parser, choices):
375 options = value.split(',')
376 for opt in options:
377 if opt not in choices:
378 raise OptionValueError('Invalid option %s - valid options are: %s'
379 % (opt, ', '.join(choices)))
380 setattr(parser.values, option.dest, options)
383def magnitude_range(option, opt_str, value, parser):
384 mag_range = value.split('-')
385 if len(mag_range) != 2:
386 raise OptionValueError(
387 'Invalid magnitude %s - valid range is e.g. 6-7.' % value)
388 try:
389 mag_range = tuple(map(float, mag_range))
390 except ValueError:
391 raise OptionValueError('Magnitudes must be numbers.')
393 if mag_range[0] > mag_range[1]:
394 raise OptionValueError('Minimum magnitude must be larger than'
395 ' maximum magnitude.')
396 setattr(parser.values, option.dest, mag_range)
399def command_scenario(args):
401 STORE_STATIC = 'crust2_ib_static'
402 STORE_WAVEFORMS = 'crust2_ib'
404 def setup(parser):
405 parser.add_option(
406 '--targets', action='callback', dest='targets', type=str,
407 callback=multiple_choice, callback_kwargs={
408 'choices': ('waveforms', 'gnss', 'insar')
409 },
410 default='waveforms',
411 help='forward modelling targets for the scenario. Select from:'
412 ' waveforms, gnss and insar. '
413 '(default: --targets=%default,'
414 ' multiple selection by --targets=waveforms,gnss,insar)')
415 parser.add_option(
416 '--problem', dest='problem', default='cmt',
417 type='choice', choices=['cmt', 'rectangular'],
418 help='problem to generate: \'dc\' (double couple)'
419 ' or \'rectangular\' (rectangular finite fault)'
420 ' (default: \'%default\')')
421 parser.add_option(
422 '--magnitude-range', dest='magnitude_range', type=str,
423 action='callback', callback=magnitude_range, default=[6.0, 7.0],
424 help='Magnitude range min_mag-max_mag (default: %default)')
425 parser.add_option(
426 '--nstations', dest='nstations', type=int, default=20,
427 help='number of seismic stations to create (default: %default)')
428 parser.add_option(
429 '--gnss_nstations', dest='gnss_nstations', type=int, default=20,
430 help='number of GNSS campaign stations to create'
431 ' (default: %default)')
432 parser.add_option(
433 '--nevents', dest='nevents', type=int, default=1,
434 help='number of events to create (default: %default)')
435 parser.add_option(
436 '--lat', dest='lat', type=float, default=41.0,
437 help='center latitude of the scenario (default: %default)')
438 parser.add_option(
439 '--lon', dest='lon', type=float, default=33.3,
440 help='center latitude of the scenario (default: %default)')
441 parser.add_option(
442 '--radius', dest='radius', type=float, default=100.,
443 help='radius of the scenario in [km] (default: %default)')
444 parser.add_option(
445 '--source-radius', dest='source_radius', type=float, default=10.,
446 help='radius of the source area in [km] (default: %default)')
447 parser.add_option(
448 '--stations-paths', dest='stations_paths', type=str, default=None,
449 help='paths to a Pyrocko station file, seperated by \',\''
450 '(default: %default)')
451 parser.add_option(
452 '--stationxml-paths', dest='stationxml_paths', type=str,
453 default=None,
454 help='paths to StationXML files, seperated by \',\''
455 '(default: %default)')
456 parser.add_option(
457 '--gf-waveforms', dest='store_waveforms', type=str,
458 default=STORE_WAVEFORMS,
459 help='Green\'s function store for waveform modelling, '
460 '(default: %default)')
461 parser.add_option(
462 '--gf-static', dest='store_statics', type=str,
463 default=STORE_STATIC,
464 help='Green\'s function store for static modelling, '
465 '(default: %default)')
466 parser.add_option(
467 '--force', dest='force', action='store_true',
468 help='overwrite existing project folder.')
469 parser.add_option(
470 '--gf-store-superdirs',
471 dest='gf_store_superdirs',
472 help='Comma-separated list of directories containing GF stores')
473 parser.add_option(
474 '--no-map',
475 dest='make_map',
476 default=True,
477 action='store_false',
478 help='suppress generation of map')
479 parser.add_option(
480 '--rebuild',
481 dest='rebuild', action='store_true', default=False,
482 help='Rebuild a manually configured grond scenario')
484 parser, options, args = cl_parse('scenario', args, setup)
486 gf_store_superdirs = None
487 if options.gf_store_superdirs:
488 gf_store_superdirs = options.gf_store_superdirs.split(',')
490 if len(args) == 1:
491 project_dir = args[0]
492 else:
493 parser.print_help()
494 sys.exit(1)
496 from grond import scenario as grond_scenario
498 try:
499 scenario = grond_scenario.GrondScenario(
500 project_dir,
501 center_lat=options.lat, center_lon=options.lon,
502 radius=options.radius*km)
504 scenario.rebuild = options.rebuild
505 if options.rebuild:
506 options.force = True
508 if 'waveforms' in options.targets:
509 if options.stationxml_paths:
510 options.stationxml_paths = [
511 op.abspath(path) for path in
512 options.stationxml_paths.split(',')]
514 if options.stations_paths:
515 options.stations_paths = [
516 op.abspath(path) for path in
517 options.stations_paths.split(',')]
519 obs = grond_scenario.WaveformObservation(
520 nstations=options.nstations,
521 store_id=options.store_waveforms,
522 stations_paths=options.stations_paths,
523 stationxml_paths=options.stationxml_paths)
524 scenario.add_observation(obs)
526 if 'insar' in options.targets:
527 obs = grond_scenario.InSARObservation(
528 store_id=options.store_statics)
529 scenario.add_observation(obs)
531 if 'gnss' in options.targets:
532 obs = grond_scenario.GNSSCampaignObservation(
533 nstations=options.gnss_nstations,
534 store_id=options.store_statics)
535 scenario.add_observation(obs)
537 if options.problem == 'cmt':
538 problem = grond_scenario.DCSourceProblem(
539 nevents=options.nevents,
540 radius=options.source_radius*km,
541 magnitude_min=options.magnitude_range[0],
542 magnitude_max=options.magnitude_range[1])
543 elif options.problem == 'rectangular':
544 problem = grond_scenario.RectangularSourceProblem(
545 nevents=options.nevents)
546 scenario.set_problem(problem)
548 scenario.build(
549 force=options.force,
550 interactive=True,
551 gf_store_superdirs=gf_store_superdirs,
552 make_map=options.make_map)
554 logger.info(CLIHints('scenario',
555 config=scenario.get_grond_config_path(),
556 project_dir=project_dir))
558 except grond.GrondError as e:
559 die(str(e))
562def command_init(args):
564 from .cmd_init import GrondInit
566 grond_init = GrondInit()
568 def print_section(entries):
569 if len(entries) == 0:
570 return '\tNone available.'
572 padding = max([len(n) for n in entries.keys()])
573 rstr = []
574 lcat = None
575 for name, desc in entries.items():
577 cat = name.split('_')[0]
578 if lcat is not None and lcat != cat:
579 rstr.append('')
580 lcat = cat
582 rstr.append(' {c.BOLD}{name:<{padding}}{c.END} : {desc}'.format(
583 name=name, desc=desc, padding=padding, c=Color))
584 return '\n'.join(rstr)
586 help_text = '''Available configuration examples for Grond.
588{c.BOLD}Example Projects{c.END}
590 Deploy a full project structure into a directory.
592 usage: grond init <example> <projectdir>
594 where <example> is any of the following:
596{examples_list}
598{c.BOLD}Config Sections{c.END}
600 Print out configuration snippets for various components.
602 usage: grond init <section>
604 where <section> is any of the following:
606{sections_list}
607'''.format(c=Color,
608 examples_list=print_section(grond_init.get_examples()),
609 sections_list=print_section(grond_init.get_sections()))
611 def setup(parser):
612 parser.add_option(
613 '--force', dest='force', action='store_true')
615 parser, options, args = cl_parse(
616 'init', args, setup,
617 'Use grond init list to show available examples.')
619 if len(args) not in (1, 2):
620 help_and_die(parser, '1 or 2 arguments required')
622 if args[0] == 'list':
623 print(help_text)
624 return
626 if args[0].startswith('example_'):
627 if len(args) == 1:
628 config = grond_init.get_content_example(args[0])
629 if not config:
630 help_and_die(parser, 'Unknown example: %s' % args[0])
632 sys.stdout.write(config+'\n\n')
634 logger.info('Hint: To create a project, use: grond init <example> '
635 '<projectdir>')
637 elif op.exists(op.abspath(args[1])) and not options.force:
638 help_and_die(
639 parser,
640 'Directory "%s" already exists! Use --force to overwrite.'
641 % args[1])
642 else:
643 try:
644 grond_init.init_example(args[0], args[1], force=options.force)
645 except OSError as e:
646 print(str(e))
648 else:
649 sec = grond_init.get_content_snippet(args[0])
650 if not sec:
651 help_and_die(parser, 'Unknown snippet: %s' % args[0])
653 sys.stdout.write(sec)
656def command_init_old(args):
658 from . import cmd_init as init
660 def setup(parser):
661 parser.add_option(
662 '--targets', action='callback', dest='targets', type=str,
663 callback=multiple_choice, callback_kwargs={
664 'choices': ('waveforms', 'gnss', 'insar', 'all')
665 },
666 default='waveforms',
667 help='select from:'
668 ' waveforms, gnss and insar. '
669 '(default: --targets=%default,'
670 ' multiple selection by --targets=waveforms,gnss,insar)')
671 parser.add_option(
672 '--problem', dest='problem',
673 type='choice', choices=['cmt', 'rectangular'],
674 help='problem to generate: \'dc\' (double couple)'
675 ' or\'rectangular\' (rectangular finite fault)'
676 ' (default: \'%default\')')
677 parser.add_option(
678 '--force', dest='force', action='store_true',
679 help='overwrite existing project folder')
681 parser, options, args = cl_parse('init', args, setup)
683 try:
684 project = init.GrondProject()
686 if 'all' in options.targets:
687 targets = ['waveforms', 'gnss', 'insar']
688 else:
689 targets = options.targets
691 if not options.problem:
692 if 'insar' in targets or 'gnss' in targets:
693 problem = 'rectangular'
694 else:
695 problem = 'cmt'
696 else:
697 problem = options.problem
699 if problem == 'rectangular':
700 project.set_rectangular_source()
701 elif problem == 'cmt':
702 project.set_cmt_source()
704 if 'waveforms' in targets:
705 project.add_waveforms()
707 if 'insar' in targets:
708 project.add_insar()
710 if 'gnss' in targets:
711 project.add_gnss()
713 if len(args) == 1:
714 project_dir = args[0]
715 project.build(project_dir, options.force)
716 logger.info(CLIHints(
717 'init', project_dir=project_dir,
718 config=op.join(project_dir, 'config', 'config.gronf')))
719 else:
720 sys.stdout.write(project.dump())
722 except grond.GrondError as e:
723 die(str(e))
726def command_events(args):
727 def setup(parser):
728 pass
730 parser, options, args = cl_parse('events', args, setup)
731 if len(args) != 1:
732 help_and_die(parser, 'missing arguments')
734 config_path = args[0]
735 try:
736 config = grond.read_config(config_path)
738 for event_name in grond.get_event_names(config):
739 print(event_name)
741 except grond.GrondError as e:
742 die(str(e))
745def command_check(args):
747 from grond.environment import Environment
749 def setup(parser):
750 parser.add_option(
751 '--target-ids', dest='target_string_ids', metavar='TARGET_IDS',
752 help='process only selected targets. TARGET_IDS is a '
753 'comma-separated list of target IDs. Target IDs have the '
754 'form SUPERGROUP.GROUP.NETWORK.STATION.LOCATION.CHANNEL.')
756 parser.add_option(
757 '--waveforms', dest='show_waveforms', action='store_true',
758 help='show raw, restituted, projected, and processed waveforms')
760 parser.add_option(
761 '--nrandom', dest='n_random_synthetics', metavar='N', type=int,
762 default=10,
763 help='set number of random synthetics to forward model (default: '
764 '10). If set to zero, create synthetics for the reference '
765 'solution.')
767 parser.add_option(
768 '--save-stations-used', dest='stations_used_path',
769 metavar='FILENAME',
770 help='aggregate all stations used by the setup into a file')
772 details = '''
773Will first check if the configuration of the inversion problem and targets are
774consistent with the GF database (distance range, depth range, and, if
775applicable, frequency range). Then it will try to run *nrandom* forward models
776(default=10) to check if the modelling works and if it can find and read the
777input data.
778'''.strip()
780 parser, options, args = cl_parse('check', args, setup, details)
781 if len(args) < 1:
782 help_and_die(parser, 'missing arguments')
784 try:
785 env = Environment(args)
786 config = env.get_config()
788 target_string_ids = None
789 if options.target_string_ids:
790 target_string_ids = options.target_string_ids.split(',')
792 grond.check(
793 config,
794 event_names=env.get_selected_event_names(),
795 target_string_ids=target_string_ids,
796 show_waveforms=options.show_waveforms,
797 n_random_synthetics=options.n_random_synthetics,
798 stations_used_path=options.stations_used_path)
800 logger.info(CLIHints('check', config=env.get_config_path()))
802 except grond.GrondError as e:
803 die(str(e))
806def command_go(args):
808 from grond.environment import Environment
810 def setup(parser):
811 parser.add_option(
812 '--force', dest='force', action='store_true',
813 help='overwrite existing run directory')
814 parser.add_option(
815 '--preserve', dest='preserve', action='store_true',
816 help='preserve old rundir')
818 details = '''
819Runs the inversion as defined in the configuration file. <eventname> defines
820which event is inverted, replace <eventname> by the word `all` to invert all
821events included in the config file. Use option --parallel to run inversions of
822different events in parallel.
823'''.strip()
825 parser, options, args = cl_parse('go', args, setup, details)
827 try:
828 env = Environment(args)
830 grond.go(
831 env,
832 force=options.force,
833 preserve=options.preserve)
835 if len(env.get_selected_event_names()) == 1:
836 logger.info(CLIHints(
837 'go', rundir=env.get_rundir_path()))
839 except grond.GrondError as e:
840 die(str(e))
843def command_forward(args):
845 from grond.environment import Environment
847 def setup(parser):
848 parser.add_option(
849 '--show', dest='show', metavar='WHAT',
850 default='filtered',
851 choices=('filtered', 'processed'),
852 help='select whether to show only "filtered" or fully "processed" '
853 '(i.e. tapered) waveforms (default "%default").')
855 parser, options, args = cl_parse('forward', args, setup)
856 if len(args) < 1:
857 help_and_die(parser, 'missing arguments')
859 try:
860 env = Environment(args)
861 grond.forward(env, show=options.show)
862 except grond.GrondError as e:
863 die(str(e))
866def command_harvest(args):
867 def setup(parser):
868 parser.add_option(
869 '--force', dest='force', action='store_true',
870 help='overwrite existing harvest directory')
871 parser.add_option(
872 '--neach', dest='neach', type=int, default=10,
873 help='take NEACH best samples from each chain (default: %default)')
874 parser.add_option(
875 '--weed', dest='weed', type=int, default=0,
876 help='weed out bootstrap samples with bad global performance. '
877 '0: no weeding (default), '
878 '1: only bootstrap chains where all NEACH best samples '
879 'global misfit is less than the global average misfit of all '
880 'NEACH best in all chains plus one standard deviation are '
881 'included in the harvest ensemble, '
882 '2: same as 1 but additionally individual samples are '
883 'removed if their global misfit is greater than the global '
884 'average misfit of all NEACH best in all chains, '
885 '3: harvesting is done on the global chain only, bootstrap '
886 'chains are excluded')
887 parser.add_option(
888 '--export-fits', dest='export_fits', default='',
889 help='additionally export details about the fit of individual '
890 'targets. "best" - export fits of best model, "mean" - '
891 'export fits of ensemble mean model, "ensemble" - export '
892 'fits of all models in harvest ensemble.')
894 details = '''
895Can be called after running `grond go` to tweak the results extraction. The
896user can apply a weeding of bootstrap chains, and define how many best samples
897of each chain to consider. In addition, `grond harvest` can be used to retrieve
898the misfits of all targets for best, mean, or the ensemble of solutions.
899'''.strip()
901 parser, options, args = cl_parse('harvest', args, setup, details)
902 if len(args) < 1:
903 help_and_die(parser, 'no rundir')
905 export_fits = []
906 if options.export_fits.strip():
907 export_fits = [x.strip() for x in options.export_fits.split(',')]
909 for run_path in args:
910 try:
911 grond.harvest(
912 run_path,
913 force=options.force,
914 nbest=options.neach,
915 weed=options.weed,
916 export_fits=export_fits)
918 except grond.DirectoryAlreadyExists as e:
919 die(str(e) + '\n Use --force to overwrite.')
921 except grond.GrondError as e:
922 die(str(e))
925def command_cluster(args):
926 from grond import Clustering
927 from grond.clustering import metrics, methods, read_config, write_config
929 def setup(parser):
930 parser.add_option(
931 '--metric', dest='metric', metavar='METRIC',
932 default='kagan_angle',
933 choices=metrics.metrics,
934 help='metric to measure model distances. Choices: [%s]. Default: '
935 'kagan_angle' % ', '.join(metrics.metrics))
937 parser.add_option(
938 '--write-config',
939 dest='write_config',
940 metavar='FILE',
941 help='write configuration (or default configuration) to FILE')
943 method = args[0] if args else ''
944 try:
945 parser, options, args = cl_parse(
946 'cluster', args[1:], setup=Clustering.cli_setup(method, setup),
947 details='Available clustering methods: [%s]. Use '
948 '"grond cluster <method> --help" to get list of method '
949 'dependent options.' % ', '.join(methods))
951 if method not in Clustering.name_to_class and not op.exists(method):
952 help_and_die(
953 parser,
954 'no such clustering method: %s' % method if method else
955 'no clustering method specified')
957 if op.exists(method):
958 clustering = read_config(method)
959 else:
960 clustering = Clustering.cli_instantiate(method, options)
962 if options.write_config:
963 write_config(clustering, options.write_config)
964 else:
965 if len(args) != 1:
966 help_and_die(parser, 'no rundir')
967 run_path, = args
969 grond.cluster(run_path, clustering, metric=options.metric)
971 except grond.GrondError as e:
972 die(str(e))
975def command_plot(args):
977 def setup(parser):
978 parser.add_option(
979 '--show', dest='show', action='store_true',
980 help='show plot for interactive inspection')
982 details = '''
983Manually plot the inversion results. To get an overview of available plots that
984can be generated use the command: `grond plot list`. Plots are saved in
985directory `plots/` inside the run directory.
986'''.strip()
988 parser, options, args = cl_parse('plot', args, setup, details)
990 if not options.show:
991 import matplotlib
992 matplotlib.use('Agg')
994 from grond.environment import Environment
996 if len(args) not in (1, 2, 3):
997 help_and_die(parser, '1, 2 or 3 arguments required')
999 if len(args) > 1:
1000 env = Environment(args[1:])
1001 else:
1002 env = None
1004 from grond import plot
1005 if args[0] == 'list':
1007 def get_doc_title(doc):
1008 for ln in doc.split('\n'):
1009 ln = ln.strip()
1010 if ln != '':
1011 ln = ln.strip('.')
1012 return ln
1013 return 'Undocumented.'
1015 if env:
1016 plot_classes = env.get_plot_classes()
1017 else:
1018 plot_classes = plot.get_all_plot_classes()
1020 plot_names, plot_doc = zip(*[(pc.name, pc.__doc__)
1021 for pc in plot_classes])
1023 plot_descs = [get_doc_title(doc) for doc in plot_doc]
1024 left_spaces = max([len(pn) for pn in plot_names])
1026 for name, desc in zip(plot_names, plot_descs):
1027 print('{name:<{ls}} - {desc}'.format(
1028 ls=left_spaces, name=name, desc=desc))
1030 elif args[0] == 'config':
1031 plot_config_collection = plot.get_plot_config_collection(env)
1032 print(plot_config_collection)
1034 elif args[0] == 'all':
1035 if env is None:
1036 help_and_die(parser, 'two or three arguments required')
1037 plot_names = plot.get_plot_names(env)
1038 plot.make_plots(env, plot_names=plot_names, show=options.show)
1040 elif op.exists(args[0]):
1041 if env is None:
1042 help_and_die(parser, 'two or three arguments required')
1043 plots = plot.PlotConfigCollection.load(args[0])
1044 plot.make_plots(env, plots, show=options.show)
1046 else:
1047 if env is None:
1048 help_and_die(parser, 'two or three arguments required')
1049 plot_names = [name.strip() for name in args[0].split(',')]
1050 plot.make_plots(env, plot_names=plot_names, show=options.show)
1053def command_movie(args):
1055 import matplotlib
1056 matplotlib.use('Agg')
1058 def setup(parser):
1059 pass
1061 parser, options, args = cl_parse('movie', args, setup)
1063 if len(args) != 4:
1064 help_and_die(parser, 'four arguments required')
1066 run_path, xpar_name, ypar_name, movie_filename_template = args
1068 from grond import plot
1070 movie_filename = movie_filename_template % {
1071 'xpar': xpar_name,
1072 'ypar': ypar_name}
1074 try:
1075 plot.make_movie(run_path, xpar_name, ypar_name, movie_filename)
1077 except grond.GrondError as e:
1078 die(str(e))
1081def command_export(args):
1083 def setup(parser):
1084 parser.add_option(
1085 '--type', dest='type', metavar='TYPE',
1086 choices=('event', 'event-yaml', 'source', 'vector'),
1087 help='select type of objects to be exported. Choices: '
1088 '"event" (default), "event-yaml", "source", "vector".')
1090 parser.add_option(
1091 '--parameters', dest='parameters', metavar='PLIST',
1092 help='select parameters to be exported. PLIST is a '
1093 'comma-separated list where each entry has the form '
1094 '"<parameter>[.<measure>]". Available measures: "best", '
1095 '"mean", "std", "minimum", "percentile16", "median", '
1096 '"percentile84", "maximum".')
1098 parser.add_option(
1099 '--selection', dest='selection', metavar='EXPRESSION',
1100 help='only export data for runs which match EXPRESSION. '
1101 'Example expression: "tags_contains:excellent,good"')
1103 parser.add_option(
1104 '--output', dest='filename', metavar='FILE',
1105 help='write output to FILE')
1107 parser.add_option(
1108 '--effective-lat-lon', dest='effective_lat_lon',
1109 action='store_true',
1110 help='convert north_shift/east_shift offsets to true lat/lon '
1111 'coordinates (when outputting event objects).')
1113 parser, options, args = cl_parse('export', args, setup)
1114 if len(args) < 2:
1115 help_and_die(parser, 'arguments required')
1117 what = args[0]
1119 dirnames = args[1:]
1121 what_choices = ('best', 'mean', 'ensemble', 'stats')
1123 if what not in what_choices:
1124 help_and_die(
1125 parser,
1126 'invalid choice: %s (choose from %s)' % (
1127 repr(what), ', '.join(repr(x) for x in what_choices)))
1129 if options.parameters:
1130 pnames = options.parameters.split(',')
1131 else:
1132 pnames = None
1134 try:
1135 grond.export(
1136 what,
1137 dirnames,
1138 filename=options.filename,
1139 type=options.type,
1140 pnames=pnames,
1141 selection=options.selection,
1142 effective_lat_lon=options.effective_lat_lon)
1144 except grond.GrondError as e:
1145 die(str(e))
1148def command_tag(args):
1150 def setup(parser):
1151 parser.add_option(
1152 '-d', '--dir-names',
1153 dest='show_dirnames',
1154 action='store_true',
1155 help='show directory names instead of run names')
1157 parser, options, args = cl_parse('tag', args, setup)
1158 if len(args) < 2:
1159 help_and_die(parser, 'two or more arguments required')
1161 action = args.pop(0)
1163 if action not in ('add', 'remove', 'list'):
1164 help_and_die(parser, 'invalid action: %s' % action)
1166 if action in ('add', 'remove'):
1167 if len(args) < 2:
1168 help_and_die(parser, 'three or more arguments required')
1170 tag = args.pop(0)
1172 rundirs = args
1174 if action == 'list':
1175 rundirs = args
1177 from grond.environment import Environment
1179 errors = False
1180 for rundir in rundirs:
1181 try:
1182 env = Environment([rundir])
1183 if options.show_dirnames:
1184 name = rundir
1185 else:
1186 name = env.get_problem().name
1188 info = env.get_run_info()
1189 if action == 'add':
1190 info.add_tag(tag)
1191 env.set_run_info(info)
1192 elif action == 'remove':
1193 info.remove_tag(tag)
1194 env.set_run_info(info)
1195 elif action == 'list':
1196 print('%-60s : %s' % (
1197 name,
1198 ', '.join(info.tags)))
1200 except grond.GrondError as e:
1201 errors = True
1202 logger.error(e)
1204 if errors:
1205 die('Errors occurred, see log messages above.')
1208def make_report(env_args, event_name, conf, update_without_plotting):
1209 from grond.environment import Environment
1210 from grond.report import report
1211 try:
1212 env = Environment(env_args)
1213 if event_name:
1214 env.set_current_event_name(event_name)
1216 report(
1217 env, conf,
1218 update_without_plotting=update_without_plotting,
1219 make_index=False,
1220 make_archive=False)
1222 env.reset()
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 '--archive',
1300 dest='make_archive',
1301 default=None,
1302 action='store_true',
1303 help='create archive file, even if `make_archive` in the report '
1304 'configuration file is set to `false`. The config file '
1305 'default creates no archive.')
1306 parser.add_option(
1307 '--no-archive',
1308 dest='make_archive',
1309 default=None,
1310 action='store_false',
1311 help='do not create archive file, even if `make_archive` in the '
1312 'report configuration file is set to `true`. The config file '
1313 'default creates no archive.')
1315 parser, options, args = cl_parse('report', args, setup)
1317 s_conf = ''
1318 if options.config:
1319 try:
1320 conf = read_config(options.config)
1321 except grond.GrondError as e:
1322 die(str(e))
1324 s_conf = ' --config="%s"' % options.config
1325 else:
1326 from grond import plot
1327 conf = ReportConfig(
1328 plot_config_collection=plot.get_plot_config_collection())
1329 conf.set_basepath('.')
1331 if options.write_config:
1332 try:
1333 write_config(conf, options.write_config)
1334 sys.exit(0)
1336 except grond.GrondError as e:
1337 die(str(e))
1339 # commandline options that can override config values
1340 if options.make_archive is not None:
1341 conf.make_archive = options.make_archive
1343 if len(args) == 1 and op.exists(op.join(args[0], 'index.html')):
1344 conf.report_base_path = conf.rel_path(args[0])
1345 s_conf = ' %s' % args[0]
1346 args = []
1348 report_base_path = conf.expand_path(conf.report_base_path)
1350 if options.index_only:
1351 report_index(conf)
1352 report_archive(conf)
1353 args = []
1355 entries_generated = False
1357 payload = []
1358 if args and all(op.isdir(rundir) for rundir in args):
1359 rundirs = args
1360 all_failed = True
1361 for rundir in rundirs:
1362 payload.append((
1363 [rundir], None, conf, options.update_without_plotting))
1365 elif args:
1366 try:
1367 env = Environment(args)
1368 for event_name in env.get_selected_event_names():
1369 payload.append((
1370 args, event_name, conf, options.update_without_plotting))
1372 except grond.GrondError as e:
1373 die(str(e))
1375 if payload:
1376 entries_generated = []
1377 for result in parimap.parimap(
1378 make_report, *zip(*payload), nprocs=options.nparallel):
1380 entries_generated.append(result)
1382 all_failed = not any(entries_generated)
1383 entries_generated = any(entries_generated)
1385 if all_failed:
1386 die('no report entries generated')
1388 report_index(conf)
1389 report_archive(conf)
1391 if options.serve or options.serve_external:
1392 if options.serve_external:
1393 host = 'default'
1394 else:
1395 host = options.host
1397 addr = serve_ip(host), options.port
1399 serve_report(
1400 addr,
1401 report_config=conf,
1402 fixed_port=options.fixed_port or options.serve_external,
1403 open=options.open)
1405 elif options.open:
1406 import webbrowser
1407 url = 'file://%s/index.html' % op.abspath(report_base_path)
1408 webbrowser.open(url)
1410 else:
1411 if not entries_generated and not options.index_only:
1412 logger.info('Nothing to do, see: grond report --help')
1414 if entries_generated and not (options.serve or options.serve_external):
1415 logger.info(CLIHints('report', config=s_conf))
1418def command_qc_polarization(args):
1420 def setup(parser):
1421 parser.add_option(
1422 '--time-factor-pre', dest='time_factor_pre', type=float,
1423 metavar='NUMBER',
1424 default=0.5,
1425 help='set duration to extract before synthetic P phase arrival, '
1426 'relative to 1/fmin. fmin is taken from the selected target '
1427 'group in the config file (default=%default)')
1428 parser.add_option(
1429 '--time-factor-post', dest='time_factor_post', type=float,
1430 metavar='NUMBER',
1431 default=0.5,
1432 help='set duration to extract after synthetic P phase arrival, '
1433 'relative to 1/fmin. fmin is taken from the selected target '
1434 'group in the config file (default=%default)')
1435 parser.add_option(
1436 '--distance-min', dest='distance_min', type=float,
1437 metavar='NUMBER',
1438 help='minimum event-station distance [m]')
1439 parser.add_option(
1440 '--distance-max', dest='distance_max', type=float,
1441 metavar='NUMBER',
1442 help='maximum event-station distance [m]')
1443 parser.add_option(
1444 '--depth-min', dest='depth_min', type=float,
1445 metavar='NUMBER',
1446 help='minimum station depth [m]')
1447 parser.add_option(
1448 '--depth-max', dest='depth_max', type=float,
1449 metavar='NUMBER',
1450 help='maximum station depth [m]')
1451 parser.add_option(
1452 '--picks', dest='picks_filename',
1453 metavar='FILENAME',
1454 help='add file with P picks in Snuffler marker format')
1455 parser.add_option(
1456 '--save', dest='output_filename',
1457 metavar='FILENAME.FORMAT',
1458 help='save output to file FILENAME.FORMAT')
1459 parser.add_option(
1460 '--dpi', dest='output_dpi', type=float, default=120.,
1461 metavar='NUMBER',
1462 help='DPI setting for raster formats (default=120)')
1464 parser, options, args = cl_parse('qc-polarization', args, setup)
1465 if len(args) != 3:
1466 help_and_die(parser, 'missing arguments')
1468 if options.output_filename:
1469 import matplotlib
1470 matplotlib.use('Agg')
1472 import grond.qc
1474 config_path, event_name, target_group_path = args
1476 try:
1477 config = grond.read_config(config_path)
1478 except grond.GrondError as e:
1479 die(str(e))
1481 ds = config.get_dataset(event_name)
1483 engine = config.engine_config.get_engine()
1485 nsl_to_time = None
1486 if options.picks_filename:
1487 markers = marker.load_markers(options.picks_filename)
1488 marker.associate_phases_to_events(markers)
1490 nsl_to_time = {}
1491 for m in markers:
1492 if isinstance(m, marker.PhaseMarker):
1493 ev = m.get_event()
1494 if ev is not None and ev.name == event_name:
1495 nsl_to_time[m.one_nslc()[:3]] = m.tmin
1497 if not nsl_to_time:
1498 help_and_die(
1499 parser,
1500 'no markers associated with event "%s" found in file "%s"' % (
1501 event_name, options.picks_filename))
1503 target_group_paths_avail = []
1504 for target_group in config.target_groups:
1505 name = target_group.path
1506 if name == target_group_path:
1507 imc = target_group.misfit_config
1508 fmin = imc.fmin
1509 fmax = imc.fmax
1510 ffactor = imc.ffactor
1512 store = engine.get_store(target_group.store_id)
1513 timing = '{cake:P|cake:p|cake:P\\|cake:p\\}'
1515 grond.qc.polarization(
1516 ds, store, timing, fmin=fmin, fmax=fmax, ffactor=ffactor,
1517 time_factor_pre=options.time_factor_pre,
1518 time_factor_post=options.time_factor_post,
1519 distance_min=options.distance_min,
1520 distance_max=options.distance_max,
1521 depth_min=options.depth_min,
1522 depth_max=options.depth_max,
1523 nsl_to_time=nsl_to_time,
1524 output_filename=options.output_filename,
1525 output_dpi=options.output_dpi)
1527 return
1529 target_group_paths_avail.append(name)
1531 die('no target group with path "%s" found. Available: %s' % (
1532 target_group_path, ', '.join(target_group_paths_avail)))
1535def command_upgrade_config(args):
1536 def setup(parser):
1537 parser.add_option(
1538 '--diff', dest='diff', action='store_true',
1539 help='create diff between normalized old and new versions')
1541 parser, options, args = cl_parse('upgrade-config', args, setup)
1542 if len(args) != 1:
1543 help_and_die(parser, 'missing argument <configfile>')
1545 from grond import upgrade
1546 upgrade.upgrade_config_file(args[0], diff=options.diff)
1549def command_diff(args):
1550 def setup(parser):
1551 pass
1553 parser, options, args = cl_parse('diff', args, setup)
1554 if len(args) != 2:
1555 help_and_die(parser, 'requires exactly two arguments')
1557 from grond.config import diff_configs
1558 diff_configs(*args)
1561def command_version(args):
1562 def setup(parser):
1563 parser.add_option(
1564 '--short', dest='short', action='store_true',
1565 help='only print Grond\'s version number')
1566 parser.add_option(
1567 '--failsafe', dest='failsafe', action='store_true',
1568 help='do not get irritated when some dependencies are missing')
1570 parser, options, args = cl_parse('version', args, setup)
1572 if options.short:
1573 print(grond.__version__)
1574 return
1576 elif not options.failsafe:
1577 from grond import info
1578 print(info.version_info())
1579 return
1581 print("grond: %s" % grond.__version__)
1583 try:
1584 import pyrocko
1585 print('pyrocko: %s' % pyrocko.long_version)
1586 except ImportError:
1587 print('pyrocko: N/A')
1589 try:
1590 import numpy
1591 print('numpy: %s' % numpy.__version__)
1592 except ImportError:
1593 print('numpy: N/A')
1595 try:
1596 import scipy
1597 print('scipy: %s' % scipy.__version__)
1598 except ImportError:
1599 print('scipy: N/A')
1601 try:
1602 import matplotlib
1603 print('matplotlib: %s' % matplotlib.__version__)
1604 except ImportError:
1605 print('matplotlib: N/A')
1607 try:
1608 from pyrocko.gui.qt_compat import Qt
1609 print('PyQt: %s' % Qt.PYQT_VERSION_STR)
1610 print('Qt: %s' % Qt.QT_VERSION_STR)
1611 except ImportError:
1612 print('PyQt: N/A')
1613 print('Qt: N/A')
1615 import sys
1616 print('python: %s.%s.%s' % sys.version_info[:3])
1618 if not options.failsafe:
1619 die('fell back to failsafe version printing')
1622if __name__ == '__main__':
1623 main()