def test_calcVincentyInverse2(self): """ Test calc_vincenty_inverse() method with test data from Geocentric Datum of Australia. (see http://www.icsm.gov.au/gda/gdatm/gdav2.3.pdf) """ # test data: # Point 1: Flinders Peak, Point 2: Buninyong lat1 = -(37 + (57 / 60.) + (3.72030 / 3600.)) lon1 = 144 + (25 / 60.) + (29.52440 / 3600.) lat2 = -(37 + (39 / 60.) + (10.15610 / 3600.)) lon2 = 143 + (55 / 60.) + (35.38390 / 3600.) dist = 54972.271 alpha12 = 306 + (52 / 60.) + (5.37 / 3600.) alpha21 = 127 + (10 / 60.) + (25.07 / 3600.) # calculate result calc_dist, calc_alpha12, calc_alpha21 = calc_vincenty_inverse( lat1, lon1, lat2, lon2) # calculate deviations from test data dist_err_rel = abs(dist - calc_dist) / dist alpha12_err = abs(alpha12 - calc_alpha12) alpha21_err = abs(alpha21 - calc_alpha21) self.assertEqual(dist_err_rel < 1.0e-5, True) self.assertEqual(alpha12_err < 1.0e-5, True) self.assertEqual(alpha21_err < 1.0e-5, True) # calculate result with +- 360 for lon values dist, alpha12, alpha21 = calc_vincenty_inverse( lat1, lon1 + 360, lat2, lon2 - 720) self.assertAlmostEqual(dist, calc_dist) self.assertAlmostEqual(alpha12, calc_alpha12) self.assertAlmostEqual(alpha21, calc_alpha21)
def test_calc_vincenty_inverse_2(self): """ Test calc_vincenty_inverse() method with test data from Geocentric Datum of Australia. (see http://www.icsm.gov.au/gda/gdatm/gdav2.3.pdf) """ # test data: # Point 1: Flinders Peak, Point 2: Buninyong lat1 = -(37 + (57 / 60.) + (3.72030 / 3600.)) lon1 = 144 + (25 / 60.) + (29.52440 / 3600.) lat2 = -(37 + (39 / 60.) + (10.15610 / 3600.)) lon2 = 143 + (55 / 60.) + (35.38390 / 3600.) dist = 54972.271 alpha12 = 306 + (52 / 60.) + (5.37 / 3600.) alpha21 = 127 + (10 / 60.) + (25.07 / 3600.) # calculate result calc_dist, calc_alpha12, calc_alpha21 = calc_vincenty_inverse( lat1, lon1, lat2, lon2) # calculate deviations from test data dist_err_rel = abs(dist - calc_dist) / dist alpha12_err = abs(alpha12 - calc_alpha12) alpha21_err = abs(alpha21 - calc_alpha21) self.assertEqual(dist_err_rel < 1.0e-5, True) self.assertEqual(alpha12_err < 1.0e-5, True) self.assertEqual(alpha21_err < 1.0e-5, True) # calculate result with +- 360 for lon values dist, alpha12, alpha21 = calc_vincenty_inverse(lat1, lon1 + 360, lat2, lon2 - 720) self.assertAlmostEqual(dist, calc_dist) self.assertAlmostEqual(alpha12, calc_alpha12) self.assertAlmostEqual(alpha21, calc_alpha21)
def test_calc_vincenty_inverse(self): """ Tests for the Vincenty's Inverse formulae. """ # the following will raise StopIteration exceptions because of two # nearly antipodal points self.assertRaises(StopIteration, calc_vincenty_inverse, 15.26804251, 2.93007342, -14.80522806, -177.2299081) self.assertRaises(StopIteration, calc_vincenty_inverse, 27.3562106, 72.2382356, -27.55995499, -107.78571981) self.assertRaises(StopIteration, calc_vincenty_inverse, 27.4675551, 17.28133229, -27.65771704, -162.65420626) self.assertRaises(StopIteration, calc_vincenty_inverse, 27.4675551, 17.28133229, -27.65771704, -162.65420626) self.assertRaises(StopIteration, calc_vincenty_inverse, 0, 0, 0, 13) # working examples res = calc_vincenty_inverse(0, 0.2, 0, 20) self.assertAlmostEqual(res[0], 2204125.9174282863) self.assertAlmostEqual(res[1], 90.0) self.assertAlmostEqual(res[2], 270.0) res = calc_vincenty_inverse(0, 0, 0, 10) self.assertAlmostEqual(res[0], 1113194.9077920639) self.assertAlmostEqual(res[1], 90.0) self.assertAlmostEqual(res[2], 270.0) res = calc_vincenty_inverse(0, 0, 0, 17) self.assertAlmostEqual(res[0], 1892431.3432465086) self.assertAlmostEqual(res[1], 90.0) self.assertAlmostEqual(res[2], 270.0) # out of bounds self.assertRaises(ValueError, calc_vincenty_inverse, 91, 0, 0, 0) self.assertRaises(ValueError, calc_vincenty_inverse, -91, 0, 0, 0) self.assertRaises(ValueError, calc_vincenty_inverse, 0, 0, 91, 0) self.assertRaises(ValueError, calc_vincenty_inverse, 0, 0, -91, 0)
def test_calcVincentyInverse(self): """ Tests for the Vincenty's Inverse formulae. """ # the following will raise StopIteration exceptions because of two # nearly antipodal points self.assertRaises(StopIteration, calc_vincenty_inverse, 15.26804251, 2.93007342, -14.80522806, -177.2299081) self.assertRaises(StopIteration, calc_vincenty_inverse, 27.3562106, 72.2382356, -27.55995499, -107.78571981) self.assertRaises(StopIteration, calc_vincenty_inverse, 27.4675551, 17.28133229, -27.65771704, -162.65420626) self.assertRaises(StopIteration, calc_vincenty_inverse, 27.4675551, 17.28133229, -27.65771704, -162.65420626) self.assertRaises(StopIteration, calc_vincenty_inverse, 0, 0, 0, 13) # working examples res = calc_vincenty_inverse(0, 0.2, 0, 20) self.assertAlmostEqual(res[0], 2204125.9174282863) self.assertAlmostEqual(res[1], 90.0) self.assertAlmostEqual(res[2], 270.0) res = calc_vincenty_inverse(0, 0, 0, 10) self.assertAlmostEqual(res[0], 1113194.9077920639) self.assertAlmostEqual(res[1], 90.0) self.assertAlmostEqual(res[2], 270.0) res = calc_vincenty_inverse(0, 0, 0, 17) self.assertAlmostEqual(res[0], 1892431.3432465086) self.assertAlmostEqual(res[1], 90.0) self.assertAlmostEqual(res[2], 270.0) # out of bounds self.assertRaises(ValueError, calc_vincenty_inverse, 91, 0, 0, 0) self.assertRaises(ValueError, calc_vincenty_inverse, -91, 0, 0, 0) self.assertRaises(ValueError, calc_vincenty_inverse, 0, 0, 91, 0) self.assertRaises(ValueError, calc_vincenty_inverse, 0, 0, -91, 0)
def user_func(config, sta_info, origin, obsd_tr, synt_tr): npts = synt_tr.stats.npts base_water_level = config.stalta_waterlevel base_cc = config.cc_acceptance_level base_tshift = config.tshift_acceptance_level base_dlna = config.dlna_acceptance_level base_s2n = config.s2n_limit # turn parameters into arrays stalta_waterlevel = np.ones(npts) * base_water_level cc = np.ones(npts) * base_cc tshift = np.ones(npts) * base_tshift dlna = np.ones(npts) * base_dlna s2n = np.ones(npts) * base_s2n trace_info = sta_info.get_coordinates(synt_tr.id[:-1]+"Z") times = get_time_array(synt_tr, origin) dist_km = calc_vincenty_inverse( origin.latitude, origin.longitude, trace_info["latitude"], trace_info["longitude"])[0] / 1000 # rayleigh r_vel = 3.2 r_time = dist_km/r_vel r_index = None # Find index in time array of Rayleigh wave arrival r_index = (np.abs(times - r_time)).argmin() + 1 s2n[r_index:] = 10 * base_s2n cc[r_index:] = 0.95 tshift[r_index:] = base_tshift/3.0 dlna[r_index:] = base_dlna/3.0 stalta_waterlevel[r_index:] = base_water_level * 2.0 # Check event depth # origin.depth is in meters if origin.depth > 70000 and origin.depth < 300000: # intermediate tshift[:] = base_tshift * 1.4 elif origin.depth > 300000: # deep tshift[:] = base_tshift * 1.7 # replace config vars new_config = copy.deepcopy(config) new_config.stalta_waterlevel = stalta_waterlevel new_config.tshift_acceptance_level = tshift new_config.dlna_acceptance_level = dlna new_config.cc_acceptance_level = cc new_config.s2n_limit = s2n new_config.signal_end_index = r_index return new_config
def get_dist_in_km(station, event, obsd): """ Returns distance in km """ stats = obsd.stats station_coor = station.get_coordinates(".".join([stats.network, stats.station, stats.location, stats.channel[:-1]+"Z"])) evlat = event.events[0].origins[0].latitude evlon = event.events[0].origins[0].longitude dist = calc_vincenty_inverse(station_coor["latitude"], station_coor["longitude"], evlat, evlon)[0] / 1000 return dist
def select_windows( data_trace, synthetic_trace, stf_trace, event_latitude, event_longitude, station_latitude, station_longitude, minimum_period, maximum_period, min_cc=0.10, max_noise=0.10, max_noise_window=0.4, min_velocity=2.4, threshold_shift=0.30, threshold_correlation=0.75, min_length_period=1.5, min_peaks_troughs=2, max_energy_ratio=10.0, min_envelope_similarity=0.2, global_inversion=False, window_everything=False, verbose=False, plot=False, ): """ Window selection algorithm for picking windows suitable for misfit calculation based on phase differences. Returns a list of windows which might be empty due to various reasons. This function is really long and a lot of things. For a more detailed description, please see the LASIF paper. :param data_trace: The data trace. :type data_trace: :class:`~obspy.core.trace.Trace` :param synthetic_trace: The synthetic trace. :type synthetic_trace: :class:`~obspy.core.trace.Trace` :param stf_trace: The stf trace. :type stf_trace: :class:`~obspy.core.trace.Trace` :param event_latitude: The event latitude. :type event_latitude: float :param event_longitude: The event longitude. :type event_longitude: float :param station_latitude: The station latitude. :type station_latitude: float :param station_longitude: The station longitude. :type station_longitude: float :param minimum_period: The minimum period of the data in seconds. :type minimum_period: float :param maximum_period: The maximum period of the data in seconds. :type maximum_period: float :param min_cc: Minimum normalised correlation coefficient of the complete traces. :type min_cc: float :param max_noise: Maximum relative noise level for the whole trace. Measured from maximum amplitudes before and after the first arrival. :type max_noise: float :param max_noise_window: Maximum relative noise level for individual windows. :type max_noise_window: float :param min_velocity: All arrivals later than those corresponding to the threshold velocity [km/s] will be excluded. :type min_velocity: float :param threshold_shift: Maximum allowable time shift within a window, as a fraction of the minimum period. :type threshold_shift: float :param threshold_correlation: Minimum normalised correlation coeeficient within a window. :type threshold_correlation: float :param min_length_period: Minimum length of the time windows relative to the minimum period. :type min_length_period: float :param min_peaks_troughs: Minimum number of extrema in an individual time window (excluding the edges). :type min_peaks_troughs: float :param max_energy_ratio: Maximum energy ratio between data and synthetics within a time window. Don't make this too small! :type max_energy_ratio: float :param min_envelope_similarity: The minimum similarity of the envelopes of both data and synthetics. This essentially assures that the amplitudes of data and synthetics can not diverge too much within a window. It is a bit like the inverse of the ratio of both envelopes so a value of 0.2 makes sure neither amplitude can be more then 5 times larger than the other. :type min_envelope_similarity: float :param global_inversion: Don't perform any traveltime elimination at the end of the trace if set to True. :param global_inversion: bool :param window_everything: If set to True, windows the whole trace, if global acception criteria are met, such as the noise level in the trace. :type window_everything: bool :param verbose: No output by default. :type verbose: bool :param plot: Create a plot of the algortihm while it does its work. :type plot: bool """ # Shortcuts to frequently accessed variables. data_starttime = data_trace.stats.starttime data_delta = data_trace.stats.delta dt = data_trace.stats.delta npts = data_trace.stats.npts synth = synthetic_trace.data data = data_trace.data times = data_trace.times() # Don't return any windows if there is a NaN if np.sum(np.isnan(data)) > 0 or np.sum(np.isnan(synth)) > 0: return [] # ------------------------------------------------------------------------- # Geographical calculations and the time of the first arrival. # ------------------------------------------------------------------------- dist_in_km = ( geodetics.calc_vincenty_inverse( station_latitude, station_longitude, event_latitude, event_longitude, )[0] / 1000.0 ) # ------------------------------------------------------------------------- # Window settings # ------------------------------------------------------------------------- # Number of samples in the sliding window. Currently, the length of the # window is set to a multiple of the dominant period of the synthetics. # Make sure it is an uneven number; just to have a trivial midpoint # definition and one sample does not matter much in any case. window_length = int(round(float(2 * minimum_period) / dt)) if not window_length % 2: window_length += 1 # Use a Hanning window. No particular reason for it but its a well-behaved # window and has nice spectral properties. taper = np.hanning(window_length) # ========================================================================= # check if whole seismograms are sufficiently correlated and estimate # noise level # ========================================================================= # Overall Correlation coefficient. norm = np.sqrt(np.sum(data ** 2)) * np.sqrt(np.sum(synth ** 2)) cc = np.sum(data * synth) / norm if verbose: _log_window_selection( data_trace.id, "Correlation Coefficient: %.4f" % cc ) first_tt_arrival = np.where(np.abs(synth) > 5e-3 * np.max(np.abs(synth)))[0][0] idx_end = int(0.9 * first_tt_arrival) idx_end = max(idx_end, 1) # ensure at least 1 sample is available idx_start = 0 if idx_start >= idx_end: idx_start = max(0, idx_end - 10) abs_data = np.abs(data) # Return empty window when no data is available if np.max(abs_data) == 0.0 or np.max(np.abs(synth)) == 0.0: return [] noise_absolute = abs_data[idx_start:idx_end].max() noise_relative = noise_absolute / abs_data.max() if verbose: _log_window_selection( data_trace.id, "Absolute Noise Level: %e" % noise_absolute ) _log_window_selection( data_trace.id, "Relative Noise Level: %e" % noise_relative ) # Basic global rejection criteria. accept_traces = True if (cc < min_cc) and (noise_relative > max_noise / 3.0): msg = "Correlation %.4f is below threshold of %.4f" % (cc, min_cc) if verbose: _log_window_selection(data_trace.id, msg) accept_traces = msg if noise_relative > max_noise: msg = "Noise level %.3f is above threshold of %.3f" % ( noise_relative, max_noise, ) if verbose: _log_window_selection(data_trace.id, msg) accept_traces = msg minimum_distance = 2 * minimum_period * min_velocity if dist_in_km < minimum_distance: msg = "Source - Receiver distance %.3f is below threshold of %.3f" % ( dist_in_km, minimum_distance, ) if verbose: _log_window_selection(data_trace.id, msg) accept_traces = msg # Calculate the envelope of both data and synthetics. This is to make sure # that the amplitude of both is not too different over time and is # used as another selector. Only calculated if the trace is generally # accepted as it is fairly slow. if accept_traces is True: data_env = obspy.signal.filter.envelope(data) synth_env = obspy.signal.filter.envelope(synth) # ------------------------------------------------------------------------- # Initial Plot setup. # ------------------------------------------------------------------------- # All the plot calls are interleaved. I realize this is really ugly but # the alternative would be to either have two functions (one with plots, # one without) or split the plotting function in various subfunctions, # neither of which are acceptable in my opinion. The impact on # performance is minimal if plotting is turned off: all imports are lazy # and a couple of conditionals are cheap. if plot: import matplotlib.pylab as plt # NOQA import matplotlib.patheffects as PathEffects # NOQA if accept_traces is True: plt.figure(figsize=(18, 12)) plt.subplots_adjust( left=0.05, bottom=0.05, right=0.98, top=0.95, wspace=None, hspace=0.0, ) grid = (31, 1) # Axes showing the data. data_plot = plt.subplot2grid(grid, (0, 0), rowspan=8) else: # Only show one axes it the traces are not accepted. plt.figure(figsize=(18, 3)) # Plot envelopes if needed. if accept_traces is True: plt.plot( times, data_env, color="black", alpha=0.5, lw=0.4, label="data envelope", ) plt.plot( synthetic_trace.times(), synth_env, color="#e41a1c", alpha=0.4, lw=0.5, label="synthetics envelope", ) plt.plot(times, data, color="black", label="data", lw=1.5) plt.plot( synthetic_trace.times(), synth, color="#e41a1c", label="synthetics", lw=1.5, ) # Symmetric around y axis. middle = data.mean() d_max, d_min = data.max(), data.min() r = max(d_max - middle, middle - d_min) * 1.1 ylim = (middle - r, middle + r) xlim = (times[0], times[-1]) plt.ylim(*ylim) plt.xlim(*xlim) offset = (xlim[1] - xlim[0]) * 0.005 plt.vlines(first_tt_arrival, ylim[0], ylim[1], colors="#ff7f00", lw=2) plt.text( first_tt_arrival + offset, ylim[1] - (ylim[1] - ylim[0]) * 0.02, "first arrival", verticalalignment="top", horizontalalignment="left", color="#ee6e00", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white") ], ) plt.vlines( first_tt_arrival - minimum_period / 2.0, ylim[0], ylim[1], colors="#ff7f00", lw=2, ) plt.text( first_tt_arrival - minimum_period / 2.0 - offset, ylim[0] + (ylim[1] - ylim[0]) * 0.02, "first arrival - min period / 2", verticalalignment="bottom", horizontalalignment="right", color="#ee6e00", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white") ], ) for velocity in [6, 5, 4, 3, min_velocity]: tt = dist_in_km / velocity plt.vlines(tt, ylim[0], ylim[1], colors="gray", lw=2) if velocity == min_velocity: hal = "right" o_s = -1.0 * offset else: hal = "left" o_s = offset plt.text( tt + o_s, ylim[0] + (ylim[1] - ylim[0]) * 0.02, str(velocity) + " km/s", verticalalignment="bottom", horizontalalignment=hal, color="0.15", ) plt.vlines( dist_in_km / min_velocity + minimum_period / 2.0, ylim[0], ylim[1], colors="gray", lw=2, ) plt.text( dist_in_km / min_velocity + minimum_period / 2.0 - offset, ylim[1] - (ylim[1] - ylim[0]) * 0.02, "min surface velocity + min period / 2", verticalalignment="top", horizontalalignment="right", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white") ], ) plt.hlines( noise_absolute, xlim[0], xlim[1], linestyle="--", color="gray" ) plt.hlines( -noise_absolute, xlim[0], xlim[1], linestyle="--", color="gray" ) plt.text( offset, noise_absolute + (ylim[1] - ylim[0]) * 0.01, "noise level", verticalalignment="bottom", horizontalalignment="left", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white") ], ) plt.legend( loc="lower right", fancybox=True, framealpha=0.5, fontsize="small" ) plt.gca().xaxis.set_ticklabels([]) # Plot the basic global information. ax = plt.gca() txt = ( "Total CC Coeff: %.4f\nAbsolute Noise: %e\nRelative Noise: %.3f" % (cc, noise_absolute, noise_relative,) ) ax.text( 0.01, 0.95, txt, transform=ax.transAxes, fontdict=dict(fontsize="small", ha="left", va="top"), bbox=dict(boxstyle="round", fc="w", alpha=0.8), ) plt.suptitle("Channel %s" % data_trace.id, fontsize="larger") # Show plot and return if not accepted. if accept_traces is not True: txt = "Rejected: %s" % (accept_traces) ax.text( 0.99, 0.95, txt, transform=ax.transAxes, fontdict=dict(fontsize="small", ha="right", va="top"), bbox=dict(boxstyle="round", fc="red", alpha=1.0), ) plt.show() if accept_traces is not True: return [] # Initialise masked arrays. The mask will be set to True where no # windows are chosen. time_windows = np.ma.ones(npts) time_windows.mask = False if plot: old_time_windows = time_windows.copy() # Elimination Stage 1: Eliminate everything half a period before or # after the minimum and maximum travel times, respectively. # theoretical arrival as positive. # Account for delays in the source time functions as well min_idx = first_tt_arrival - int(minimum_period / dt) min_idx = max(0, min_idx) if global_inversion: max_idx = len(synth) else: stf_env = obspy.signal.filter.envelope(stf_trace) max_env_amplitude_idx = np.argmax(stf_env.data) threshold = 0.5 * np.max(stf_env.data) max_idx = int( math.ceil((dist_in_km / min_velocity + minimum_period / 2.0) / dt) ) max_idx += int( np.argmax(stf_env[max_env_amplitude_idx:] < threshold) + np.argmax(stf_env.data) ) if window_everything and accept_traces is True: windows = [(data_starttime + dt * min_idx, data_starttime + max_idx * dt, 1.0)] return windows time_windows.mask[: min_idx + 1] = True time_windows.mask[max_idx:] = True if plot: plt.subplot2grid(grid, (8, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="TRAVELTIME ELIMINATION" ) old_time_windows = time_windows.copy() # ------------------------------------------------------------------------- # Compute sliding time shifts and correlation coefficients for time # frames that passed the traveltime elimination stage. # ------------------------------------------------------------------------- # Allocate arrays to collect the time dependent values. sliding_time_shift = np.ma.zeros(npts, dtype="float32") sliding_time_shift.mask = True max_cc_coeff = np.ma.zeros(npts, dtype="float32") max_cc_coeff.mask = True # Compute the amount of indices by which to shift the sliding windows # for long seismograms this otherwise gets unnecessarily expensive window_shift = int(0.05 * window_length) if not window_shift % 2: window_shift += 1 window_shift = max(window_shift, 1) for start_idx, end_idx, midpoint_idx in _window_generator( npts, window_length, window_shift ): if not min_idx < midpoint_idx < max_idx: continue # Slice windows. Create a copy to be able to taper without affecting # the original time series. data_window = data[start_idx:end_idx].copy() * taper synthetic_window = synth[start_idx:end_idx].copy() * taper # Elimination Stage 2: Skip windows that have essentially no energy # to avoid instabilities. No windows can be picked in these. sw_start_idx = int(midpoint_idx - ((window_shift - 1) / 2)) sw_end_idx = int(midpoint_idx + ((window_shift - 1) / 2) + 1) if synthetic_window.ptp() < synth.ptp() * 0.001: time_windows.mask[sw_start_idx:sw_end_idx] = True continue # Calculate the time shift. Here this is defined as the shift of the # synthetics relative to the data. So a value of 2, for instance, means # that the synthetics are 2 timesteps later then the data. cc = np.correlate(data_window, synthetic_window, mode="full") time_shift = cc.argmax() - window_length + 1 # Express the time shift in fraction of the minimum period. sliding_time_shift[sw_start_idx:sw_end_idx] = ( time_shift * dt ) / minimum_period # Normalized cross correlation. max_cc_value = cc.max() / np.sqrt( (synthetic_window ** 2).sum() * (data_window ** 2).sum() ) max_cc_coeff[sw_start_idx:sw_end_idx] = max_cc_value if plot: plt.subplot2grid(grid, (9, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="NO ENERGY IN CC WINDOW" ) # Axes with the CC coeffs plt.subplot2grid(grid, (15, 0), rowspan=4) plt.hlines(0, xlim[0], xlim[1], color="lightgray") plt.hlines( -threshold_shift, xlim[0], xlim[1], color="gray", linestyle="--" ) plt.hlines( threshold_shift, xlim[0], xlim[1], color="gray", linestyle="--" ) plt.text( 5, -threshold_shift - (2) * 0.03, "threshold", verticalalignment="top", horizontalalignment="left", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white") ], ) plt.plot( times, sliding_time_shift, color="#377eb8", label="Time shift in fraction of minimum period", lw=1.5, ) ylim = plt.ylim() plt.yticks([-0.75, 0, 0.75]) plt.xticks([300, 600, 900, 1200, 1500, 1800]) plt.ylim(ylim[0], ylim[1] + ylim[1] - ylim[0]) plt.ylim(-1.0, 1.0) plt.xlim(xlim) plt.gca().xaxis.set_ticklabels([]) plt.legend( loc="lower right", fancybox=True, framealpha=0.5, fontsize="small" ) plt.subplot2grid(grid, (10, 0), rowspan=4) plt.hlines( threshold_correlation, xlim[0], xlim[1], color="0.15", linestyle="--", ) plt.hlines(1, xlim[0], xlim[1], color="lightgray") plt.hlines(0, xlim[0], xlim[1], color="lightgray") plt.text( 5, threshold_correlation + (1.4) * 0.01, "threshold", verticalalignment="bottom", horizontalalignment="left", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white") ], ) plt.plot( times, max_cc_coeff, color="#4daf4a", label="Maximum CC coefficient", lw=1.5, ) plt.ylim(-0.2, 1.2) plt.yticks([0, 0.5, 1]) plt.xticks([300, 600, 900, 1200, 1500, 1800]) plt.xlim(xlim) plt.gca().xaxis.set_ticklabels([]) plt.legend( loc="lower right", fancybox=True, framealpha=0.5, fontsize="small" ) # Elimination Stage 3: Mark all areas where the normalized cross # correlation coefficient is under threshold_correlation as negative if plot: old_time_windows = time_windows.copy() time_windows.mask[max_cc_coeff < threshold_correlation] = True if plot: plt.subplot2grid(grid, (14, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="CORRELATION COEFF THRESHOLD ELIMINATION", ) # Elimination Stage 4: Mark everything with an absolute travel time # shift of more than # threshold_shift times the dominant period as # negative if plot: old_time_windows = time_windows.copy() time_windows.mask[np.ma.abs(sliding_time_shift) > threshold_shift] = True if plot: plt.subplot2grid(grid, (19, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="TIME SHIFT THRESHOLD ELIMINATION", ) # Elimination Stage 5: Mark the area around every "travel time shift # jump" (based on the traveltime time difference) negative. The width of # the area is currently chosen to be a tenth of a dominant period to # each side. if plot: old_time_windows = time_windows.copy() sample_buffer = int(np.ceil(minimum_period / dt * 0.1)) indices = np.ma.where(np.ma.abs(np.ma.diff(sliding_time_shift)) > 0.1)[0] for index in indices: time_windows.mask[index - sample_buffer : index + sample_buffer] = True if plot: plt.subplot2grid(grid, (20, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="TIME SHIFT JUMPS ELIMINATION" ) # Clip both to avoid large numbers by division. stacked = np.vstack( [ np.ma.clip( synth_env, synth_env.max() * min_envelope_similarity * 0.5, synth_env.max(), ), np.ma.clip( data_env, data_env.max() * min_envelope_similarity * 0.5, data_env.max(), ), ] ) # Ratio. ratio = stacked.min(axis=0) / stacked.max(axis=0) # Elimination Stage 6: Make sure the amplitudes of both don't vary too # much. if plot: old_time_windows = time_windows.copy() time_windows.mask[ratio < min_envelope_similarity] = True if plot: plt.subplot2grid(grid, (25, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="ENVELOPE AMPLITUDE SIMILARITY ELIMINATION", ) if plot: plt.subplot2grid(grid, (21, 0), rowspan=4) plt.hlines( min_envelope_similarity, xlim[0], xlim[1], color="gray", linestyle="--", ) plt.text( 5, min_envelope_similarity + (2) * 0.03, "threshold", verticalalignment="bottom", horizontalalignment="left", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white") ], ) plt.plot( times, ratio, color="#9B59B6", label="Envelope amplitude similarity", lw=1.5, ) plt.yticks([0, 0.2, 0.4, 0.6, 0.8, 1.0]) plt.ylim(0.05, 1.05) plt.xticks([300, 600, 900, 1200, 1500, 1800]) plt.xlim(xlim) plt.gca().xaxis.set_ticklabels([]) plt.legend( loc="lower right", fancybox=True, framealpha=0.5, fontsize="small" ) # First minimum window length elimination stage. This is cheap and if # not done it can easily destabilize the peak-and-trough marching stage # which would then have to deal with way more edge cases. if plot: old_time_windows = time_windows.copy() min_length = min( minimum_period / dt * min_length_period, maximum_period / dt ) for i in flatnotmasked_contiguous(time_windows): # Step 7: Throw away all windows with a length of less then # min_length_period the dominant period. if (i.stop - i.start) < min_length: time_windows.mask[i.start : i.stop] = True if plot: plt.subplot2grid(grid, (26, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="MINIMUM WINDOW LENGTH ELIMINATION 1", ) # ------------------------------------------------------------------------- # Peak and trough marching algorithm # ------------------------------------------------------------------------- final_windows = [] for i in flatnotmasked_contiguous(time_windows): # Cut respective windows. window_npts = i.stop - i.start synthetic_window = synth[i.start : i.stop] data_window = data[i.start : i.stop] # Find extrema in the data and the synthetics. data_p, data_t = find_local_extrema(data_window) synth_p, synth_t = find_local_extrema(synthetic_window) window_mask = np.ones(window_npts, dtype="bool") # sometimes, no local extrema are found and the below fails # in that case dont crash, but return the windows that did not fail. try: closest_peaks = find_closest(data_p, synth_p) diffs = np.diff(closest_peaks) for idx in np.where(diffs == 1)[0]: if idx > 0: start = synth_p[idx - 1] else: start = 0 if idx < (len(synth_p) - 1): end = synth_p[idx + 1] else: end = -1 window_mask[start:end] = False closest_troughs = find_closest(data_t, synth_t) diffs = np.diff(closest_troughs) for idx in np.where(diffs == 1)[0]: if idx > 0: start = synth_t[idx - 1] else: start = 0 if idx < (len(synth_t) - 1): end = synth_t[idx + 1] else: end = -1 window_mask[start:end] = False except Exception as e: print(e) continue window_mask = np.ma.masked_array(window_mask, mask=window_mask) if window_mask.mask.all(): continue for j in flatnotmasked_contiguous(window_mask): final_windows.append((i.start + j.start, i.start + j.stop)) if plot: old_time_windows = time_windows.copy() time_windows.mask[:] = True for start, stop in final_windows: time_windows.mask[start:stop] = False if plot: plt.subplot2grid(grid, (27, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="PEAK AND TROUGH MARCHING ELIMINATION", ) # Loop through all the time windows, remove windows not satisfying the # minimum number of peaks and troughs per window. Acts mainly as a # safety guard. old_time_windows = time_windows.copy() for i in flatnotmasked_contiguous(old_time_windows): synthetic_window = synth[i.start : i.stop] data_window = data[i.start : i.stop] data_p, data_t = find_local_extrema(data_window) synth_p, synth_t = find_local_extrema(synthetic_window) if ( np.min([len(synth_p), len(synth_t), len(data_p), len(data_t)]) < min_peaks_troughs ): time_windows.mask[i.start : i.stop] = True if plot: plt.subplot2grid(grid, (28, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="PEAK/TROUGH COUNT ELIMINATION", ) # Second minimum window length elimination stage. if plot: old_time_windows = time_windows.copy() min_length = min( minimum_period / dt * min_length_period, maximum_period / dt ) for i in flatnotmasked_contiguous(time_windows): # Step 7: Throw away all windows with a length of less then # min_length_period the dominant period. if (i.stop - i.start) < min_length: time_windows.mask[i.start : i.stop] = True if plot: plt.subplot2grid(grid, (29, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="MINIMUM WINDOW LENGTH ELIMINATION 2", ) # Final step, eliminating windows with little energy. final_windows = [] for j in flatnotmasked_contiguous(time_windows): # Again assert a certain minimal length. if (j.stop - j.start) < min_length: continue # Compare the energy in the data window and the synthetic window. data_energy = (data[j.start : j.stop] ** 2).sum() synth_energy = (synth[j.start : j.stop] ** 2).sum() energies = sorted([data_energy, synth_energy]) if energies[1] > max_energy_ratio * energies[0]: if verbose: _log_window_selection( data_trace.id, "Deselecting window due to energy ratio between " "data and synthetics.", ) continue # Check that amplitudes in the data are above the noise if noise_absolute / data[j.start : j.stop].ptp() > max_noise_window: if verbose: _log_window_selection( data_trace.id, "Deselecting window due having no amplitude above the " "signal to noise ratio.", ) final_windows.append((j.start, j.stop)) if plot: old_time_windows = time_windows.copy() time_windows.mask[:] = True for start, stop in final_windows: time_windows.mask[start:stop] = False if plot: plt.subplot2grid(grid, (30, 0), rowspan=1) _plot_mask( time_windows, old_time_windows, name="LITTLE ENERGY ELIMINATION" ) if verbose: _log_window_selection( data_trace.id, "Done, Selected %i window(s)" % len(final_windows) ) # Final step is to convert the index value windows to actual times. windows = [] for start, stop in final_windows: start = data_starttime + start * data_delta stop = data_starttime + stop * data_delta weight = 1.0 windows.append((start, stop, weight)) if plot: # Plot the final windows to the data axes. import matplotlib.transforms as mtransforms # NOQA ax = data_plot trans = mtransforms.blended_transform_factory( ax.transData, ax.transAxes ) for start, stop in final_windows: ax.fill_between( [start * data_delta, stop * data_delta], 0, 1, facecolor="#CDDC39", alpha=0.5, transform=trans, ) plt.show() return windows
def test_calc_vincenty_inverse_tabulated(self): """ Tabulated results for Vincenty Inverse Table II of Vincenty's paper (T. Vincenty 1975, "Direct and inverse solutions of geodesics on the ellipsoid with application of nested equations" Survey Review XXII pp.88-93) has five test examples for the forward and inverse problem (with results rounded to 0.00001 seconds of arc and 1 mm). The inverse versions of these are implemented here. Note the non-standard (old) ellipsoid usage. Here we test that we match these examples for the inverse problem. """ # Row "A" # NB: for this case there seems to be a typo in # the tabulated data. Tabulated data is commented # out and values from geographiclib are used in their place # dist = 14110526.170 dist = 14039003.954192352 # azi1 = dms2dec(96.0, 36.0, 8.79960) azi1 = 95.88145755849257 # azi2 = dms2dec(137.0, 52.0, 22.01454) azi2 = 138.30481836546775 bazi = azi2 + 180.0 lat1 = dms2dec(55.0, 45.0, 0.0) lat2 = dms2dec(-33.0, 26.0, 0.0) lon2 = dms2dec(108.0, 13.0, 0.0) a = 6377397.155 f = 1.0 / 299.1528128 calc_dist, calc_azi1, calc_bazi = calc_vincenty_inverse( lat1, 0.0, lat2, lon2, a, f) self.assertAlmostEqual(dist, calc_dist, 2) self.assertAlmostEqual(azi1, calc_azi1, 5) self.assertAlmostEqual(bazi, calc_bazi, 5) # Row "B" dist = 4085966.703 azi1 = dms2dec(95.0, 27.0, 59.63089) azi2 = dms2dec(118, 5.0, 58.96161) bazi = azi2 + 180.0 lat1 = dms2dec(37.0, 19.0, 54.95367) lat2 = dms2dec(26.0, 7.0, 42.83946) lon2 = dms2dec(41.0, 28.0, 35.50729) a = 6378388.000 f = 1.0 / 297.0 calc_dist, calc_azi1, calc_bazi = calc_vincenty_inverse( lat1, 0.0, lat2, lon2, a, f) self.assertAlmostEqual(dist, calc_dist, 2) self.assertAlmostEqual(azi1, calc_azi1, 5) self.assertAlmostEqual(bazi, calc_bazi, 5) # Row "C" dist = 8084823.839 azi1 = dms2dec(15.0, 44.0, 23.74850) azi2 = dms2dec(144.0, 55.0, 39.92147) bazi = azi2 + 180.0 lat1 = dms2dec(35.0, 16.0, 11.24862) lat2 = dms2dec(67.0, 22.0, 14.77638) lon2 = dms2dec(137.0, 47.0, 28.31435) a = 6378388.000 f = 1.0 / 297.0 calc_dist, calc_azi1, calc_bazi = calc_vincenty_inverse( lat1, 0.0, lat2, lon2, a, f) self.assertAlmostEqual(dist, calc_dist, 2) self.assertAlmostEqual(azi1, calc_azi1, 5) self.assertAlmostEqual(bazi, calc_bazi, 5)
s_pick_time = [] window = [] channel_list = [] # selecting events within distance threshold cntEv=0 f = open(eventFileOut,'w') outStr=('year,month,day,hour,minute,second,magnitude,latitude,longitude,id\n') f.write(outStr) for ev_num, event_id in enumerate(df_event.id): # print(event_id + ' (' + str(ev_num + 1) + '/' + str(len(df_event.id)) + ')') lonEv=df_event.longitude[ev_num] latEv=df_event.latitude[ev_num] magEv=df_event.magnitude[ev_num] # calc_vincenty_inverse(lat1, lon1, lat2, lon2, a=6378137.0, f=0.0033528106647474805)[source] dist_m,az,baz=calc_vincenty_inverse(latEv, lonEv, latCenter, lonCenter, a=6378137.0, f=0.0033528106647474805) dist_km=dist_m/1000 if (dist_km < distEv and magEv>=minMag): # print('Event within distance threshold: ',dist_km,'(',lonEv,latEv,')') # outStr=('year,month,day,hour,minute,second,magnitude,latitude,longitude,id\n') outStr=(str(df_event.year[ev_num])+','+str(format2(df_event.month[ev_num]))+','+str(format2(df_event.day[ev_num]))+','+str(format2(df_event.hour[ev_num]))+','+str(format2(df_event.minute[ev_num]))+','+str(format2(df_event.second[ev_num]))+','+str(df_event.magnitude[ev_num])+','+str(df_event.latitude[ev_num])+','+str(df_event.longitude[ev_num])+','+str(df_event.id[ev_num])+'\n') f.write(outStr) cntEv +=1 # list number of events selected and close file print(' Number of events within distance threshold of ',distEv,'km =',cntEv) f.close() # reference start and end times from input to parameter file # first-available data is from event 20130801 #t_start = UTCDateTime('2009'+'-'+'01'+'-'+'01'+'T00:00:00.000') t_start = UTCDateTime('2013'+'-'+'08'+'-'+'01'+'T00:00:00.000')
# Read station data with open("STATIONS") as f: for line in f: sta, net, sta_lat, sta_lon, _, _ = line.split() sta_lat = float(sta_lat) sta_lon = float(sta_lon) break # In[5]: # Read synthetic data synts = obspy.read("seis/AA.A0001.MX*.sem.sac") # Calculate distance, azimuth, backazimuth dist_m, az, baz = calc_vincenty_inverse(event_lat, event_lon, sta_lat, sta_lon) # Rotate seismograms synts.rotate("NE->RT", back_azimuth=baz) # Select transverse component and bandpass filter 40-250 s synt = synts.select(channel="*T")[0] synt.filter("bandpass", freqmin=1.0/250, freqmax=1.0/40, zerophase=True) # Copy synt trace to get "observed" data obsd = synt.copy() # "observed" data is shifted by 3 seconds and have 0.9 amplitude of the synt shift = int(round(3.0/synt.stats.delta)) obsd.data[:-shift] = obsd.data[shift:] obsd.data[-shift:] = 0 obsd.data *= 0.9 # Plot the data fig, ax = plt.subplots(figsize=(20, 5))
def test_calc_vincenty_inverse_tabulated(self): """ Tabulated results for Vincenty Inverse Table II of Vincenty's paper (T. Vincenty 1975, "Direct and inverse solutions of geodesics on the ellipsoid with application of nested equations" Survey Review XXII pp.88-93) has five test examples for the forward and inverse problem (with results rounded to 0.00001 seconds of arc and 1 mm). The inverse versions of these are implemented here. Note the non-standard (old) ellipsoid usage. Here we test that we match these examples for the inverse problem. """ # Row "A" # NB: for this case there seems to be a typo in # the tabulated data. Tabulated data is commented # out and values from geographiclib are used in their place # dist = 14110526.170 dist = 14039003.954192352 # azi1 = dms2dec(96.0, 36.0, 8.79960) azi1 = 95.88145755849257 # azi2 = dms2dec(137.0, 52.0, 22.01454) azi2 = 138.30481836546775 bazi = azi2 + 180.0 lat1 = dms2dec(55.0, 45.0, 0.0) lat2 = dms2dec(-33.0, 26.0, 0.0) lon2 = dms2dec(108.0, 13.0, 0.0) a = 6377397.155 f = 1.0/299.1528128 calc_dist, calc_azi1, calc_bazi = calc_vincenty_inverse( lat1, 0.0, lat2, lon2, a, f) self.assertAlmostEqual(dist, calc_dist, 2) self.assertAlmostEqual(azi1, calc_azi1, 5) self.assertAlmostEqual(bazi, calc_bazi, 5) # Row "B" dist = 4085966.703 azi1 = dms2dec(95.0, 27.0, 59.63089) azi2 = dms2dec(118, 5.0, 58.96161) bazi = azi2 + 180.0 lat1 = dms2dec(37.0, 19.0, 54.95367) lat2 = dms2dec(26.0, 7.0, 42.83946) lon2 = dms2dec(41.0, 28.0, 35.50729) a = 6378388.000 f = 1.0/297.0 calc_dist, calc_azi1, calc_bazi = calc_vincenty_inverse( lat1, 0.0, lat2, lon2, a, f) self.assertAlmostEqual(dist, calc_dist, 2) self.assertAlmostEqual(azi1, calc_azi1, 5) self.assertAlmostEqual(bazi, calc_bazi, 5) # Row "C" dist = 8084823.839 azi1 = dms2dec(15.0, 44.0, 23.74850) azi2 = dms2dec(144.0, 55.0, 39.92147) bazi = azi2 + 180.0 lat1 = dms2dec(35.0, 16.0, 11.24862) lat2 = dms2dec(67.0, 22.0, 14.77638) lon2 = dms2dec(137.0, 47.0, 28.31435) a = 6378388.000 f = 1.0/297.0 calc_dist, calc_azi1, calc_bazi = calc_vincenty_inverse( lat1, 0.0, lat2, lon2, a, f) self.assertAlmostEqual(dist, calc_dist, 2) self.assertAlmostEqual(azi1, calc_azi1, 5) self.assertAlmostEqual(bazi, calc_bazi, 5)
def select_windows(data_trace, synthetic_trace, event_latitude, event_longitude, event_depth_in_km, station_latitude, station_longitude, minimum_period, maximum_period, min_cc=0.10, max_noise=0.10, max_noise_window=0.4, min_velocity=2.4, threshold_shift=0.30, threshold_correlation=0.75, min_length_period=1.5, min_peaks_troughs=2, max_energy_ratio=10.0, min_envelope_similarity=0.2, verbose=False, plot=False): """ Window selection algorithm for picking windows suitable for misfit calculation based on phase differences. Returns a list of windows which might be empty due to various reasons. This function is really long and a lot of things. For a more detailed description, please see the LASIF paper. :param data_trace: The data trace. :type data_trace: :class:`~obspy.core.trace.Trace` :param synthetic_trace: The synthetic trace. :type synthetic_trace: :class:`~obspy.core.trace.Trace` :param event_latitude: The event latitude. :type event_latitude: float :param event_longitude: The event longitude. :type event_longitude: float :param event_depth_in_km: The event depth in km. :type event_depth_in_km: float :param station_latitude: The station latitude. :type station_latitude: float :param station_longitude: The station longitude. :type station_longitude: float :param minimum_period: The minimum period of the data in seconds. :type minimum_period: float :param maximum_period: The maximum period of the data in seconds. :type maximum_period: float :param min_cc: Minimum normalised correlation coefficient of the complete traces. :type min_cc: float :param max_noise: Maximum relative noise level for the whole trace. Measured from maximum amplitudes before and after the first arrival. :type max_noise: float :param max_noise_window: Maximum relative noise level for individual windows. :type max_noise_window: float :param min_velocity: All arrivals later than those corresponding to the threshold velocity [km/s] will be excluded. :type min_velocity: float :param threshold_shift: Maximum allowable time shift within a window, as a fraction of the minimum period. :type threshold_shift: float :param threshold_correlation: Minimum normalised correlation coeeficient within a window. :type threshold_correlation: float :param min_length_period: Minimum length of the time windows relative to the minimum period. :type min_length_period: float :param min_peaks_troughs: Minimum number of extrema in an individual time window (excluding the edges). :type min_peaks_troughs: float :param max_energy_ratio: Maximum energy ratio between data and synthetics within a time window. Don't make this too small! :type max_energy_ratio: float :param min_envelope_similarity: The minimum similarity of the envelopes of both data and synthetics. This essentially assures that the amplitudes of data and synthetics can not diverge too much within a window. It is a bit like the inverse of the ratio of both envelopes so a value of 0.2 makes sure neither amplitude can be more then 5 times larger than the other. :type min_envelope_similarity: float :param verbose: No output by default. :type verbose: bool :param plot: Create a plot of the algortihm while it does its work. :type plot: bool """ # Shortcuts to frequently accessed variables. data_starttime = data_trace.stats.starttime data_delta = data_trace.stats.delta dt = data_trace.stats.delta npts = data_trace.stats.npts synth = synthetic_trace.data data = data_trace.data times = data_trace.times() # Fill cache if necessary. if not TAUPY_MODEL_CACHE: from obspy.taup import TauPyModel # NOQA TAUPY_MODEL_CACHE["model"] = TauPyModel("AK135") model = TAUPY_MODEL_CACHE["model"] # ------------------------------------------------------------------------- # Geographical calculations and the time of the first arrival. # ------------------------------------------------------------------------- dist_in_deg = geodetics.locations2degrees(station_latitude, station_longitude, event_latitude, event_longitude) dist_in_km = geodetics.calc_vincenty_inverse( station_latitude, station_longitude, event_latitude, event_longitude)[0] / 1000.0 # Get only a couple of P phases which should be the first arrival # for every epicentral distance. Its quite a bit faster than calculating # the arrival times for every phase. # Assumes the first sample is the centroid time of the event. tts = model.get_travel_times(source_depth_in_km=event_depth_in_km, distance_in_degree=dist_in_deg, phase_list=["ttp"]) # Sort just as a safety measure. tts = sorted(tts, key=lambda x: x.time) first_tt_arrival = tts[0].time # ------------------------------------------------------------------------- # Window settings # ------------------------------------------------------------------------- # Number of samples in the sliding window. Currently, the length of the # window is set to a multiple of the dominant period of the synthetics. # Make sure it is an uneven number; just to have a trivial midpoint # definition and one sample does not matter much in any case. window_length = int(round(float(2 * minimum_period) / dt)) if not window_length % 2: window_length += 1 # Use a Hanning window. No particular reason for it but its a well-behaved # window and has nice spectral properties. taper = np.hanning(window_length) # ========================================================================= # check if whole seismograms are sufficiently correlated and estimate # noise level # ========================================================================= # Overall Correlation coefficient. norm = np.sqrt(np.sum(data ** 2)) * np.sqrt(np.sum(synth ** 2)) cc = np.sum(data * synth) / norm if verbose: _log_window_selection(data_trace.id, "Correlation Coefficient: %.4f" % cc) # Estimate noise level from waveforms prior to the first arrival. idx_end = int(np.ceil((first_tt_arrival - 0.5 * minimum_period) / dt)) idx_end = max(10, idx_end) idx_start = int(np.ceil((first_tt_arrival - 2.5 * minimum_period) / dt)) idx_start = max(10, idx_start) if idx_start >= idx_end: idx_start = max(0, idx_end - 10) abs_data = np.abs(data) noise_absolute = abs_data[idx_start:idx_end].max() noise_relative = noise_absolute / abs_data.max() if verbose: _log_window_selection(data_trace.id, "Absolute Noise Level: %e" % noise_absolute) _log_window_selection(data_trace.id, "Relative Noise Level: %e" % noise_relative) # Basic global rejection criteria. accept_traces = True if (cc < min_cc) and (noise_relative > max_noise / 3.0): msg = "Correlation %.4f is below threshold of %.4f" % (cc, min_cc) if verbose: _log_window_selection(data_trace.id, msg) accept_traces = msg if noise_relative > max_noise: msg = "Noise level %.3f is above threshold of %.3f" % ( noise_relative, max_noise) if verbose: _log_window_selection( data_trace.id, msg) accept_traces = msg # Calculate the envelope of both data and synthetics. This is to make sure # that the amplitude of both is not too different over time and is # used as another selector. Only calculated if the trace is generally # accepted as it is fairly slow. if accept_traces is True: data_env = obspy.signal.filter.envelope(data) synth_env = obspy.signal.filter.envelope(synth) # ------------------------------------------------------------------------- # Initial Plot setup. # ------------------------------------------------------------------------- # All the plot calls are interleaved. I realize this is really ugly but # the alternative would be to either have two functions (one with plots, # one without) or split the plotting function in various subfunctions, # neither of which are acceptable in my opinion. The impact on # performance is minimal if plotting is turned off: all imports are lazy # and a couple of conditionals are cheap. if plot: import matplotlib.pylab as plt # NOQA import matplotlib.patheffects as PathEffects # NOQA if accept_traces is True: plt.figure(figsize=(18, 12)) plt.subplots_adjust(left=0.05, bottom=0.05, right=0.98, top=0.95, wspace=None, hspace=0.0) grid = (31, 1) # Axes showing the data. data_plot = plt.subplot2grid(grid, (0, 0), rowspan=8) else: # Only show one axes it the traces are not accepted. plt.figure(figsize=(18, 3)) # Plot envelopes if needed. if accept_traces is True: plt.plot(times, data_env, color="black", alpha=0.5, lw=0.4, label="data envelope") plt.plot(synthetic_trace.times(), synth_env, color="#e41a1c", alpha=0.4, lw=0.5, label="synthetics envelope") plt.plot(times, data, color="black", label="data", lw=1.5) plt.plot(synthetic_trace.times(), synth, color="#e41a1c", label="synthetics", lw=1.5) # Symmetric around y axis. middle = data.mean() d_max, d_min = data.max(), data.min() r = max(d_max - middle, middle - d_min) * 1.1 ylim = (middle - r, middle + r) xlim = (times[0], times[-1]) plt.ylim(*ylim) plt.xlim(*xlim) offset = (xlim[1] - xlim[0]) * 0.005 plt.vlines(first_tt_arrival, ylim[0], ylim[1], colors="#ff7f00", lw=2) plt.text(first_tt_arrival + offset, ylim[1] - (ylim[1] - ylim[0]) * 0.02, "first arrival", verticalalignment="top", horizontalalignment="left", color="#ee6e00", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white")]) plt.vlines(first_tt_arrival - minimum_period / 2.0, ylim[0], ylim[1], colors="#ff7f00", lw=2) plt.text(first_tt_arrival - minimum_period / 2.0 - offset, ylim[0] + (ylim[1] - ylim[0]) * 0.02, "first arrival - min period / 2", verticalalignment="bottom", horizontalalignment="right", color="#ee6e00", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white")]) for velocity in [6, 5, 4, 3, min_velocity]: tt = dist_in_km / velocity plt.vlines(tt, ylim[0], ylim[1], colors="gray", lw=2) if velocity == min_velocity: hal = "right" o_s = -1.0 * offset else: hal = "left" o_s = offset plt.text(tt + o_s, ylim[0] + (ylim[1] - ylim[0]) * 0.02, str(velocity) + " km/s", verticalalignment="bottom", horizontalalignment=hal, color="0.15") plt.vlines(dist_in_km / min_velocity + minimum_period / 2.0, ylim[0], ylim[1], colors="gray", lw=2) plt.text(dist_in_km / min_velocity + minimum_period / 2.0 - offset, ylim[1] - (ylim[1] - ylim[0]) * 0.02, "min surface velocity + min period / 2", verticalalignment="top", horizontalalignment="right", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white")]) plt.hlines(noise_absolute, xlim[0], xlim[1], linestyle="--", color="gray") plt.hlines(-noise_absolute, xlim[0], xlim[1], linestyle="--", color="gray") plt.text(offset, noise_absolute + (ylim[1] - ylim[0]) * 0.01, "noise level", verticalalignment="bottom", horizontalalignment="left", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white")]) plt.legend(loc="lower right", fancybox=True, framealpha=0.5, fontsize="small") plt.gca().xaxis.set_ticklabels([]) # Plot the basic global information. ax = plt.gca() txt = ( "Total CC Coeff: %.4f\nAbsolute Noise: %e\nRelative Noise: %.3f" % (cc, noise_absolute, noise_relative)) ax.text(0.01, 0.95, txt, transform=ax.transAxes, fontdict=dict(fontsize="small", ha='left', va='top'), bbox=dict(boxstyle="round", fc="w", alpha=0.8)) plt.suptitle("Channel %s" % data_trace.id, fontsize="larger") # Show plot and return if not accepted. if accept_traces is not True: txt = "Rejected: %s" % (accept_traces) ax.text(0.99, 0.95, txt, transform=ax.transAxes, fontdict=dict(fontsize="small", ha='right', va='top'), bbox=dict(boxstyle="round", fc="red", alpha=1.0)) plt.show() if accept_traces is not True: return [] # Initialise masked arrays. The mask will be set to True where no # windows are chosen. time_windows = np.ma.ones(npts) time_windows.mask = False if plot: old_time_windows = time_windows.copy() # Elimination Stage 1: Eliminate everything half a period before or # after the minimum and maximum travel times, respectively. # theoretical arrival as positive. min_idx = int((first_tt_arrival - (minimum_period / 2.0)) / dt) max_idx = int(math.ceil(( dist_in_km / min_velocity + minimum_period / 2.0) / dt)) time_windows.mask[:min_idx + 1] = True time_windows.mask[max_idx:] = True if plot: plt.subplot2grid(grid, (8, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="TRAVELTIME ELIMINATION") old_time_windows = time_windows.copy() # ------------------------------------------------------------------------- # Compute sliding time shifts and correlation coefficients for time # frames that passed the traveltime elimination stage. # ------------------------------------------------------------------------- # Allocate arrays to collect the time dependent values. sliding_time_shift = np.ma.zeros(npts, dtype="float32") sliding_time_shift.mask = True max_cc_coeff = np.ma.zeros(npts, dtype="float32") max_cc_coeff.mask = True for start_idx, end_idx, midpoint_idx in _window_generator(npts, window_length): if not min_idx < midpoint_idx < max_idx: continue # Slice windows. Create a copy to be able to taper without affecting # the original time series. data_window = data[start_idx: end_idx].copy() * taper synthetic_window = \ synth[start_idx: end_idx].copy() * taper # Elimination Stage 2: Skip windows that have essentially no energy # to avoid instabilities. No windows can be picked in these. if synthetic_window.ptp() < synth.ptp() * 0.001: time_windows.mask[midpoint_idx] = True continue # Calculate the time shift. Here this is defined as the shift of the # synthetics relative to the data. So a value of 2, for instance, means # that the synthetics are 2 timesteps later then the data. cc = np.correlate(data_window, synthetic_window, mode="full") time_shift = cc.argmax() - window_length + 1 # Express the time shift in fraction of the minimum period. sliding_time_shift[midpoint_idx] = (time_shift * dt) / minimum_period # Normalized cross correlation. max_cc_value = cc.max() / np.sqrt((synthetic_window ** 2).sum() * (data_window ** 2).sum()) max_cc_coeff[midpoint_idx] = max_cc_value if plot: plt.subplot2grid(grid, (9, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="NO ENERGY IN CC WINDOW") # Axes with the CC coeffs plt.subplot2grid(grid, (15, 0), rowspan=4) plt.hlines(0, xlim[0], xlim[1], color="lightgray") plt.hlines(-threshold_shift, xlim[0], xlim[1], color="gray", linestyle="--") plt.hlines(threshold_shift, xlim[0], xlim[1], color="gray", linestyle="--") plt.text(5, -threshold_shift - (2) * 0.03, "threshold", verticalalignment="top", horizontalalignment="left", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white")]) plt.plot(times, sliding_time_shift, color="#377eb8", label="Time shift in fraction of minimum period", lw=1.5) ylim = plt.ylim() plt.yticks([-0.75, 0, 0.75]) plt.xticks([300, 600, 900, 1200, 1500, 1800]) plt.ylim(ylim[0], ylim[1] + ylim[1] - ylim[0]) plt.ylim(-1.0, 1.0) plt.xlim(xlim) plt.gca().xaxis.set_ticklabels([]) plt.legend(loc="lower right", fancybox=True, framealpha=0.5, fontsize="small") plt.subplot2grid(grid, (10, 0), rowspan=4) plt.hlines(threshold_correlation, xlim[0], xlim[1], color="0.15", linestyle="--") plt.hlines(1, xlim[0], xlim[1], color="lightgray") plt.hlines(0, xlim[0], xlim[1], color="lightgray") plt.text(5, threshold_correlation + (1.4) * 0.01, "threshold", verticalalignment="bottom", horizontalalignment="left", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white")]) plt.plot(times, max_cc_coeff, color="#4daf4a", label="Maximum CC coefficient", lw=1.5) plt.ylim(-0.2, 1.2) plt.yticks([0, 0.5, 1]) plt.xticks([300, 600, 900, 1200, 1500, 1800]) plt.xlim(xlim) plt.gca().xaxis.set_ticklabels([]) plt.legend(loc="lower right", fancybox=True, framealpha=0.5, fontsize="small") # Elimination Stage 3: Mark all areas where the normalized cross # correlation coefficient is under threshold_correlation as negative if plot: old_time_windows = time_windows.copy() time_windows.mask[max_cc_coeff < threshold_correlation] = True if plot: plt.subplot2grid(grid, (14, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="CORRELATION COEFF THRESHOLD ELIMINATION") # Elimination Stage 4: Mark everything with an absolute travel time # shift of more than # threshold_shift times the dominant period as # negative if plot: old_time_windows = time_windows.copy() time_windows.mask[np.ma.abs(sliding_time_shift) > threshold_shift] = True if plot: plt.subplot2grid(grid, (19, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="TIME SHIFT THRESHOLD ELIMINATION") # Elimination Stage 5: Mark the area around every "travel time shift # jump" (based on the traveltime time difference) negative. The width of # the area is currently chosen to be a tenth of a dominant period to # each side. if plot: old_time_windows = time_windows.copy() sample_buffer = int(np.ceil(minimum_period / dt * 0.1)) indices = np.ma.where(np.ma.abs(np.ma.diff(sliding_time_shift)) > 0.1)[0] for index in indices: time_windows.mask[index - sample_buffer: index + sample_buffer] = True if plot: plt.subplot2grid(grid, (20, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="TIME SHIFT JUMPS ELIMINATION") # Clip both to avoid large numbers by division. stacked = np.vstack([ np.ma.clip(synth_env, synth_env.max() * min_envelope_similarity * 0.5, synth_env.max()), np.ma.clip(data_env, data_env.max() * min_envelope_similarity * 0.5, data_env.max())]) # Ratio. ratio = stacked.min(axis=0) / stacked.max(axis=0) # Elimination Stage 6: Make sure the amplitudes of both don't vary too # much. if plot: old_time_windows = time_windows.copy() time_windows.mask[ratio < min_envelope_similarity] = True if plot: plt.subplot2grid(grid, (25, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="ENVELOPE AMPLITUDE SIMILARITY ELIMINATION") if plot: plt.subplot2grid(grid, (21, 0), rowspan=4) plt.hlines(min_envelope_similarity, xlim[0], xlim[1], color="gray", linestyle="--") plt.text(5, min_envelope_similarity + (2) * 0.03, "threshold", verticalalignment="bottom", horizontalalignment="left", color="0.15", path_effects=[ PathEffects.withStroke(linewidth=3, foreground="white")]) plt.plot(times, ratio, color="#9B59B6", label="Envelope amplitude similarity", lw=1.5) plt.yticks([0, 0.2, 0.4, 0.6, 0.8, 1.0]) plt.ylim(0.05, 1.05) plt.xticks([300, 600, 900, 1200, 1500, 1800]) plt.xlim(xlim) plt.gca().xaxis.set_ticklabels([]) plt.legend(loc="lower right", fancybox=True, framealpha=0.5, fontsize="small") # First minimum window length elimination stage. This is cheap and if # not done it can easily destabilize the peak-and-trough marching stage # which would then have to deal with way more edge cases. if plot: old_time_windows = time_windows.copy() min_length = \ min(minimum_period / dt * min_length_period, maximum_period / dt) for i in flatnotmasked_contiguous(time_windows): # Step 7: Throw away all windows with a length of less then # min_length_period the dominant period. if (i.stop - i.start) < min_length: time_windows.mask[i.start: i.stop] = True if plot: plt.subplot2grid(grid, (26, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="MINIMUM WINDOW LENGTH ELIMINATION 1") # ------------------------------------------------------------------------- # Peak and trough marching algorithm # ------------------------------------------------------------------------- final_windows = [] for i in flatnotmasked_contiguous(time_windows): # Cut respective windows. window_npts = i.stop - i.start synthetic_window = synth[i.start: i.stop] data_window = data[i.start: i.stop] # Find extrema in the data and the synthetics. data_p, data_t = find_local_extrema(data_window) synth_p, synth_t = find_local_extrema(synthetic_window) window_mask = np.ones(window_npts, dtype="bool") closest_peaks = find_closest(data_p, synth_p) diffs = np.diff(closest_peaks) for idx in np.where(diffs == 1)[0]: if idx > 0: start = synth_p[idx - 1] else: start = 0 if idx < (len(synth_p) - 1): end = synth_p[idx + 1] else: end = -1 window_mask[start: end] = False closest_troughs = find_closest(data_t, synth_t) diffs = np.diff(closest_troughs) for idx in np.where(diffs == 1)[0]: if idx > 0: start = synth_t[idx - 1] else: start = 0 if idx < (len(synth_t) - 1): end = synth_t[idx + 1] else: end = -1 window_mask[start: end] = False window_mask = np.ma.masked_array(window_mask, mask=window_mask) if window_mask.mask.all(): continue for j in flatnotmasked_contiguous(window_mask): final_windows.append((i.start + j.start, i.start + j.stop)) if plot: old_time_windows = time_windows.copy() time_windows.mask[:] = True for start, stop in final_windows: time_windows.mask[start:stop] = False if plot: plt.subplot2grid(grid, (27, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="PEAK AND TROUGH MARCHING ELIMINATION") # Loop through all the time windows, remove windows not satisfying the # minimum number of peaks and troughs per window. Acts mainly as a # safety guard. old_time_windows = time_windows.copy() for i in flatnotmasked_contiguous(old_time_windows): synthetic_window = synth[i.start: i.stop] data_window = data[i.start: i.stop] data_p, data_t = find_local_extrema(data_window) synth_p, synth_t = find_local_extrema(synthetic_window) if np.min([len(synth_p), len(synth_t), len(data_p), len(data_t)]) < \ min_peaks_troughs: time_windows.mask[i.start: i.stop] = True if plot: plt.subplot2grid(grid, (28, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="PEAK/TROUGH COUNT ELIMINATION") # Second minimum window length elimination stage. if plot: old_time_windows = time_windows.copy() min_length = \ min(minimum_period / dt * min_length_period, maximum_period / dt) for i in flatnotmasked_contiguous(time_windows): # Step 7: Throw away all windows with a length of less then # min_length_period the dominant period. if (i.stop - i.start) < min_length: time_windows.mask[i.start: i.stop] = True if plot: plt.subplot2grid(grid, (29, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="MINIMUM WINDOW LENGTH ELIMINATION 2") # Final step, eliminating windows with little energy. final_windows = [] for j in flatnotmasked_contiguous(time_windows): # Again assert a certain minimal length. if (j.stop - j.start) < min_length: continue # Compare the energy in the data window and the synthetic window. data_energy = (data[j.start: j.stop] ** 2).sum() synth_energy = (synth[j.start: j.stop] ** 2).sum() energies = sorted([data_energy, synth_energy]) if energies[1] > max_energy_ratio * energies[0]: if verbose: _log_window_selection( data_trace.id, "Deselecting window due to energy ratio between " "data and synthetics.") continue # Check that amplitudes in the data are above the noise if noise_absolute / data[j.start: j.stop].ptp() > \ max_noise_window: if verbose: _log_window_selection( data_trace.id, "Deselecting window due having no amplitude above the " "signal to noise ratio.") final_windows.append((j.start, j.stop)) if plot: old_time_windows = time_windows.copy() time_windows.mask[:] = True for start, stop in final_windows: time_windows.mask[start:stop] = False if plot: plt.subplot2grid(grid, (30, 0), rowspan=1) _plot_mask(time_windows, old_time_windows, name="LITTLE ENERGY ELIMINATION") if verbose: _log_window_selection( data_trace.id, "Done, Selected %i window(s)" % len(final_windows)) # Final step is to convert the index value windows to actual times. windows = [] for start, stop in final_windows: start = data_starttime + start * data_delta stop = data_starttime + stop * data_delta windows.append((start, stop)) if plot: # Plot the final windows to the data axes. import matplotlib.transforms as mtransforms # NOQA ax = data_plot trans = mtransforms.blended_transform_factory(ax.transData, ax.transAxes) for start, stop in final_windows: ax.fill_between([start * data_delta, stop * data_delta], 0, 1, facecolor="#CDDC39", alpha=0.5, transform=trans) plt.show() return windows
size_t = 100 size_amp = 0.1 stations = [] for filename in glob.glob("./seismic_data/*.HGZ.SAC"): tr = obspy.read(filename, format="SAC")[0] for _ in range(2): tr.detrend("demean") tr.detrend("linear") tr.taper(0.05) tr.integrate() tr_len = len(tr.data) tr_plot, = ax.plot([], [], "r", zorder=100, transform=ccrs.Geodetic()) dist_m, _, _ = calc_vincenty_inverse(evla, evlo, tr.stats.sac.stla, tr.stats.sac.stlo) p_arrival = dist_m / 1000 / vp stations.append({ "name": tr.id, "lat": tr.stats.sac.stla, "lon": tr.stats.sac.stlo, "trace": tr, "length": tr_len, "plot": tr_plot, "amp_scale": size_amp / np.abs(tr.data).max(), "time_scale": size_t / tr_len, "t_offset": p_arrival - tr.stats.sac.a, }) ax.scatter(
def test_calc_vincenty_inverse(self): """ Tests for the Vincenty's Inverse formulae. """ # the following will raise StopIteration exceptions because of two # nearly antipodal points with pytest.raises(StopIteration): calc_vincenty_inverse( 15.26804251, 2.93007342, -14.80522806, -177.2299081, ) with pytest.raises(StopIteration): calc_vincenty_inverse( 27.3562106, 72.2382356, -27.55995499, -107.78571981, ) with pytest.raises(StopIteration): calc_vincenty_inverse( 27.4675551, 17.28133229, -27.65771704, -162.65420626, ) with pytest.raises(StopIteration): calc_vincenty_inverse( 27.4675551, 17.28133229, -27.65771704, -162.65420626, ) # working examples res = calc_vincenty_inverse(0, 0.2, 0, 20) assert round(abs(res[0] - 2204125.9174282863), 7) == 0 assert round(abs(res[1] - 90.0), 7) == 0 assert round(abs(res[2] - 270.0), 7) == 0 res = calc_vincenty_inverse(0, 0, 0, 10) assert round(abs(res[0] - 1113194.9077920639), 7) == 0 assert round(abs(res[1] - 90.0), 7) == 0 assert round(abs(res[2] - 270.0), 7) == 0 res = calc_vincenty_inverse(0, 0, 0, 13) assert round(abs(res[0] - 1447153.3801296828), 7) == 0 assert round(abs(res[1] - 90.0), 7) == 0 assert round(abs(res[2] - 270.0), 7) == 0 res = calc_vincenty_inverse(0, 0, 0, 17) assert round(abs(res[0] - 1892431.3432465086), 7) == 0 assert round(abs(res[1] - 90.0), 7) == 0 assert round(abs(res[2] - 270.0), 7) == 0 # out of bounds with pytest.raises(ValueError): calc_vincenty_inverse(91, 0, 0, 0) with pytest.raises(ValueError): calc_vincenty_inverse(-91, 0, 0, 0) with pytest.raises(ValueError): calc_vincenty_inverse(0, 0, 91, 0) with pytest.raises(ValueError): calc_vincenty_inverse(0, 0, -91, 0)
return fig, axes, selectors # In[10]: dists = {} res = plot_pick_and_find_dist(st, dists, (0, 200)) plt.show() # In[11]: coords = inv.get_coordinates("KO.ELL..BHZ") dist, az, baz = calc_vincenty_inverse(coords["latitude"], coords["longitude"], origin.latitude, origin.longitude) print("Iris çözümü için uzaklık: {:.2f} km".format(dist/1000)) print("Bulunan uzaklık: {:.2f} km".format(dists["KO.ELL..BHZ"])) # In[12]: def plot_find_amps(st, amps, trange=None): if trange is None: trange = [st[0].times()[0], st[0].times()[-1]] nsta = len(st) fig, axes = plt.subplots(nrows=nsta, sharex=True, figsize=(9, 2*nsta)) st_axes = defaultdict(list) for i, tr in enumerate(st): axes[i].plot(tr.times(), tr.data, "k", label=tr.id)