pyatoa 0.3.1__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {pyatoa-0.3.1 → pyatoa-0.4.2}/PKG-INFO +1 -1
  2. {pyatoa-0.3.1 → pyatoa-0.4.2}/docs/conf.py +1 -1
  3. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/core/inspector.py +431 -33
  4. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/core/manager.py +182 -125
  5. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/test_asdf_utils.py +2 -2
  6. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/test_inspector.py +2 -25
  7. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/test_manager.py +17 -27
  8. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/test_process_util.py +49 -9
  9. pyatoa-0.4.2/pyatoa/utils/asdf/add.py +239 -0
  10. pyatoa-0.4.2/pyatoa/utils/asdf/clean.py +168 -0
  11. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/asdf/load.py +24 -13
  12. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/form.py +20 -10
  13. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/process.py +52 -29
  14. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/visuals/insp_plot.py +572 -204
  15. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/visuals/wave_maker.py +11 -10
  16. pyatoa-0.4.2/pyatoa/visuals/wave_train.py +348 -0
  17. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa.egg-info/PKG-INFO +1 -1
  18. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa.egg-info/SOURCES.txt +2 -3
  19. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyproject.toml +1 -1
  20. pyatoa-0.3.1/pyatoa/utils/asdf/add.py +0 -121
  21. pyatoa-0.3.1/pyatoa/utils/asdf/clean.py +0 -101
  22. pyatoa-0.3.1/pyatoa/visuals/comp_wave.py +0 -586
  23. pyatoa-0.3.1/pyatoa/visuals/improve_wave.py +0 -486
  24. {pyatoa-0.3.1 → pyatoa-0.4.2}/LICENSE.txt +0 -0
  25. {pyatoa-0.3.1 → pyatoa-0.4.2}/README.md +0 -0
  26. {pyatoa-0.3.1 → pyatoa-0.4.2}/docs/__init__.py +0 -0
  27. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/__init__.py +0 -0
  28. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/core/__init__.py +0 -0
  29. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/core/config.py +0 -0
  30. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/core/executive.py +0 -0
  31. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/scripts/__init__.py +0 -0
  32. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/scripts/data_data_example.py +0 -0
  33. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/scripts/load_example_data.py +0 -0
  34. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/scripts/process_data_w_mpi.py +0 -0
  35. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/scripts/syn_syn_example.py +0 -0
  36. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/__init__.py +0 -0
  37. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/test_config.py +0 -0
  38. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/test_executive.py +0 -0
  39. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/test_mgmt_plot.py +0 -0
  40. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/test_utils.py +0 -0
  41. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/tests/test_wave_maker.py +0 -0
  42. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/__init__.py +0 -0
  43. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/adjoint.py +0 -0
  44. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/asdf/__init__.py +0 -0
  45. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/asdf/write.py +0 -0
  46. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/calculate.py +0 -0
  47. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/images.py +0 -0
  48. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/srcrcv.py +0 -0
  49. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/window.py +0 -0
  50. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/utils/write.py +0 -0
  51. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/visuals/__init__.py +0 -0
  52. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa/visuals/map_maker.py +0 -0
  53. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa.egg-info/dependency_links.txt +0 -0
  54. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa.egg-info/requires.txt +0 -0
  55. {pyatoa-0.3.1 → pyatoa-0.4.2}/pyatoa.egg-info/top_level.txt +0 -0
  56. {pyatoa-0.3.1 → pyatoa-0.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyatoa
3
- Version: 0.3.1
3
+ Version: 0.4.2
4
4
  Summary: Python's Adjoint Tomography Operations Assistant
5
5
  Author: adjTomo Dev Team
6
6
  Author-email: adjtomo@gmail.com
@@ -21,7 +21,7 @@ import sphinx_rtd_theme
21
21
  # -- Project information -----------------------------------------------------
22
22
 
23
23
  project = 'Pyatoa'
24
- copyright = '2023, adjTomo'
24
+ copyright = '2024, adjTomo'
25
25
  author = 'adjTomo Dev Team'
26
26
 
27
27
  # The short X.Y version
@@ -6,6 +6,7 @@ using Pandas.
6
6
  import os
7
7
  import pyasdf
8
8
  import traceback
9
+ import matplotlib.pyplot as plt
9
10
  import numpy as np
10
11
  import pandas as pd
11
12
  from glob import glob
@@ -26,7 +27,7 @@ class Inspector(InspectorPlotter):
26
27
  Inherits plotting capabilities from InspectorPlotter class to reduce clutter
27
28
  """
28
29
 
29
- def __init__(self, tag="default", verbose=True):
30
+ def __init__(self, tag="inspector", verbose=False):
30
31
  """
31
32
  Inspector will automatically search for relevant file names using the
32
33
  tag attribute. If nothing is found, internal dataframes will be empty.
@@ -41,7 +42,6 @@ class Inspector(InspectorPlotter):
41
42
  self.sources = pd.DataFrame()
42
43
  self.receivers = pd.DataFrame()
43
44
  self.tag = tag
44
- self.verbose = verbose
45
45
 
46
46
  # Placeholder attributes for getters
47
47
  self._models = None
@@ -50,6 +50,11 @@ class Inspector(InspectorPlotter):
50
50
  self._event_misfit = None
51
51
  self._station_misfit = None
52
52
 
53
+ if verbose:
54
+ logger.setLevel("DEBUG")
55
+ else:
56
+ logger.setLevel("CRITICAL")
57
+
53
58
  # Try to load an already created Inspector
54
59
  try:
55
60
  self.read(tag=self.tag)
@@ -65,7 +70,8 @@ class Inspector(InspectorPlotter):
65
70
  str_out = (f"{len(self.events):<4} event(s)\n"
66
71
  f"{len(self.stations):<4} station(s)\n"
67
72
  f"{len(self.iterations):<4} iteration(s)\n"
68
- f"{self.evaluations:<4} evaluation(s)")
73
+ f"{self.evaluations:<4} evaluation(s)"
74
+ )
69
75
 
70
76
  except KeyError:
71
77
  str_out = (f"{0:<4} event(s)\n"
@@ -236,6 +242,252 @@ class Inspector(InspectorPlotter):
236
242
  """Return a dictionary of event depths in units of meters"""
237
243
  return self._try_print("depth_km")
238
244
 
245
+ def generate_report(self, path_report=None, iteration=None,
246
+ step_count=None, geographic=True, outliers=True,
247
+ scatter=True, summary=True, nstd=2,
248
+ dpi=200, **kwargs):
249
+ """
250
+ An aggregate function that generates a "report" by creating a number
251
+ of figures that summarize the misfit of the inversion. Makes it easier
252
+ for the User as they don't have to remember each of the functions in
253
+ the Inspector's wheelhouse, all relevant figures will be generated
254
+ automatically.
255
+
256
+ :type path_report: str
257
+ :param path_report: The path where the report will be saved.
258
+ Defaults to "./report".
259
+ :type iteration: int
260
+ :param iteration: The iteration number.
261
+ :type step_count: int
262
+ :param step_count: The step count.
263
+ :type geographic: bool
264
+ :param geographic: If True, includes geographic data in the report.
265
+ Defaults to True.
266
+ :type outliers: bool
267
+ :param outliers: If True, includes outliers in the report.
268
+ Defaults to True.
269
+ :type scatter: bool
270
+ :param scatter: If True, includes a scatter plot in the report.
271
+ Defaults to True.
272
+ :type summary: bool
273
+ :param summary: If True, includes a summary in the report.
274
+ Defaults to True.
275
+ :type nstd: int
276
+ :param nstd: The number of standard deviations for outlier detection.
277
+ Defaults to 2.
278
+ :type dpi: int
279
+ :param dpi: The resolution in dots per inch for the figures in the
280
+ report. Defaults to 200.
281
+ :type kwargs: dict
282
+ :param kwargs: Additional keyword arguments.
283
+ """
284
+ # By default we generate a report for the final model
285
+ iteration, step_count = self.validate_evaluation(iteration, step_count,
286
+ choice="final")
287
+ if path_report is None:
288
+ path_report = f"./report_{iteration}{step_count}"
289
+ if not os.path.exists(path_report):
290
+ os.makedirs(path_report)
291
+
292
+ # Generate some geographic information
293
+ if geographic:
294
+ geographic_plot_functions = ["map", "travel_times", "raypaths",
295
+ "event_depths"]
296
+ for plot_function in geographic_plot_functions:
297
+ save = os.path.join(path_report, f"{plot_function}.png")
298
+ if os.path.exists(save):
299
+ continue
300
+ getattr(self, plot_function)(iteration=iteration,
301
+ step_count=step_count,
302
+ show=False, dpi=dpi)
303
+ plt.close()
304
+
305
+ # Plot misfit spider plots of event misfit for events that are outside
306
+ # N standard deviations of the mean w.r.t misfit value
307
+ if outliers:
308
+ upper_outliers, lower_outliers, mean ,std = \
309
+ self.event_outliers(iteration, step_count, nstd=nstd)
310
+
311
+ if upper_outliers.empty and lower_outliers.empty:
312
+ logger.warning("No outliers found, skipping outlier plots, "
313
+ "reduce `nstd` to reevaluate for outliers")
314
+ else:
315
+ for outliers, tag in zip([upper_outliers, lower_outliers],
316
+ ["upper_outlier", "lower_outlier"]):
317
+ for event_name in outliers.index.to_list():
318
+ self.event_station_misfit_map(
319
+ event=event_name, iteration=iteration,
320
+ step_count=step_count,
321
+ save=os.path.join(path_report,
322
+ f"{tag}_{event_name}.png"),
323
+ show=False, dpi=dpi
324
+ )
325
+ plt.close()
326
+
327
+ # Create a few scatterplots comparing some parameters
328
+ if scatter:
329
+ for xy in [
330
+ ("distance_km", "cc_shift_in_seconds"),
331
+ ("backazimuth", "cc_shift_in_seconds"),
332
+ ("length_s", "cc_shift_in_seconds"),
333
+ ]:
334
+ x, y = xy
335
+ self.scatter(x=x, y=y, show=False, dpi=dpi,
336
+ save=os.path.join(path_report, f"{x}_v_{y}.png")
337
+ )
338
+
339
+ # Plot summary figures that show the status of inversion holistically
340
+ if summary:
341
+ self.convergence(normalize=True, show=False, dpi=dpi,
342
+ save=os.path.join(path_report, "convergence.png")
343
+ )
344
+
345
+ summary_functions = ["event_station_hist2d", "event_comparison",
346
+ "window_stack", "histogram_summary"]
347
+ for plot_function in summary_functions:
348
+ save = os.path.join(path_report, f"{plot_function}.png")
349
+ if os.path.exists(save):
350
+ continue
351
+ # We want the histogram summary to be comparative
352
+ if plot_function == "histogram_summary":
353
+ getattr(self, plot_function)(
354
+ iteration="i01", step_count="s00",
355
+ iteration_comp=iteration, step_count_comp=step_count,
356
+ save=save, show=False, dpi=dpi
357
+ )
358
+ else:
359
+ getattr(self, plot_function)(
360
+ iteration=iteration, step_count=step_count, save=save,
361
+ show=False, dpi=dpi
362
+ )
363
+ plt.close()
364
+
365
+ plt.close("all")
366
+
367
+ self.generate_report_text(path_report, nstd)
368
+
369
+ def generate_report_text(self, path_report="./", nstd=1):
370
+ """
371
+ Generate a text report highlighting good/bad performing events and
372
+ stations that will provide the User a quickly accessible summary of
373
+ their inversion and may motivate looking at some waveforms
374
+ """
375
+ line_break = "\n" + "=" * 80 +"\n"
376
+ iter_end, step_end = self.validate_evaluation(
377
+ iteration=None, step_count=None, choice="final"
378
+ )
379
+
380
+ # Get event mean and std for current evaluation
381
+ _, _, mean ,std = \
382
+ self.event_outliers(iter_end, step_end, nstd=nstd)
383
+
384
+ # Get an ascended list of misfit/windows per event
385
+ _windows = self.windows
386
+ self.windows = self.isolate(iteration=iter_end, step_count=step_end)
387
+
388
+ # Event specific window information
389
+ win_per_event = self.nwin(level="event")
390
+ avg_win_per_event = win_per_event.nwin.mean()
391
+
392
+ _tenwin = win_per_event.iloc[0].nwin
393
+ top_event_by_win = win_per_event.iloc[0].name[-1]
394
+
395
+ _benwin = win_per_event.iloc[-1].nwin
396
+ bot_event_by_win = win_per_event.iloc[-1].name[-1]
397
+
398
+ win_per_event_str = win_per_event.to_string()
399
+
400
+ # Station specific window information
401
+ win_per_station = self.nwin(level="station")
402
+ avg_win_per_station = win_per_station.nwin.mean()
403
+
404
+ _tsnwin = win_per_station.iloc[0].nwin
405
+ top_sta_by_win = win_per_station.iloc[0].name[-1]
406
+
407
+ _bsnwin = win_per_station.iloc[-1].nwin
408
+ bot_sta_by_win = win_per_station.iloc[-1].name[-1]
409
+
410
+ win_per_station_str = win_per_station.to_string()
411
+
412
+ # Event specific misfit information
413
+ misfit_per_event = self.misfit(level="event", reset=True)
414
+
415
+ _temsft = misfit_per_event.iloc[0].misfit
416
+ highest_misfit_event = misfit_per_event.iloc[0].name[-1]
417
+
418
+ _bemsft = misfit_per_event.iloc[-1].misfit
419
+ lowest_misfit_event = misfit_per_event.iloc[-1].name[-1]
420
+
421
+ misfit_per_event_str = misfit_per_event.sort_values(
422
+ "misfit", ascending=False).to_string()
423
+
424
+ # Station specific misfit information
425
+ misfit_per_sta = self.misfit(level="station", reset=True)
426
+
427
+ _tsmsft = misfit_per_sta.iloc[0].misfit
428
+ highest_misfit_sta = misfit_per_sta.iloc[0].name[-1]
429
+
430
+ _bsmsft = misfit_per_sta.iloc[-1].misfit
431
+ lowest_misfit_sta = misfit_per_sta.iloc[-1].name[-1]
432
+
433
+ misfit_per_sta_str = misfit_per_sta.sort_values(
434
+ "misfit", ascending=False).to_string()
435
+
436
+ # Get windows per component for the current evaluation
437
+ _windows_eval = self.windows
438
+ win_str = ""
439
+ for component in self.windows.component.unique():
440
+ self.windows = self.isolate(component=component)
441
+ # Sort of a hacky way of getting what we know is a single value
442
+ nwin_per_comp = self.nwin().nwin.to_list()[0]
443
+ win_str += f"- {component}: {nwin_per_comp}\n"
444
+ self.windows = _windows_eval
445
+
446
+ # Restore the original windows, just incase but likely not needed?
447
+ self.windows = _windows
448
+
449
+ # Compile all the above information into a nice text output to be writ
450
+ srcrcv_summary = (
451
+ f"Avg windows per event: {avg_win_per_event:.2f}\n"
452
+ f"- Evt w/ max win: {top_event_by_win} ({_tenwin:.0f})\n"
453
+ f"- Evt w/ min win: {bot_event_by_win} ({_benwin:.0f})\n"
454
+ f"- Evt w/ max msft: {highest_misfit_event} ({_temsft:.2f})\n"
455
+ f"- Evt w/ min msft: {lowest_misfit_event} ({_bemsft:.2f})\n"
456
+ "\n"
457
+ f"Avg windows per sta: {avg_win_per_station:.2f}\n"
458
+ f"- Sta w/ max win: {top_sta_by_win} ({_tsnwin:.0f})\n"
459
+ f"- Sta w/ min win: {bot_sta_by_win} ({_bsnwin:.0f})\n"
460
+ f"- Sta w/ max msft: {highest_misfit_sta} ({_tsmsft:.2f})\n"
461
+ f"- Sta w/ min msft: {lowest_misfit_sta} ({_bsmsft:.2f})\n"
462
+ "\n"
463
+ f"Windows per component:\n"
464
+ f"{win_str}"
465
+ )
466
+
467
+ # Header contains general information for understanding inversion
468
+ report = [
469
+ f"{'INSPECTOR REPORT':^80}",
470
+ f"{'SUMMARY':^80}",
471
+ f"{self._get_str()}",
472
+ f"{f'SRCRCV SUMMARY [{iter_end}{step_end}]':^80}",
473
+ f"{srcrcv_summary}",
474
+ f"{'TOTAL WINDOWS':^80}",
475
+ f"{self.nwin().to_string()}",
476
+ f"{'TOTAL MISFIT':^80}",
477
+ f"{self.misfit().to_string()}",
478
+ f"{'WINDOWS PER EVENT':^80}",
479
+ f"{win_per_event_str}",
480
+ f"{f'MISFIT PER EVENT (MEAN={mean:.2f}, {nstd}STD={std:.2f})':^80}",
481
+ f"{misfit_per_event_str}",
482
+ f"{'WINDOWS PER STATION':^80}",
483
+ f"{win_per_station_str}",
484
+ f"{f'MISFIT PER STATION':^80}",
485
+ f"{misfit_per_sta_str}",
486
+ ]
487
+
488
+ with open(os.path.join(path_report, "inspector_report.txt"), "w") as f:
489
+ f.writelines(f"{line_break}".join(report))
490
+
239
491
  def _get_srcrcv_from_dataset(self, ds):
240
492
  """
241
493
  Get source and receiver information from dataset, this includes
@@ -336,12 +588,13 @@ class Inspector(InspectorPlotter):
336
588
  # where no step count is given (e.g., iteration == 'default')
337
589
  iters, steps = [], []
338
590
  for iter_ in misfit_windows.list():
339
- iters.append(iter_)
340
591
  for step in misfit_windows[iter_].list():
341
592
  # Ensure that step counts are formatted like: 's00'
342
593
  # if not then we DONT have step counts in the dataset
343
594
  if not step.startswith("s") and not len(step) == 3:
344
595
  step = ""
596
+
597
+ iters.append(iter_)
345
598
  steps.append(step)
346
599
 
347
600
  # Pulling out important information from the windows and adj src.
@@ -377,8 +630,7 @@ class Inspector(InspectorPlotter):
377
630
  window["misfit"].append(adjoint_source_eval[
378
631
  adj_tag].parameters["misfit"])
379
632
  except IndexError:
380
- if self.verbose:
381
- print(f"No matching adjoint source for {cha_id}")
633
+ logger.warning(f"No matching adjoint source for {cha_id}")
382
634
  window["misfit"].append(np.nan)
383
635
 
384
636
  # winfo keys match the keys of the Pyflex Window objects
@@ -407,35 +659,51 @@ class Inspector(InspectorPlotter):
407
659
  self.windows = pd.concat([self.windows, pd.DataFrame(window)],
408
660
  ignore_index=True)
409
661
 
410
- def _parse_nonetype_eval(self, iteration, step_count):
662
+ def validate_evaluation(self, iteration, step_count, choice="final"):
411
663
  """
664
+ Provide acceptable values for 'iteration' and 'step_count' to underlying
665
+ functions that require it.
412
666
  Whenever a user does not choose an iteration or step count, e.g., in
413
667
  plotting functions, this function defines default values based on the
414
668
  initial model (if neither given), or the last step count for a given
415
- iteration (if only iteration is given). Only step count is not allowed
669
+ iteration (if only iteration is given). Only step count is not allowed.
670
+ If both iteration and step count are provided, just check that these
671
+ are acceptable values
416
672
 
417
673
  :type iteration: str
418
674
  :param iteration: chosen iteration, formatted as e.g., 'i01'
419
675
  :type step_count: str
420
676
  :param step_count: chosen step count, formatted as e.g., 's00'
677
+ :type choice: str
678
+ :param choice: 'initial' or 'final' to set the default behavior of
679
+ NoneType iteration and step_count returning either the initial
680
+ model or the final model evaluation
421
681
  :rtype: tuple of str
422
682
  :return: (iteration, step_count) default values for the iteration
423
683
  and step_count
424
684
  """
425
685
  # Default iteration and step count if None are given
426
686
  if iteration is None and step_count is None:
427
- iteration, step_count = self.initial_model
428
- print(f"No iteration or step count given, defaulting to initial "
429
- f"model: {iteration}{step_count}")
687
+ if choice == "initial":
688
+ iteration, step_count = self.initial_model
689
+ elif choice == "final":
690
+ iteration, step_count = self.final_model
691
+ logger.debug(f"No iteration or step count given, defaulting to "
692
+ f"{choice} model: {iteration}{step_count}")
430
693
  elif iteration and (step_count is None):
431
694
  step_count = self.steps[iteration][-1]
432
- print(f"No step count given, defaulting to final step count within"
433
- f"given iteration: {iteration}{step_count}")
695
+
696
+ logger.debug(f"No step count given, defaulting to final step count "
697
+ f"within given iteration: {iteration}{step_count}")
434
698
  elif (iteration is None) and (step_count is not None):
435
699
  raise ValueError("'step_count' cannot be provided by itself, you "
436
700
  "must also set the variable: 'iteration'")
701
+ else:
702
+ assert (iteration in self.iterations and
703
+ step_count in self.steps[iteration]), \
704
+ f"{iteration}{step_count} does not exist in Inspector"
437
705
  return iteration, step_count
438
-
706
+
439
707
  def discover(self, path="./", ignore_symlinks=True):
440
708
  """
441
709
  Allow the Inspector to scour through a path and find relevant files,
@@ -452,18 +720,17 @@ class Inspector(InspectorPlotter):
452
720
  if ignore_symlinks:
453
721
  dsfids = [_ for _ in dsfids if not os.path.islink(_)]
454
722
  for i, dsfid in enumerate(dsfids):
455
- if self.verbose:
456
- print(f"{os.path.basename(dsfid):<25} "
457
- f"{i+1:0>3}/{len(dsfids):0>3}", end="..."
458
- )
723
+
459
724
  try:
460
725
  self.append(dsfid)
461
- if self.verbose:
462
- print("done")
726
+ logger.info(f"{os.path.basename(dsfid):<25} "
727
+ f"{i + 1:0>3}/{len(dsfids):0>3}: done",
728
+ )
463
729
  except KeyError as e:
464
- if self.verbose:
465
- print(f"error: {e}")
466
- traceback.print_exc()
730
+ logger.info(f"{os.path.basename(dsfid):<25} "
731
+ f"{i + 1:0>3}/{len(dsfids):0>3}: error {e}",
732
+ )
733
+ traceback.print_exc()
467
734
  continue
468
735
 
469
736
  return self
@@ -489,13 +756,11 @@ class Inspector(InspectorPlotter):
489
756
  try:
490
757
  self._get_windows_from_dataset(ds)
491
758
  except AttributeError as e:
492
- if self.verbose:
493
- print("error reading dataset: "
494
- "missing auxiliary data")
759
+ logger.warning("error reading dataset: missing "
760
+ "auxiliary data")
495
761
  return
496
762
  except OSError:
497
- if self.verbose:
498
- print(f"error reading dataset: already open")
763
+ logger.warning(f"error reading dataset: already open")
499
764
  return
500
765
 
501
766
  def extend(self, windows):
@@ -653,7 +918,7 @@ class Inspector(InspectorPlotter):
653
918
  self.sources = pd.DataFrame()
654
919
  self.receivers = pd.DataFrame()
655
920
 
656
- def isolate(self, iteration=None, step_count=None, event=None,
921
+ def isolate(self, iteration=None, step_count=None, event=None,
657
922
  network=None, station=None, channel=None, component=None,
658
923
  keys=None, exclude=None, unique_key=None):
659
924
  """
@@ -793,8 +1058,8 @@ class Inspector(InspectorPlotter):
793
1058
  return self._event_misfit
794
1059
 
795
1060
  # Various levels to sort the misfit by
796
- group_list = ["iteration", "step", "event", "station", "component",
797
- "misfit"]
1061
+ group_list = ["iteration", "step", "event", "network", "station",
1062
+ "component", "misfit"]
798
1063
  misfits = self.windows.loc[:, tuple(group_list)]
799
1064
 
800
1065
  # Count the number of windows on a per station basis
@@ -927,8 +1192,8 @@ class Inspector(InspectorPlotter):
927
1192
 
928
1193
  return minmax_dict
929
1194
 
930
- def compare(self, iteration_a=None, step_count_a=None, iteration_b=None,
931
- step_count_b=None):
1195
+ def compare_events(self, iteration_a=None, step_count_a=None,
1196
+ iteration_b=None, step_count_b=None):
932
1197
  """
933
1198
  Compare the misfit and number of windows on an event by event basis
934
1199
  between two evaluations. Provides absolute values as well as
@@ -1049,7 +1314,70 @@ class Inspector(InspectorPlotter):
1049
1314
  df[f"diff_{val}"] = df[f"{val}_{final}"] - df[f"{val}_{initial}"]
1050
1315
 
1051
1316
  return df
1317
+
1318
+ def compare_misfits(self, iteration_a=None, step_count_a=None,
1319
+ iteration_b=None, step_count_b=None):
1320
+ """
1321
+ Compare the misfit values between the final and initial model for each
1322
+ source receiver pair. Returns differences of unscaled misfit and
1323
+ difference in number of windows between two evaluations.
1324
+
1325
+ .. note::
1052
1326
 
1327
+ See also compare_events() for a similar function but for events only
1328
+
1329
+ :type iteration_a: str
1330
+ :param iteration_a: initial iteration to use in comparison
1331
+ :type step_count_a: str
1332
+ :param step_count_a: initial step count to use in comparison
1333
+ :type iteration_b: str
1334
+ :param iteration_b: final iteration to use in comparison
1335
+ :type step_count_b: str
1336
+ :param step_count_b: final step count to use in comparison
1337
+ :rtype: pandas.core.data_frame.DataFrame
1338
+ :return: a data frame containing differences of misfit and number of
1339
+ windows between final and initial models
1340
+ """
1341
+ iter_start, step_start = self.validate_evaluation(
1342
+ iteration=iteration_a, step_count=step_count_a, choice="initial"
1343
+ )
1344
+ iter_end, step_end = self.validate_evaluation(
1345
+ iteration=iteration_b, step_count=step_count_b, choice="final"
1346
+ )
1347
+
1348
+ _windows = self.windows
1349
+ self.windows = self.isolate(iteration=iter_start, step_count=step_start)
1350
+ misfit_start = self.misfit(level="station", reset=True)
1351
+
1352
+ self.windows = _windows
1353
+ self.windows = self.isolate(iteration=iter_end, step_count=step_end)
1354
+ misfit_end = self.misfit(level="station", reset=True)
1355
+
1356
+ # Merge the two making sure they match event and station
1357
+ sfx_start = f"{iter_start}{step_start}"
1358
+ sfx_end = f"{iter_end}{step_end}"
1359
+ df = pd.merge(misfit_start, misfit_end,
1360
+ on=["event", "network", "station"],
1361
+ suffixes=(f"_{sfx_start}", f"_{sfx_end}")
1362
+ )
1363
+
1364
+ # Calculate the difference between the two misfits
1365
+ df["diff_misfit"] = \
1366
+ df[f"unscaled_misfit_{sfx_end}"] - df[f"unscaled_misfit_{sfx_start}"]
1367
+ df["diff_nwin"] = df[f"nwin_{sfx_end}"] - df[f"nwin_{sfx_start}"]
1368
+
1369
+ # Drop the original values
1370
+ df.drop([f"unscaled_misfit_{sfx_end}", f"unscaled_misfit_{sfx_start}",
1371
+ f"misfit_{sfx_end}", f"misfit_{sfx_start}",
1372
+ f"nwin_{sfx_end}", f"nwin_{sfx_start}"],
1373
+ axis=1, inplace=True)
1374
+
1375
+ # Reset windows so User can still use the Inspector as advertised
1376
+ self.windows = _windows
1377
+
1378
+ # Isolate the largest misfit offenders
1379
+ return df.sort_values(by="diff_misfit")
1380
+
1053
1381
  def filter_sources(self, lat_min=None, lat_max=None, lon_min=None,
1054
1382
  lon_max=None, depth_min=None, depth_max=None,
1055
1383
  mag_min=None, mag_max=None, min_start=None,
@@ -1109,6 +1437,48 @@ class Inspector(InspectorPlotter):
1109
1437
 
1110
1438
  return sources
1111
1439
 
1440
+ def event_outliers(self, iteration=None, step_count=None, choice="misfit",
1441
+ nstd=1):
1442
+ """
1443
+ Returns outliers for a given misfit measure (misfit or window number)
1444
+ by calculating mean and standard deviation and finding events that
1445
+ fall outside some integer multiple of standard deviations from the mean.
1446
+ Used for plotting in `event_comparison` but also useful for quickly
1447
+ assessing which events have anomalously low or high misfit values
1448
+
1449
+ :type iteration: str
1450
+ :param iteration: iteration to choose for misfit
1451
+ :type step_count: str
1452
+ :param step_count: step count to query, e.g. 's00'
1453
+ :type choice: str
1454
+ :param choice: choice of misfit value, either 'misfit' or 'nwin' or
1455
+ 'unscaled_misfit'
1456
+ :type nstd: int
1457
+ :param nstd: number of standard deviations to set upper and lower
1458
+ thresholds. Defaults to 1
1459
+ :rtype: (Pandas.series, Pandas.series, float, float)
1460
+ :return: (events above upper threshold, events below lower threshold,
1461
+ mean, standard deviation)
1462
+ """
1463
+ iteration, step_count = self.validate_evaluation(iteration, step_count)
1464
+
1465
+ arr = self.misfit(
1466
+ level="event")[choice][iteration][step_count].to_numpy()
1467
+ index = self.misfit(level="event")[choice][iteration][step_count]
1468
+ mean = np.mean(arr)
1469
+ std = np.std(arr)
1470
+
1471
+ upper_thresh = mean + (nstd * std)
1472
+ lower_thresh = mean - (nstd * std)
1473
+
1474
+ idx_upper_outliers = np.where(arr >= upper_thresh)
1475
+ idx_lower_outliers = np.where(arr <= lower_thresh)
1476
+
1477
+ upper_outliers = index.iloc[idx_upper_outliers]
1478
+ lower_outliers = index.iloc[idx_lower_outliers]
1479
+
1480
+ return upper_outliers, lower_outliers, mean, std
1481
+
1112
1482
  def get_models(self):
1113
1483
  """
1114
1484
  Return a sorted list of misfits which correspond to accepted models,
@@ -1222,3 +1592,31 @@ class Inspector(InspectorPlotter):
1222
1592
  models.reset_index(drop=True, inplace=True)
1223
1593
 
1224
1594
  return models
1595
+
1596
+
1597
+ if __name__ == "__main__":
1598
+
1599
+
1600
+ """
1601
+ Here we define a simple command-line tool for using the Inspector. Useful
1602
+ for Users who have already generated the inspector, and want to quickly
1603
+ make figures to explore the misfit of their inversion. Must be run in the
1604
+ directory containing your '.csv' Inspector files. Kwargs can be passed as
1605
+ later arguments in the format 'key=val'
1606
+
1607
+ .. rubric::
1608
+
1609
+ $ python inspector.py <function_name> <kwarg_key=kwarg_val> ...
1610
+ e.g.,
1611
+ $ python inspector.py event_station_hist2d iteration=1 step_count=2
1612
+ """
1613
+ import sys
1614
+ insp = Inspector()
1615
+ kwargs = {}
1616
+ if len(sys.argv) > 2:
1617
+ for arg in sys.argv[2:]:
1618
+ key, val = arg.split("=")
1619
+ kwargs[key] = val
1620
+ getattr(insp, sys.argv[1])(**kwargs)
1621
+
1622
+