Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/snuffler/snufflings/polarization.py: 13%
445 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 09:52 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 09:52 +0000
1import logging
2import numpy as num
3import matplotlib
4from matplotlib import patches
5from pyrocko import util, trace, plot
6from ..snuffling import Snuffling, Param, Marker, Switch, EventMarker
8logger = logging.getLogger('pyrocko.gui.snufflings.polarization')
10d2r = num.pi / 180.
11r2d = 1.0 / d2r
14def get_callbacks(obj):
15 try:
16 return obj.callbacks
17 except AttributeError:
18 return obj._callbacks
21def get_tr(traces, f):
22 for tr in traces:
23 if f(tr):
24 return tr
26 raise ValueError('Element not found.')
29def darken(color, f=0.5):
30 return tuple(c*f for c in color[:3]) + color[3:]
33class PCAError(Exception):
34 pass
37class LayoutError(Exception):
38 pass
41class Polarization(Snuffling):
43 def help(self):
44 return '''
45<html>
46<head>
47<style type="text/css">
48 body { margin-left:10px };
49</style>
50</head>
51<body>
53<h1>
54Investigate patterns of ground motion during the passage of seismic waves.
55</h1>
57<p>
58This Snuffling can be used to analyze and visualize the polarization of seismic
59waves from 3-component seismic recordings or to check component orientations of
60a seismic sensors when used on signals with known directional properties. The
61spatial pattern of ground motion is shown in horizontal and vertical
62projections. Principal component analysis and rotation to radial/transverse
63components are available as tools. Time window and filter settings can be
64adjusted interactively.
65</p>
67<h2>
68Usage
69</h2>
71<p>
72Select one or more normal/phase markers as anchor points for the extraction and
73press <strong>Run</strong>. Multiple stations can be selected for direct
74comparison. Channels matching the pattern-triplet given in the
75<strong>Channels</strong> setting are selected for extraction of a time window
76around the anchor point. It is assumed that these three channels correspond to
77sensor components in the order east, north, vertical (upward), even if they are
78named differently.
79</p>
81<p>
82The time window can be adjusted with the <strong>Length</strong> and
83<strong>Offset</strong> parameters. Extracted waveforms are filtered according
84the <strong>Highpass</strong> and <strong>Lowpass</strong> parameters
85(Butterworth 4th order) and demeaned.
86</p>
88<p>
89To rotate the horizontal components around a vertical axis, use the
90<strong>\u0394 Azimuth</strong> setting. When station coordinates and an
91"active" event are available, the horizontal components can be rotated to
92radial (away from event) and transverse (leftwards) orientations using the
93computed event back-azimuth (dashed gray line).
94</p>
96<p>
97If <strong>Show 2D Eigensystems</strong> is selected, a principal component
98analysis is performed in each of the shown projects. Red lines are shown to
99depict the eigenvectors and the eigenvalues are visualized using ellipse
100symbols. If <strong>Show 3D Eigensystems</strong> is selected, a principal
101component analysis is performed using all three (non-rotated) components and
102the resulting eigenvectors are depicted with purple lines.
103</p>
105<p>
106By default the scaling is automatically adjusted to the maximum value (the
107maximum vector length of the three-component signal within the selected time
108window). The currently used scaling factor can be frozen by checking
109<strong>Fix Scale</strong>.
110</p>
111</body>
112</html>
113'''
115 def setup(self):
116 self.set_name('Polarization')
118 self.add_parameter(
119 Param('Length [s]', 't_length', 1., 0.001, 1000.))
121 self.add_parameter(
122 Param('Offset (relative)', 'ft_offset', 0., -5., 5.))
124 self.add_parameter(
125 Param(u'\u0394 Azimuth', 'azimuth', 0., -180., 180.))
127 self.add_parameter(
128 Param(
129 'Highpass [Hz]', 'highpass', None, 0.001, 1000.,
130 low_is_none=True))
132 self.add_parameter(
133 Param(
134 'Lowpass [Hz]', 'lowpass', None, 0.001, 1000.,
135 high_is_none=True))
137 self.add_parameter(
138 Param('Dot Position', 'dot_position', 0., 0., 1.))
140 self.add_parameter(
141 Switch(
142 'Rotate to RT',
143 'rotate_to_rt',
144 False))
146 self.add_parameter(
147 Switch(
148 'Show 2D Eigensystems',
149 'show_eigensystem_2d',
150 False))
152 self.add_parameter(
153 Switch(
154 'Show 3D Eigensystem',
155 'show_eigensystem_3d',
156 False))
158 self.add_parameter(
159 Switch(
160 'Fix Scale',
161 'fix_scale',
162 False))
164 def new_figure():
165 self.setup_figure_frame()
166 self.call()
168 self.add_trigger('New Figure', new_figure)
170 self.fframe = None
171 self.iframe = 0
172 self.figure_key = None
173 self.nsl_to_amax = {}
174 self.last_rotate_to_rt = False
176 self.font_size = float(matplotlib.rcParams['font.size'])
178 self.colors = {
179 'fg': matplotlib.rcParams['axes.edgecolor'],
180 'lines': 'black',
181 'rot': plot.mpl_color('skyblue1'),
182 'pca_2d': plot.mpl_color('scarletred1'),
183 'pca_3d': plot.mpl_color('plum2'),
184 'backazimuth': plot.mpl_color('aluminium3'),
185 'time_window': plot.mpl_color('aluminium2')}
187 def panel_visibility_changed(self, visible):
188 viewer = self.get_viewer()
189 if visible:
190 viewer.pile_has_changed_signal.connect(self.adjust_controls)
191 self.adjust_controls()
193 else:
194 viewer.pile_has_changed_signal.disconnect(self.adjust_controls)
196 def adjust_controls(self):
197 viewer = self.get_viewer()
198 dtmin, dtmax = viewer.content_deltat_range()
199 maxfreq = 0.5/dtmin
200 minfreq = (0.5/dtmax)*0.001
201 self.set_parameter_range('lowpass', minfreq, maxfreq)
202 self.set_parameter_range('highpass', minfreq, maxfreq)
204 def get_selected(self):
205 markers = [
206 marker for marker in self.get_selected_markers()
207 if not isinstance(marker, EventMarker)]
209 if not markers:
210 self.fail(
211 'No selected markers.\n\nCreate and select markers at points '
212 'of interest. Normal and phase markers are accepted.')
214 d = {}
215 for marker in markers:
216 tspan = self.transform_time_span(marker.tmin, marker.tmax)
217 for nslc in marker.nslc_ids:
218 k = nslc[:3] + (nslc[3][:-1], marker.tmin)
219 d[k] = tspan
221 return d
223 def transform_time_span(self, tmin, tmax):
224 tmin = tmin + self.t_length * self.ft_offset
225 tmax = tmin + self.t_length
226 return (tmin, tmax)
228 def make_selection_markers(self, selected, groups):
229 selection_markers = []
230 for k in sorted(selected):
231 tmin, tmax = selected[k]
232 patterns = [tr.nslc_id for tr in groups[k]]
233 marker = Marker(
234 nslc_ids=patterns, tmin=tmin, tmax=tmax, kind=2)
235 selection_markers.append(marker)
237 return selection_markers
239 def sorted_by_component_scheme(self, group):
240 if len(group) not in (2, 3):
241 self.fail('Need 2 or 3 component recordings. Got %i.' % len(group))
243 comps = []
244 channels = []
245 for tr in group:
246 comps.append(tr.channel[-1])
247 channels.append(tr.channel)
249 # special case for channel naming convention of Cube dataloggers
250 for scheme in [['p2', 'p1', 'p0'], ['p2', 'p1']]:
252 if sorted(channels) == sorted(scheme):
253 return [
254 get_tr(group, lambda tr: tr.channel == ch)
255 for ch in scheme]
257 for scheme in [
258 ['E', 'N', 'Z'], ['1', '2', 'Z'], ['1', '2', '3'],
259 ['0', '1', '2'], ['R', 'T', 'Z'],
260 ['E', 'N'], ['1', '2'], ['0', '1']]:
262 if sorted(comps) == sorted(scheme):
263 return [
264 get_tr(group, lambda tr: tr.channel[-1] == comp)
265 for comp in scheme]
267 self.fail('Cannot handle component group: %s' % ', '.join(channels))
269 def get_traces(self, selected, nsl_to_bazi, tpad):
270 if self.highpass is not None:
271 tpad_filter = 3.0 / self.highpass
272 elif self.lowpass is not None:
273 tpad_filter = 3.0 / self.lowpass
274 else:
275 tpad_filter = 0.0
277 # prevent getting insanely long cutouts if e.g. if highpass is still
278 # very low, e.g. while moving the slider
279 tpad_filter = min(tpad_filter, 5.0 * self.t_length)
281 d = {}
282 for k in sorted(selected):
283 nsl = k[0:3]
284 tmin, tmax = selected[k]
286 bazimuth = self.azimuth
288 if self.rotate_to_rt:
289 if nsl not in nsl_to_bazi:
290 self.fail(
291 'Cannot rotate to RT.\n\nStation coordinates must be '
292 'available and an event must be marked as the '
293 '"active" event (select event and press "e").')
295 bazimuth += nsl_to_bazi[nsl] + 90.
297 pattern = '.'.join(nsl + (k[3] + '?',))
299 group = []
300 for batch in self.get_pile().chopper(
301 tmin=tmin, tmax=tmax, tpad=tpad + tpad_filter,
302 want_incomplete=False,
303 style='batch',
304 trace_selector=lambda tr: util.match_nslc(
305 pattern, tr.nslc_id)):
307 for tr in batch.traces:
308 tr = tr.copy()
309 if self.lowpass is not None \
310 and self.lowpass < 0.5/tr.deltat:
311 tr.lowpass(4, self.lowpass)
313 if self.highpass is not None \
314 and self.highpass < 0.5/tr.deltat:
316 tr.highpass(4, self.highpass)
318 try:
319 tr.chop(tmin - tpad, tmax + tpad)
320 tr_chop = tr.chop(tmin, tmax, inplace=False)
321 except trace.NoData:
322 self.fail('No data. Time length too short?')
324 y = tr.get_ydata()
325 tr.set_ydata(y - num.mean(tr_chop.get_ydata()))
327 group.append(tr)
329 group = self.sorted_by_component_scheme(group)
331 tr_e = group[0]
332 tr_n = group[1]
334 if tr_e is not None and tr_n is not None:
335 cha_e = tr_e.channel
336 cha_n = tr_n.channel
337 if bazimuth != 0.0:
338 trs_rot = trace.rotate(
339 [tr_n, tr_e], bazimuth,
340 in_channels=[cha_n, cha_e],
341 out_channels=[ch+"'" for ch in [cha_n, cha_e]])
342 else:
343 trs_rot = [tr.copy() for tr in [tr_n, tr_e]]
344 for tr in trs_rot:
345 tr.set_codes(channel=tr.channel + "'")
347 if len(trs_rot) == 2:
348 group.extend(
349 get_tr(trs_rot, lambda tr: tr.channel == ch+"'")
350 for ch in [cha_e, cha_n])
351 else:
352 self.fail(
353 'Cannot rotate traces. '
354 'Are the samples properly aligned?')
356 d[k] = group
358 return d
360 def setup_figure_frame(self):
361 self.iframe += 1
362 self.fframe = self.figure_frame(
363 'Polarization (%i)' % self.iframe)
365 self.fframe.gcf().my_disconnect = None
367 def get_figure(self):
368 if not self.fframe or self.fframe.closed:
369 self.setup_figure_frame()
371 return self.fframe.gcf()
373 def setup_figure(self, fig, nstations):
374 fig.clf()
375 new_axes = []
377 def iwrap(iy, ix):
378 return (ix + iy * 4) + 1
380 for istation in range(nstations):
381 axes_01 = fig.add_subplot(
382 nstations, 4, iwrap(istation, 0), aspect=1.0)
383 axes_02 = fig.add_subplot(
384 nstations, 4, iwrap(istation, 1), aspect=1.0)
385 axes_12 = fig.add_subplot(
386 nstations, 4, iwrap(istation, 2), aspect=1.0)
387 axes_tr = fig.add_subplot(
388 nstations, 4, iwrap(istation, 3))
390 for axes in (axes_01, axes_02, axes_12, axes_tr):
391 axes.my_stuff = []
393 axes.my_line, = axes.plot(
394 [], [], color=self.colors['lines'], lw=1.0)
395 axes.my_dot, = axes.plot(
396 [], [], 'o', ms=4, color=self.colors['lines'])
398 for axes in (axes_01, axes_02, axes_12):
399 axes.get_xaxis().set_tick_params(
400 labelbottom=False, bottom=False)
401 axes.get_yaxis().set_tick_params(
402 labelleft=False, left=False)
404 axes_tr.get_yaxis().set_tick_params(
405 left=False, labelleft=True, length=2.0)
407 # if istation != nstations - 1:
408 # axes_tr.get_xaxis().set_tick_params(
409 # bottom=False, labelbottom=False)
411 lines = []
412 dots = []
413 for _ in range(3):
414 lines.append(
415 axes_tr.plot(
416 [], [], color=self.colors['lines'], lw=1.0)[0])
417 dots.append(
418 axes_tr.plot(
419 [], [], 'o', ms=4, color=self.colors['lines'])[0])
421 axes_tr.my_lines = lines
422 axes_tr.my_dots = dots
423 axes_tr.my_stuff = []
425 new_axes.append(
426 (axes_01, axes_02, axes_12, axes_tr))
428 def resize_handler(*args):
429 self.layout(fig, new_axes)
431 if fig.my_disconnect:
432 fig.my_disconnect()
434 cid_resize = fig.canvas.mpl_connect('resize_event', resize_handler)
435 try:
436 cid_dpi = get_callbacks(fig).connect('dpi_changed', resize_handler)
437 except (AttributeError, ValueError):
438 pass # not available anymore in MPL >= 3.8
440 def disconnect():
441 fig.canvas.mpl_disconnect(cid_resize)
442 fig.callbacks.disconnect(cid_dpi)
444 fig.my_disconnect = disconnect
446 self.axes = new_axes
448 def get_trace(self, traces, pattern):
449 trs = [tr for tr in traces if util.match_nslc([pattern], tr.nslc_id)]
450 if len(trs) > 1:
451 self.fail('Multiple traces matching pattern %s' % pattern)
452 elif len(trs) == 0:
453 return None
454 else:
455 return trs[0]
457 def get_vector_abs_max(self, traces):
458 tr_abs = None
459 for tr in traces:
460 if tr is not None:
461 tr = tr.copy()
462 tr.ydata **= 2
463 if tr_abs is None:
464 tr_abs = tr
465 else:
466 tr_abs.add(tr)
468 tr_abs.set_ydata(num.sqrt(tr_abs.ydata))
469 return num.max(tr_abs.ydata)
471 def set_labels(
472 self, istation, nstations, tref,
473 axes_01, axes_02, axes_12, axes_tr, chas, chas_rot):
475 if self.azimuth != 0 or self.rotate_to_rt:
476 rcolor = self.colors['rot']
477 else:
478 rcolor = self.colors['fg']
479 chas_rot = chas
481 axes_01.set_ylabel(chas[1])
482 axes_02.set_ylabel(chas[2])
483 axes_12.set_ylabel(chas[2])
485 axes_01.set_xlabel(chas[0])
486 axes_02.set_xlabel(chas_rot[0])
487 axes_12.set_xlabel(chas_rot[1])
488 axes_02.get_xaxis().label.set_color(rcolor)
489 axes_12.get_xaxis().label.set_color(rcolor)
490 axes_tr.set_xlabel(
491 'Time [s] relative to %s' % util.time_to_str(tref))
493 axes_tr.set_yticks([0., 1., 2])
494 axes_tr.set_yticklabels([chas_rot[0], chas_rot[1], chas[2]])
495 for tlab in axes_tr.get_yticklabels()[:2]:
496 tlab.set_color(rcolor)
498 def pca(self, trs):
500 if any(tr is None for tr in trs):
501 raise PCAError('Missing component')
503 nss = [tr.data_len() for tr in trs]
504 if not all(ns == nss[0] for ns in nss):
505 raise PCAError('Traces have different lengths.')
507 ns = nss[0]
509 if ns < 3:
510 raise PCAError('Traces too short.')
512 data = num.zeros((len(trs), ns))
513 for itr, tr in enumerate(trs):
514 data[itr, :] = tr.ydata
516 cov = num.cov(data)
518 evals, evecs = num.linalg.eigh(cov)
520 pc = evecs[:, -1]
522 eh = num.sqrt(pc[1]**2 + pc[0]**2)
524 if len(trs) > 2:
525 incidence = r2d * num.arctan2(eh, abs(pc[2]))
526 else:
527 incidence = 90.
529 azimuth = r2d*num.arctan2(pc[1], pc[0])
530 azimuth = (-azimuth) % 180. - 90.
532 return cov, evals, evecs, azimuth, incidence
534 def draw_cov_ellipse(self, evals, evecs, color, alpha=1.0):
535 evals = num.sqrt(evals)
536 ell = patches.Ellipse(
537 xy=(0.0, 0.0),
538 width=evals[0] * 2.,
539 height=evals[1] * 2.,
540 angle=r2d*num.arctan2(evecs[1, 0], evecs[0, 0]),
541 zorder=-10,
542 fc=color,
543 ec=darken(color),
544 alpha=alpha)
546 return ell
548 def draw(self, groups, nsl_to_tspan, tpad, nsl_to_bazi):
550 def count(d, k):
551 if k not in d:
552 d[k] = 0
554 d[k] += 1
556 nsli_n = {}
557 for k in sorted(groups):
558 count(nsli_n, k[:4])
560 nsli_i = {}
561 for igroup, k in enumerate(sorted(groups)):
563 tmin, tmax = nsl_to_tspan[k]
564 nsl = k[:3]
565 nsli = k[:4]
566 count(nsli_i, nsli)
568 bazimuth = self.azimuth
570 if self.rotate_to_rt:
571 if nsl not in nsl_to_bazi:
572 self.fail(
573 'Cannot rotate to RT.\n\nActive event must '
574 'available (select event and press "e"). Station '
575 'coordinates must be available.')
577 bazimuth += nsl_to_bazi[nsl] + 90.
579 for axes in self.axes[igroup]:
580 while axes.my_stuff:
581 stuff = axes.my_stuff.pop()
582 stuff.remove()
584 axes_01, axes_02, axes_12, axes_tr = self.axes[igroup]
586 axes_01.set_title(
587 '.'.join(nsli)
588 + ('' if nsli_n[nsli] == 1 else ' (%i)' % nsli_i[nsli]))
590 trs_all = groups[k]
592 if len(trs_all) == 5:
593 trs_orig = trs_all[:3]
594 trs_rot = trs_all[3:] + trs_all[2:3]
595 elif len(trs_all) == 4:
596 trs_orig = trs_all[:2] + [None]
597 trs_rot = trs_all[2:] + [None]
598 else:
599 raise self.fail(
600 'Unexpectedly got other that 4 or 6 traces: %i'
601 % len(trs_all))
603 trs_orig_chopped = [
604 (tr.chop(tmin, tmax, inplace=False) if tr else None)
605 for tr in trs_orig]
607 trs_rot_chopped = [
608 (tr.chop(tmin, tmax, inplace=False) if tr else None)
609 for tr in trs_rot]
611 chas = [tr.channel[-1:] for tr in trs_orig_chopped]
612 chas_rot = [tr.channel[-2:] for tr in trs_rot_chopped]
614 if self.fix_scale and nsl in self.nsl_to_amax:
615 amax = self.nsl_to_amax[nsl]
616 else:
617 amax = self.get_vector_abs_max(trs_orig_chopped)
618 self.nsl_to_amax[nsl] = amax
620 if nsl in nsl_to_bazi:
621 event_bazi = nsl_to_bazi[nsl]
622 l1, = axes_01.plot(
623 [0., amax*num.cos((90. - event_bazi)*d2r)],
624 [0., amax*num.sin((90. - event_bazi)*d2r)],
625 '--',
626 lw=3,
627 zorder=-20,
628 color=self.colors['backazimuth'])
630 a1 = axes_01.annotate(
631 '%.0f\u00b0' % event_bazi,
632 (0., 1.),
633 (self.font_size, -self.font_size),
634 xycoords='axes fraction',
635 textcoords='offset points',
636 color=self.colors['backazimuth'],
637 ha='left',
638 va='top')
640 axes_01.my_stuff.extend([l1, a1])
642 for ix, iy, axes, trs in [
643 (0, 1, axes_01, trs_orig_chopped),
644 (0, 2, axes_02, trs_rot_chopped),
645 (1, 2, axes_12, trs_rot_chopped)]:
647 axes.set_xlim(-amax*1.05, amax*1.05)
648 axes.set_ylim(-amax*1.05, amax*1.05)
650 if not (trs[ix] and trs[iy]):
651 continue
653 x = trs[ix].get_ydata()
654 y = trs[iy].get_ydata()
656 axes.my_line.set_data(x, y)
657 ipos = int(round(self.dot_position * (x.size-1)))
658 axes.my_dot.set_data(x[ipos], y[ipos])
660 tref = tmin
661 for itr, (tr, tr_chopped) in enumerate(zip(
662 trs_rot, trs_rot_chopped)):
664 if tr is None or tr_chopped is None:
665 axes_tr.my_lines[itr].set_data([], [])
666 axes_tr.my_dots[itr].set_data([], [])
668 else:
669 y = tr.get_ydata() / (2.*amax) + itr
670 t = tr.get_xdata()
671 t = t - tref
673 ipos = int(round(
674 self.dot_position * (tr_chopped.data_len()-1)))
676 yp = tr_chopped.ydata[ipos] / (2.*amax) + itr
677 tp = tr_chopped.tmin - tref + tr_chopped.deltat*ipos
679 axes_tr.my_lines[itr].set_data(t, y)
680 axes_tr.my_dots[itr].set_data(tp, yp)
682 if self.azimuth != 0.0 or self.rotate_to_rt:
683 color = self.colors['rot']
685 xn = num.sin(bazimuth*d2r)
686 yn = num.cos(bazimuth*d2r)
687 xe = num.sin(bazimuth*d2r + 0.5*num.pi)
688 ye = num.cos(bazimuth*d2r + 0.5*num.pi)
690 l1, = axes_01.plot(
691 [0., amax*xn],
692 [0., amax*yn],
693 color=color)
695 font_size = self.font_size
697 a1 = axes_01.annotate(
698 chas_rot[1],
699 xy=(amax*xn, amax*yn),
700 xycoords='data',
701 xytext=(-font_size*(xe+.5*xn), -font_size*(ye+.5*yn)),
702 textcoords='offset points',
703 va='center',
704 ha='center',
705 color=color)
707 l2, = axes_01.plot(
708 [0., amax*xe],
709 [0., amax*ye],
710 color=color)
712 a2 = axes_01.annotate(
713 chas_rot[0],
714 xy=(amax*xe, amax*ye),
715 xycoords='data',
716 xytext=(-font_size*(xn+.5*xe), -font_size*(yn+.5*ye)),
717 textcoords='offset points',
718 va='center',
719 ha='center',
720 color=color)
722 aa = axes_01.annotate(
723 '%.0f\u00b0' % bazimuth,
724 (1., 1.),
725 (-self.font_size, -self.font_size),
726 xycoords='axes fraction',
727 textcoords='offset points',
728 color=color,
729 ha='right',
730 va='top')
732 axes_01.my_stuff.extend([l1, a1, l2, a2, aa])
734 axes_tr.my_stuff.append(axes_tr.axvspan(
735 tmin - tref, tmax - tref, color=self.colors['time_window']))
737 axes_tr.set_ylim(-1, 3)
738 axes_tr.set_xlim(tmin - tref - tpad, tmax - tref + tpad)
740 self.set_labels(
741 igroup, len(groups), tref, *self.axes[igroup], chas, chas_rot)
743 if self.show_eigensystem_2d:
745 for (ix, iy, axes, trs) in [
746 (0, 1, axes_01, trs_orig_chopped),
747 (0, 2, axes_02, trs_rot_chopped),
748 (1, 2, axes_12, trs_rot_chopped)]:
750 try:
751 cov, evals, evecs, azimuth, _ = self.pca(
752 [trs[ix], trs[iy]])
754 axes.my_stuff.append(
755 axes.annotate(
756 '%.0f\u00b0' % azimuth,
757 (0., 0.),
758 (self.font_size, self.font_size),
759 xycoords='axes fraction',
760 textcoords='offset points',
761 color=self.colors['pca_2d'],
762 alpha=0.5))
764 ell = self.draw_cov_ellipse(
765 evals[:2], evecs[:2, :2],
766 color=self.colors['pca_2d'], alpha=0.5)
768 axes.add_artist(ell)
769 axes.my_stuff.append(ell)
771 l1, = axes.plot(
772 [-amax*evecs[0, -1], amax*evecs[0, -1]],
773 [-amax*evecs[1, -1], amax*evecs[1, -1]],
774 color=self.colors['pca_2d'], alpha=0.8)
776 l2, = axes.plot(
777 [-amax*evecs[0, -2], amax*evecs[0, -2]],
778 [-amax*evecs[1, -2], amax*evecs[1, -2]],
779 color=self.colors['pca_2d'], alpha=0.2)
781 axes.my_stuff.extend([l1, l2])
783 except PCAError as e:
784 logger.warning('PCA failed: %s' % e)
786 if self.show_eigensystem_3d:
787 try:
788 cov, evals, evecs, azimuth, incidence = self.pca(
789 trs_orig_chopped)
790 cosa = num.cos(bazimuth*d2r)
791 sina = num.sin(bazimuth*d2r)
792 rot = num.array(
793 [[cosa, -sina, 0.0],
794 [sina, cosa, 0.0],
795 [0.0, 0.0, 1.0]], dtype=float)
797 evecs_rot = num.dot(rot, evecs)
799 for (ix, iy, axes, evecs_) in [
800 (0, 1, axes_01, evecs),
801 (0, 2, axes_02, evecs_rot),
802 (1, 2, axes_12, evecs_rot)]:
804 for (ie, alpha) in [
805 (-1, 0.5),
806 (-2, 0.5),
807 (-3, 0.5)]:
809 for vlen, lw in [
810 (num.sqrt(evals[ie]), 3),
811 (amax, 1)]:
813 lv, = axes.plot(
814 [-vlen*evecs_[ix, ie],
815 vlen*evecs_[ix, ie]],
816 [-vlen*evecs_[iy, ie],
817 vlen*evecs_[iy, ie]],
818 lw=lw,
819 color=self.colors['pca_3d'], alpha=alpha)
821 axes.my_stuff.extend([lv])
823 axes_01.my_stuff.append(
824 axes_01.annotate(
825 '%.0f\u00b0' % azimuth,
826 (1., 0.),
827 (-self.font_size, self.font_size),
828 xycoords='axes fraction',
829 textcoords='offset points',
830 color=self.colors['pca_3d'],
831 alpha=0.8,
832 ha='right'))
834 axes_02.my_stuff.append(
835 axes_02.annotate(
836 '%.0f\u00b0' % incidence,
837 (1., 0.),
838 (-self.font_size, self.font_size),
839 xycoords='axes fraction',
840 textcoords='offset points',
841 color=self.colors['pca_3d'],
842 alpha=0.8,
843 ha='right'))
845 except PCAError as e:
846 logger.warning('PCA failed: %s' % e)
848 def get_bazis(self):
849 event = self.get_viewer().get_active_event()
850 if not event:
851 return {}
853 nsl_to_bazi = dict(
854 (station.nsl(), event.azibazi_to(station)[1])
855 for station in self.get_stations())
857 return nsl_to_bazi
859 def layout(self, fig, axes):
861 # Do not access self in here. Called from resize in finished plots.
863 def get_pixels_factor(fig):
864 try:
865 r = fig.canvas.get_renderer()
866 return 1.0 / r.points_to_pixels(1.0)
867 except AttributeError:
868 return 1.0
870 def rect_to_figure_coords(rect):
871 l, b, w, h = rect # noqa
872 return (l / width, b / height, w / width, h / height) # noqa
874 ny = len(axes)
875 if ny == 0:
876 raise LayoutError('No axes given.')
878 nx = len(axes[0])
880 width, height = fig.canvas.get_width_height()
881 pixels = get_pixels_factor(fig)
883 margin_left = margin_right = 4. * self.font_size / pixels
884 margin_top = margin_bottom = 5. * self.font_size / pixels
886 spacing_width = 3. * self.font_size / pixels
887 spacing_height = 5. * self.font_size / pixels
889 axes_height_avail = height - (ny - 1) * spacing_height \
890 - margin_top - margin_bottom
892 a_min = 5.
894 if axes_height_avail < a_min * ny:
895 axes_height_avail = a_min * ny
896 logger.warning('Not enough space vertically.')
898 axes_width_avail = width - (nx - 1) * spacing_width \
899 - margin_left - margin_right
901 if axes_width_avail < a_min * (nx + 2):
902 axes_width_avail = a_min * (nx + 2)
903 logger.warning('Not enough space horizontally.')
905 a_height = axes_height_avail / ny
906 a_width = axes_width_avail / (nx + 2)
908 a = max(min(a_height, a_width), a_min)
910 pad_height = (a_height - a) * ny
911 pad_width = (a_width - a) * (nx + 2)
913 for iy in range(ny):
914 y = height - 0.5 * pad_height - margin_top \
915 - (iy + 1) * a - iy * spacing_height
916 h = a
917 for ix in range(nx):
918 x = margin_right + 0.5 * pad_width + ix * (a + spacing_width)
919 w = a if ix != (nx - 1) else a * 3.0
920 axes[iy][ix].set_position(
921 rect_to_figure_coords((x, y, w, h)), which='both')
923 def call(self):
925 self.cleanup()
927 if self.rotate_to_rt != self.last_rotate_to_rt:
928 # reset delta azimuth to avoid confusion
930 self.set_parameter('azimuth', 0.0)
932 self.last_rotate_to_rt = self.rotate_to_rt
934 selected = self.get_selected()
936 nsl_to_bazi = self.get_bazis()
938 tpad = self.t_length
939 groups = self.get_traces(selected, nsl_to_bazi, tpad)
941 if not groups:
942 self.fail(
943 'No matching traces. Check time and channel settings. Traces '
944 'may not contain gaps within the extracted time window and in '
945 'the padding areas left and right. Traces are extracted with '
946 'additional padding of 3 x filter corner period to eliminate '
947 'artifacts.')
949 selection_markers = self.make_selection_markers(selected, groups)
950 self.add_markers(selection_markers)
952 fig = self.get_figure()
954 figure_key = (len(groups), self.iframe)
956 if not self.figure_key or self.figure_key != figure_key:
957 self.setup_figure(fig, len(groups))
958 self.figure_key = figure_key
960 self.draw(groups, selected, tpad, nsl_to_bazi)
962 self.layout(fig, self.axes)
964 self.fframe.draw()
965 tabs = self.fframe.parent().parent()
966 # bring plot to front if we are not looking at the markers
967 from pyrocko.gui.pile_viewer import PileViewer
968 if not isinstance(tabs.currentWidget(), PileViewer):
969 tabs.setCurrentWidget(self.fframe)
972def __snufflings__():
973 return [Polarization()]