def reject_on_prominence_of_central_peak(self): """ Equivalent to reject_on_prominence() in the original flexwin code. """ # The fine tuning constant is often set to 0. Nothing to do in this # case as all windows will then pass the criteria by definition. if not self.config.c_2: return self.windows def filter_windows_maximum_prominence(win): smaller_troughs = self.troughs[self.troughs < win.center] larger_troughs = self.troughs[self.troughs > win.center] if not len(smaller_troughs) or not len(larger_troughs): return False left = self.stalta[smaller_troughs[-1]] right = self.stalta[larger_troughs[0]] center = self.stalta[win.center] delta_left = center - left delta_right = center - right if (delta_left < self.config.c_2 * center) or \ (delta_right < self.config.c_2 * center): return False return True windows = list(filter(filter_windows_maximum_prominence, self.windows)) self.separate_rejects(windows, "prominence") logger.info("Prominence of central peak rejection retained " "%i windows." % len(self.windows))
def initial_window_selection(self): """ Find all possible windows. This is equivalent to the setup_M_L_R() function in flexwin. """ for peak in self.peaks: # only continue if there are available minima on either side if peak <= self.troughs[0] or peak >= self.troughs[-1]: continue # only continue if this maximum is above the water level if self.stalta[peak] <= self.config.stalta_waterlevel[peak]: continue smaller_troughs = self.troughs[self.troughs < peak] larger_troughs = self.troughs[self.troughs > peak] for left, right in itertools.product(smaller_troughs, larger_troughs): self.windows.append( Window(left=left, right=right, center=peak, channel_id=self.observed.id, time_of_first_sample=self.synthetic.stats.starttime, dt=self.observed.stats.delta, min_period=self.config.min_period, weight_function=self.config.window_weight_fct)) logger.info("Initial window selection yielded %i possible windows." % len(self.windows))
def schedule_weighted_intervals(self): """ Run the weighted interval scheduling. """ self.windows = schedule_weighted_intervals(self.windows) logger.info("Weighted interval schedule optimization retained %i " "windows." % len(self.windows))
def reject_windows_based_on_minimum_length(self): """ Reject windows smaller than the minimal window length. """ windows = list( filter(lambda x: (x.right - x.left) >= self.minimum_window_length, self.windows)) self.separate_rejects(windows, "min_length") logger.info("Rejection based on minimum window length retained %i " "windows." % len(self.windows))
def calculate_ttimes(self): """ Calculate theoretical travel times. Only call if station and event information is available! """ dist_in_deg = obspy.geodetics.locations2degrees( self.station.latitude, self.station.longitude, self.event.latitude, self.event.longitude) tts = self.taupy_model.get_travel_times( source_depth_in_km=self.event.depth_in_m / 1000.0, distance_in_degree=dist_in_deg) self.ttimes = [{"time": _i.time, "name": _i.name} for _i in tts] logger.info("Calculated travel times.")
def reject_based_on_data_fit_criteria(self): """ Rejects windows based on similarity between data and synthetics. """ # First calculate the criteria for all remaining windows. for win in self.windows: win._calc_criteria(self.observed.data, self.synthetic.data) def reject_based_on_time_shift(win): tshift_min = self.config.tshift_reference - \ self.config.tshift_acceptance_level[win.center] tshift_max = self.config.tshift_reference + \ self.config.tshift_acceptance_level[win.center] if not (tshift_min < win.cc_shift * self.observed.stats.delta < tshift_max): logger.debug("Window rejected due to time shift: %f" % win.cc_shift) return False return True def reject_based_on_dlna(win): dlnA_min = self.config.dlna_reference - \ self.config.dlna_acceptance_level[win.center] dlnA_max = self.config.dlna_reference + \ self.config.dlna_acceptance_level[win.center] if not (dlnA_min < win.dlnA < dlnA_max): logger.debug("Window rejected due to amplitude fit: %f" % win.dlnA) return False return True def reject_based_on_cc_value(win): if win.max_cc_value < self.config.cc_acceptance_level[win.center]: logger.debug("Window rejected due to CC value: %f" % win.max_cc_value) return False return True for func, tag in zip([ reject_based_on_time_shift, reject_based_on_dlna, reject_based_on_cc_value ], ["tshift", "dlna", "cc"]): windows = list(filter(func, self.windows)) self.separate_rejects(windows, tag) logger.info( "Rejection based on data fit criteria retained %i windows." % len(self.windows))
def calculate_preliminiaries(self): """ Calculates the envelope, STA/LTA and the finds the local extrema. """ logger.info("Calculating envelope of synthetics.") self.synthetic_envelope = envelope(self.synthetic.data) logger.info("Calculating STA/LTA.") self.stalta = sta_lta(self.synthetic_envelope, self.observed.stats.delta, self.config.min_period) self.peaks, self.troughs = utils.find_local_extrema(self.stalta) if not len(self.peaks) and len(self.troughs): return if self.ttimes: offset = self.event.origin_time - self.observed.stats.starttime min_time = self.ttimes[0]["time"] - \ self.config.max_time_before_first_arrival + offset min_idx = int(min_time / self.observed.stats.delta) dist_in_km = obspy.geodetics.calc_vincenty_inverse( self.station.latitude, self.station.longitude, self.event.latitude, self.event.longitude)[0] / 1000.0 max_time = dist_in_km / self.config.min_surface_wave_velocity + \ offset + self.config.max_period max_idx = int(max_time / self.observed.stats.delta) # Reject all peaks and troughs before the minimal allowed start # time and after the maximum allowed end time. first_trough, last_trough = self.troughs[0], self.troughs[-1] self.troughs = self.troughs[(self.troughs >= min_idx) & (self.troughs <= max_idx)] # If troughs have been removed, readd them add the boundaries. if len(self.troughs): if first_trough != self.troughs[0]: self.troughs = np.concatenate([ np.array([min_idx], dtype=self.troughs.dtype), self.troughs ]) if last_trough != self.troughs[-1]: self.troughs = np.concatenate([ self.troughs, np.array([max_idx], dtype=self.troughs.dtype) ]) # Make sure peaks are inside the troughs! min_trough, max_trough = self.troughs[0], self.troughs[-1] self.peaks = self.peaks[(self.peaks > min_trough) & (self.peaks < max_trough)]
def reject_based_on_signal_to_noise_ratio(self): """ Rejects windows based on their signal to noise amplitude ratio. """ if self.config.noise_end_index is None: logger.warning("Cannot reject windows based on their signal to " "noise ratio. Please give station and event " "information or information about the temporal " "range of the noise.") return noise = self.observed.data[self.config.noise_start_index:self.config. noise_end_index] # Very short source-receiver distances can sometimes produce 0 length # noise signals if not noise: logger.warning("pre-arrival noise could not be determined, " "skipping rejection based on signal-to-noise ratio") return elif self.config.window_signal_to_noise_type == "amplitude": noise_amp = np.abs(noise).max() def filter_window_noise(win): win_signal = self.observed.data[win.left:win.right] win_noise_amp = np.abs(win_signal).max() / noise_amp if win_noise_amp < self.config.s2n_limit[win.center]: return False return True elif self.config.window_signal_to_noise_type == "energy": noise_energy = np.sum(noise**2) / len(noise) def filter_window_noise(win): data = self.observed.data[win.left:win.right] win_energy = np.sum(data**2) / len(data) win_noise_amp = win_energy / noise_energy if win_noise_amp < self.config.s2n_limit[win.center]: return False return True else: raise NotImplementedError windows = list(filter(filter_window_noise, self.windows)) self.separate_rejects(windows, "s2n") logger.info("SN amplitude ratio window rejection retained %i windows" % len(self.windows))
def reject_on_minima_water_level(self): """ Filter function rejecting windows whose internal minima are below the water level of the windows peak. This is equivalent to the reject_on_water_level() function in flexwin. """ def filter_window_minima(win): waterlevel_midpoint = \ self.config.c_0 * self.config.stalta_waterlevel[win.center] internal_minima = win._get_internal_indices(self.troughs) return not np.any( self.stalta[internal_minima] <= waterlevel_midpoint) windows = list(filter(filter_window_minima, self.windows)) self.separate_rejects(windows, "water_level") logger.info("Water level rejection retained %i windows" % len(self.windows))
def check_data_quality(self): """ Checks the data quality by estimating signal to noise ratios. """ if self.config.noise_end_index is None: raise PyflexError( "Cannot check data quality as the noise end index is not " "given and station and/or event information is not " "available so the theoretical arrival times cannot be " "calculated.") noise = self.observed.data[self.config.noise_start_index:self.config. noise_end_index] signal = self.observed.data[self.config.signal_start_index:self.config. signal_end_index] noise_int = np.sum(noise**2) / len(noise) noise_amp = np.abs(noise).max() signal_int = np.sum(signal**2) / len(signal) signal_amp = np.abs(signal).max() # Calculate ratios. snr_int = signal_int / noise_int snr_amp = signal_amp / noise_amp if snr_int < self.config.snr_integrate_base: msg = ("Whole waveform rejected as the integrated signal to " "noise ratio (%f) is above the threshold (%f)." % (snr_int, self.config.snr_integrate_base)) logger.warn(msg) warnings.warn(msg, PyflexWarning) return False if snr_amp < self.config.snr_max_base: msg = ("Whole waveform rejected as the signal to noise amplitude " "ratio (%f) is above the threshold (%f)." % (snr_amp, self.config.snr_max_base)) logger.warn(msg) warnings.warn(msg, PyflexWarning) return False logger.info("Global SNR checks passed. Integrated SNR: %f, Amplitude " "SNR: %f" % (snr_int, snr_amp)) return True
def reject_on_phase_separation(self): """ Reject windows based on phase seperation. Equivalent to reject_on_phase_separation() in the original flexwin code. """ def filter_phase_rejection(win): # Find the lowest minimum within the window. internal_minima = self.troughs[(self.troughs >= win.left) & (self.troughs <= win.right)] stalta_min = self.stalta[internal_minima].min() # find the height of the central maximum above this minimum value d_stalta_center = self.stalta[win.center] - stalta_min # Find all internal maxima. internal_maxima = self.peaks[(self.peaks >= win.left) & (self.peaks <= win.right) & (self.peaks != win.center)] for max_index in internal_maxima: # find height of current maximum above lowest minimum d_stalta = self.stalta[max_index] - stalta_min # find scaled time between current maximum and central maximum d_time = abs(win.center - max_index) * \ self.observed.stats.delta / self.config.min_period # find value of time decay function. # The paper has a square root in the numinator of the # exponent as well. Not the case here as it is not the case # in the original flexwin code. if (d_time >= self.config.c_3b): f_time = np.exp(-( (d_time - self.config.c_3b) / self.config.c_3b)**2) else: f_time = 1.0 # check condition if d_stalta > (self.config.c_3a * d_stalta_center * f_time): break else: return True return False windows = list(filter(filter_phase_rejection, self.windows)) self.separate_rejects(windows, "phase_sep") logger.info("Single phase group rejection retained %i windows" % len(self.windows))
def remove_duplicates(self): """ Filter to remove duplicate windows based on left and right bounds. This function will also change the middle to actually be in the center of the window. This should result in better results for the following stages as lots of thresholds are evaluated at the center of a window. """ new_windows = {} for window in self.windows: tag = (window.left, window.right) if tag not in new_windows: window.center = \ int(window.left + (window.right - window.left) / 2.0) new_windows[tag] = window self.windows = sorted(new_windows.values(), key=lambda x: x.left) logger.info("Removing duplicates retains %i windows." % len(self.windows))
def merge_windows(self): """ Merge overlapping windows. Will also recalculate the data fit criteria. """ # Sort by starttime. self.windows = sorted(self.windows, key=lambda x: x.left) windows = [self.windows.pop(0)] for right_win in self.windows: left_win = windows[-1] if (left_win.right + 1) < right_win.left: windows.append(right_win) continue left_win.right = right_win.right self.windows = windows for win in self.windows: # Recenter windows win.center = int(win.left + (win.right - win.left) / 2.0) # Recalculate criteria. win._calc_criteria(self.observed.data, self.synthetic.data) logger.info("Merging windows resulted in %i windows." % len(self.windows))
def curtail_window_length(win): time_decay_left = self.config.min_period * self.config.c_4a / dt time_decay_right = self.config.min_period * self.config.c_4b / dt # Find all internal maxima. internal_maxima = self.peaks[(self.peaks >= win.left) & (self.peaks <= win.right) & (self.peaks != win.center)] if len(internal_maxima) < 2: return win i_left = internal_maxima[0] i_right = internal_maxima[-1] delta_left = i_left - win.left delta_right = win.right - i_right # check condition if delta_left > time_decay_left: logger.info("Curtailing left") win.left = int(i_left - time_decay_left) if delta_right > time_decay_right: logger.info("Curtailing right") win.right = int(i_right + time_decay_right) return win
def reject_on_traveltimes(self): """ Reject based on traveltimes. Will reject windows containing only data before a minimum period before the first arrival and windows only containing data after the minimum allowed surface wave speed. Only call if station and event information is available! """ dist_in_km = obspy.geodetics.calc_vincenty_inverse( self.station.latitude, self.station.longitude, self.event.latitude, self.event.longitude)[0] / 1000.0 offset = self.event.origin_time - self.observed.stats.starttime min_time = self.ttimes[0]["time"] - self.config.min_period + offset max_time = dist_in_km / self.config.min_surface_wave_velocity + offset windows = [ win for win in self.windows if (win.relative_endtime >= min_time) and ( win.relative_starttime <= max_time) ] self.separate_rejects(windows, "traveltimes") logger.info("Rejection based on travel times retained %i windows." % len(self.windows))
def _parse_event_and_station(self): """ Parse the event and station information. """ # Parse the event. if self.event and not isinstance(self.event, Event): # It might be an ObsPy event catalog. if isinstance(self.event, obspy.core.event.Catalog): if len(self.event) != 1: raise PyflexError("The event catalog must contain " "exactly one event.") self.event = self.event[0] # It might be an ObsPy event object. if isinstance(self.event, obspy.core.event.Event): if not self.event.origins: raise PyflexError("Event does not contain an origin.") origin = self.event.preferred_origin() or self.event.origins[0] self.event = Event(latitude=float(origin.latitude), longitude=float(origin.longitude), depth_in_m=float(origin.depth), origin_time=origin.time) else: raise PyflexError("Could not parse the event. Unknown type.") # Parse the station information if it is an obspy inventory object. if isinstance(self.station, obspy.core.inventory.Inventory): net = self.observed.stats.network sta = self.observed.stats.station # Workaround for ObsPy 0.9.2 Newer version have a get # coordinates method... for network in self.station: if network.code == net: break else: raise PyflexError("Could not find the network of the " "observed data in the inventory object.") for station in network: if station.code == sta: break else: raise PyflexError("Could not find the station of the " "observed data in the inventory object.") self.station = Station(latitude=float(station.latitude), longitude=float(station.longitude)) # Last resort, if either is not set, and the observed or synthetics # are sac files, get the information from there. if not self.station or not self.event: if hasattr(self.observed.stats, "sac"): tr = self.observed ftype = "observed" elif hasattr(self.synthetic.stats, "sac"): tr = self.synthetic ftype = "synthetic" else: return sac = tr.stats.sac values = (sac.evla, sac.evlo, sac.evdp, sac.stla, sac.stlo, sac.b) # Invalid value in sac. if -12345.0 in values: return if not self.station: self.station = Station(latitude=values[3], longitude=values[4]) logger.info("Extracted station information from %s SAC file." % ftype) if not self.event: self.event = Event(latitude=values[0], longitude=values[1], depth_in_m=values[2] * 1000.0, origin_time=self.observed.stats.starttime - values[5]) logger.info("Extracted event information from %s SAC file." % ftype)