def to_obspy(self, channel_maps, quality_thresholds=None): """ Return obspy Pick and Arrival Arrival is None if there is no pick weight :param channel_maps: {station: ChannelMap object} :param quality_thresholds: list of 4 signal-to-noise ratio thresholds for pick quality, from lowest [quality=3] to highest [quality=0] :parm sampling_rate: sampling rate (used to estimate timing error) """ c_map = channel_maps[self.station] if self.phase_guess == 'P': id = WaveformStreamID(seed_string=c_map.P_write_seed_string) phase_hint = c_map.P_write_phase log(f'{phase_hint}: {id.get_seed_string()}', 'debug') elif self.phase_guess == 'S': id = WaveformStreamID(seed_string=c_map.S_write_seed_string) phase_hint = c_map.S_write_phase log(f'{phase_hint}: {id.get_seed_string()}', 'debug') else: raise ValueError("phase guess '{self.phase_guess}' not 'P' or 'S'") self.weight = self._get_weight(quality_thresholds) time_errors = self._time_error() pick = Pick(time=self.time, time_errors=time_errors, waveform_id=id, phase_hint=phase_hint, evaluation_mode='automatic', evaluation_status='preliminary') if self.weight is not None: return pick, Arrival(pick_id=pick.resource_id, time_weight=self.weight) else: return pick, None
def test_nordic_write(self): """ Test calculating amplitudes """ compare_file = str(self.data_path / "test.nordic") otime = UTCDateTime('2019-05-19T06:09:48') wid = WaveformStreamID(network_code='4G', station_code='STAT') wid.channel_code = 'SHZ' Ppick = Pick(time=otime + 2.2, phase_hint='P', waveform_id=wid, evaluation_mode='automatic', time_errors=QuantityError(0.01)) Parrival = Arrival(pick_id=Ppick.resource_id, time_weight=0) wid.channel_code = 'SH1' Spick = Pick(time=otime + 3.5, phase_hint='S', waveform_id=wid, evaluation_mode='automatic', time_errors=QuantityError(0.05)) wid.channel_code = 'SH2' Sarrival = Arrival(pick_id=Spick.resource_id, time_weight=1) Apick = Pick(time=otime + 4.0, phase_hint='IAML', waveform_id=wid, evaluation_mode='automatic', time_errors=QuantityError(0.1)) Amp = Amplitude(generic_amplitude=410.e-9, type='IAML', unit='m', period=0.22, magnitude_hint='ML', category='period', pick_id=Apick.resource_id, waveform_id=Apick.waveform_id) picks = [Ppick, Spick, Apick] amps = [Amp] arrivals = [Parrival, Sarrival] PSPicker.save_nordic_event(picks, otime, '.', 'test.nordic', amps, arrivals=arrivals, debug=False) self.assertTextFilesEqual('test.nordic', compare_file, ignore_lines=[1]) Path("test.nordic").unlink() for p in Path(".").glob('run_*.log'): p.unlink()
def test_amplitude(self): """ Test calculating amplitudes """ plotit = True datafile = str(self.data_path / "20190519T060917_MONA.mseed") respfile = str(self.data_path / "SPOBS2_resp.json") stream = obspy_read(datafile, 'MSEED') wid = WaveformStreamID(network_code=stream[0].stats.network, station_code=stream[0].stats.station, channel_code=stream[0].stats.channel) Ppick = Pick(time=UTCDateTime('2019-05-19T06:09:48.83'), phase_hint='P', waveform_id=wid) Spick = Pick(time=UTCDateTime('2019-05-19T06:09:51.52'), phase_hint='S', waveform_id=wid) la = LocalAmplitude(stream, [Ppick, Spick], respfile, 'JSON_PZ') wood_calc, _ = la.get_iaml(method='wood_calc') wood_est, _ = la.get_iaml(method='wood_est') raw, _ = la.get_iaml(method='raw_disp') # Values obtained when response was mis-interpreted as nm/s # self.assertAlmostEqual(amp_wood_calc.generic_amplitude, 1097.55509106/1e9) # self.assertAlmostEqual(amp_wood_calc.period, 0.112) self.assertAlmostEqual(wood_calc.generic_amplitude * 1e9, 378.3697813) self.assertAlmostEqual(wood_calc.period, 0.12) self.assertAlmostEqual(wood_est.generic_amplitude * 1e9, 140.490154756) self.assertAlmostEqual(wood_est.period, 0.032) self.assertAlmostEqual(raw.generic_amplitude * 1e9, 460.35018097) self.assertAlmostEqual(raw.period, 0.112)
def make_pick(pick_str, origin_time): """ Creates an ObsPy Pick object from a line of STP phase output. Sample pick_str: CI CLC HHZ -- 35.8157 -117.5975 775.0 P c. i 1.0 6.46 1.543 """ fields = pick_str.split() if len(fields) != 13: raise Exception('Invalid STP phase output') new_pick = Pick() (net, sta, chan, loc) = fields[:4] new_pick.waveform_id = WaveformStreamID(network_code=net, station_code=sta, channel_code=chan, location_code=loc) # Determine polarity from first motion. polarity = POLARITY_MAPPING[fields[8][0]] if polarity == '': polarity = POLARITY_MAPPING[fields[8][1]] if polarity != '': new_pick.polarity = polarity # Determine signal onset. if fields[9] == 'i': new_pick.onset = 'impulsive' elif fields[9] == 'e': new_pick.onset = 'emergent' else: new_pick.onset = 'questionable' # Determine time error from STP quality. # Use Jiggle standard and assume sample rate of 100 sps. quality = float(fields[10]) if quality == 0.0: new_pick.time_errors = QuantityError(lower_uncertainty=0.03) elif quality <= 0.3: new_pick.time_errors = QuantityError(upper_uncertainty=0.03) elif quality <= 0.5: new_pick.time_errors = QuantityError(upper_uncertainty=0.02) elif quality <= 0.8: new_pick.time_errors = QuantityError(upper_uncertainty=0.01) elif quality == 1.0: new_pick.time_errors = QuantityError(upper_uncertainty=0.0) # Determine pick time. offset = float(fields[12]) new_pick.time = origin_time + offset return new_pick
def get_picks_from_pdf(trace, height=0.5, distance=100): start_time = trace.stats.starttime peaks, properties = find_peaks(trace.pdf, height=height, distance=distance) picks = [] for p in peaks: if p: time = start_time + p / trace.stats.sampling_rate phase_hint = "P" pick = Pick(time=time, phase_hint=phase_hint) pick.waveform_id = WaveformStreamID( network_code=trace.stats.network, station_code=trace.stats.station, location_code=trace.stats.channel, channel_code=trace.stats.location) picks.append(pick) return picks
def test_amplitude(self): """ Test calculating amplitudes """ datafile = os.path.join(self.testing_path, "20190519T060917_MONA.mseed") respfile = os.path.join(self.testing_path, "SPOBS2_response.json") stream = obspy_read(datafile, 'MSEED') wid = WaveformStreamID(network_code=stream[0].stats.network, station_code=stream[0].stats.station, channel_code=stream[0].stats.channel) Ppick = Pick(time=UTCDateTime('2019-05-19T06:09:48.83'), phase_hint='P', waveform_id=wid) Spick = Pick(time=UTCDateTime('2019-05-19T06:09:51.52'), phase_hint='S', waveform_id=wid) obj = LocalAmplitude(stream, [Ppick, Spick], respfile, 'JSON_PZ') amp_wood_calc, _ = obj.get_iaml(plot=False, method='wood_calc') amp_wood_est, _ = obj.get_iaml(plot=False, method='wood_est') amp_raw, _ = obj.get_iaml(plot=False, method='raw_disp') self.assertAlmostEqual(amp_wood_calc.generic_amplitude, 1097.555091056036) self.assertAlmostEqual(amp_wood_calc.period, 0.112)
def to_waveformstreamID(self): """Returns an obspy waveformID""" return WaveformStreamID(station_code=self.station, channel_code=self.channel, network_code=self.network, location_code=self.location)
def main(st, fname, verbose=False): fs = st[0].stats.sampling_rate # Detect STA/LTA for all geodes, with minimum number of stations included proc1 = time.time() detection_list, cft_stream = network_detection(st, cft_return=True) proc2 = time.time() Logger.info("Network detection search done in %f s." % (proc2 - proc1)) Logger.info("Number of network detections = %d" % len(detection_list)) # Get picks and stats, iterating detection by detection, then station by station # Buffer window before and after detection buffer1 = 3.0 # 0.2 buffer2 = 10.0 # Load ERT data ert_surveys_file = "survey_times_ERT.csv" dateparse = lambda x: pd.datetime.strptime(x, '%Y-%m-%d %H:%M:%S') ert_surveys = pd.read_csv(ert_surveys_file, parse_dates=["time_local_start"], date_parser=dateparse) ert_surveys["time_local_start"] = ert_surveys[ "time_local_start"].dt.tz_localize("America/Edmonton", ambiguous="infer") ert_surveys["time_utc_start"] = ert_surveys[ "time_local_start"].dt.tz_convert(None) ert_surveys["time_utc_end"] = ert_surveys["time_utc_start"] + pd.Timedelta( 25, unit="m") ert_surveys["time_utc_end"] = pd.to_datetime(ert_surveys["time_utc_end"]) ert_surveys["time_utc_start"] = pd.to_datetime( ert_surveys["time_utc_start"]) catalog = Catalog() # Loop over each STA/LTA detection for detection in detection_list: # Skip if detection happens during ERT survey tmin = detection["time"]._get_datetime() is_ert_on = \ ert_surveys.loc[(ert_surveys['time_utc_start'] <= tmin) & (ert_surveys['time_utc_end'] >= tmin)].shape[ 0] > 0 if is_ert_on: Logger.warning("Skip false detection during ERT survey.") continue Logger.info("DETECTION TIME: %s\n\t DURATION_SEC: %f" % (detection["time"], detection["duration"])) det_start = detection["time"] det_end = detection["time"] + detection["duration"] # Detection stream det_st = st.slice(starttime=det_start - buffer1, endtime=det_end + buffer2) det_st.detrend() det_st_to_save = det_st.copy() t_plt = det_st[0].times("matplotlib") t_utc = det_st[0].times("utcdatetime") det_cft = cft_stream.slice(starttime=det_start - buffer1, endtime=det_end + buffer2) # Stations in detection stream station_list = list(set(detection["stations"])) station_list.sort() # Check if frequencies within window are anomalous highf_ratio_threshold = 0.6 for station in station_list: tmp = det_st.select(station=station).copy() nbad = 0 for tr in tmp: ratio = highf_ratio(data=tr.data, sampling_rate=fs) if ratio > highf_ratio_threshold: nbad += 1 if nbad > 0: for tr in tmp: Logger.warning( "Removing station %s because for %d traces, ratio of frequencies above %f is above %f" % (station, nbad, 0.25 * fs, highf_ratio_threshold)) det_st.remove(tr) # Stations in detection stream station_list = list(set(detection["stations"])) station_list.sort() if len(station_list) < 4: Logger.warning( "Only %d stations left, less than 4, so skipping this detection" % len(station_list)) # Search window for phase around STA/LTA detection time idet_start = (np.abs(t_utc - det_start)).argmin() idet_end = (np.abs(t_utc - det_end)).argmin() idx_search_max = range(idet_start, idet_end) # Analyze stations one by one pol_st = Stream() event_phases = [] for ista, station in enumerate(station_list): # Select waveform and STA-LTA streams sta_st = det_st.select(station=station).copy() network = sta_st[0].stats.network sta_st.detrend() sta_cft = det_cft.select(station=station).copy() sta_cft_stack = (sta_cft.select(channel="DPZ")[0].data + sta_cft.select(channel="DPN")[0].data + sta_cft.select(channel="DPE")[0].data) / 3 # Polarization properties tpol, pol_dict, pol_st_sta = modified_polarization_analysis( sta_st, dominant_period=DOM_PERIOD, interpolate=True) pol_st += pol_st_sta # Energy response curve for pick detection per = peak_eigenvalue_ratio(pol_dict["eigenvalue1"], win_len=int(2 * DOM_PERIOD * fs)) per = eps_smooth(per, w=int(EPS_WINLEN * fs)) jer = joint_energy_ratio(sta_cft_stack, t_plt, per, tpol) # Extract phases sta_phases = get_phases(response_curve=jer, idx_search_max=idx_search_max, time=t_utc, pol=pol_dict, verbose=False) if sta_phases: # Now do some quality control snr_threshold = 2.5 win_len_s = 0.2 sta_phases["station"] = station sta_phases["network"] = network if sta_phases["P"]["arrival_time"]: arr_time = sta_phases["P"]["arrival_time"] - 0.02 snr, channel = get_snr_phase(sta_st, time=arr_time, win_len_s=win_len_s, verbose=False, tnoise=None) Logger.info("SNR for P pick %s.%s..%s: %f \t at t = %s" % (network, station, channel, snr, arr_time)) if snr < snr_threshold: #Logger.info("P pick below SNR threshold of %f" % snr_threshold) sta_phases["P"]["arrival_time"] = None else: sta_phases["P"]["SNR"] = snr sta_phases["P"]["channel"] = channel if sta_phases["S"]["arrival_time"]: arr_time = sta_phases["S"]["arrival_time"] - 0.02 if sta_phases["P"]["arrival_time"]: tnoise = sta_phases["P"]["arrival_time"] - 0.02 else: tnoise = None snr, channel = get_snr_phase(sta_st.select(), time=arr_time, win_len_s=win_len_s, verbose=False, tnoise=tnoise) Logger.info("SNR for S pick %s.%s..%s: %f \t at t = %s" % (network, station, channel, snr, arr_time)) if snr < snr_threshold: Logger.info("S pick below SNR threshold of %f" % snr_threshold) sta_phases["S"]["arrival_time"] = None else: sta_phases["S"]["SNR"] = snr sta_phases["S"]["channel"] = channel Logger.info("Station %s: t_P = %s\tt_S = %s" % (station, sta_phases["P"]["arrival_time"], sta_phases["S"]["arrival_time"])) event_phases.append(sta_phases) else: Logger.info("No phase found for station %s" % station) # End of for loop over stations if not event_phases: Logger.info("No picks found at all for this detection.") continue else: nump = len([p for p in event_phases if p["P"]["arrival_time"]]) nums = len([p for p in event_phases if p["S"]["arrival_time"]]) Logger.info("Number of initial picks before MCCC: P = %d, S = %d" % (nump, nums)) if nump + nums == 0: Logger.info("No picks found at all for this detection.") continue # if verbose: # plot_phases(event_phases, det_st) # wadati_plot(event_phases, det_st) # Align with mccc Logger.info("Refining picks with MCCC") event_phases = align_mccc(event_phases=event_phases, stream=det_st, verbose=False) nump = len([p for p in event_phases if p["P"]["arrival_time"]]) nums = len([p for p in event_phases if p["S"]["arrival_time"]]) if nump == 0 and nums == 0: Logger.warning("No remaining picks after MCCC!") continue elif nump + nums < 5: Logger.info("Less than 5 picks remaining. Skipping event.") continue if verbose: Logger.info("Number of picks after MCCC: P = %d, S = %d" % (nump, nums)) wadati_plot(event_phases, det_st) plot_phases(event_phases, det_st) # Update polarization statistics Logger.info("Updating polarization attributes") phase_len_tol = int(10 * DOM_PERIOD * fs) for i, staph in enumerate(event_phases): sta_st = det_st.select(station=staph["station"]).copy() t = sta_st[0].times("utcdatetime") tpol, pol_dict, _ = modified_polarization_analysis( sta_st, dominant_period=DOM_PERIOD, interpolate=True) tp = staph["P"]["arrival_time"] if tp: idxP = np.argmin(np.abs(t - tp)) stats = pol_window_stats(pol_dict, idxP, phase_len_tol, show_stats=False) event_phases[i]["P"]["pol_stats"] = stats ts = staph["S"]["arrival_time"] if ts: idxS = np.argmin(np.abs(t - ts)) stats = pol_window_stats(pol_dict, idxS, phase_len_tol, show_stats=False) event_phases[i]["S"]["pol_stats"] = stats # Convert to obspy Picks and Event event_picks = [] for i, staph in enumerate(event_phases): event_picks += sta_phases_to_pick(staph=staph) event = Event(picks=event_picks) # Estimate average event distance using availables pairs of P and S picks r_med = distance_from_tstp(event.picks, min_estim=1) if not r_med: # We cannot estimate r, hence magnitude Logger.warning( "Couldn't estimate hypocentral distance from ts-tp. No magnitude calculation." ) # Add event to catalog if verbose: Logger.info( "Adding event to catalog: *******************************************" ) Logger.info(event) catalog.events.append(event) stfilepath = os.path.join("detections_waveforms", det_start.strftime("%Y%m%d")) if not os.path.exists(stfilepath): os.mkdir(stfilepath) det_st_to_save.write(os.path.join( stfilepath, "bhdetect_%s.mseed" % det_start.strftime("%Y%m%d%H%M%S")), format="MSEED") continue # Calculate magnitudes Logger.info("Computing magnitudes...") magtime_contriblist = [] magspec_contriblist = [] for ista, station in enumerate(station_list): sta_picks = [ p for p in event.picks if p.waveform_id.station_code == station ] r = distance_from_tstp(sta_picks, min_estim=2) if not r: r = r_med ts = get_pick(event.picks, station, "S") if not ts: # No ts pick Logger.warning("There is no S pick for station %s." % station) continue sta_st = det_st.select(station=station).copy() sta_st.detrend() # Estimate coda tp = get_pick(event.picks, station, "P") if not tp: tsig = ts - 0.5 else: tsig = tp - 0.02 tcoda, s_len, snr = get_coda_duration(sta_st.copy(), tsig=tsig, ts=ts, win_len_s=0.2) if not tcoda: if verbose: Logger.info( "Couldn't calculate coda duration for station %s skipping..." % station) continue # Save coda info amp = Amplitude(generic_amplitude=tcoda, snr=snr, type="END", category="duration", unit="s", magnitude_hint="Md") event.amplitudes.append(amp) # Estimate energy flux if tp: Logger.info("Calculating energy flux fr station %s" % station) epsilonS = 0 for tr in sta_st.copy(): tr_cut = tr.trim(starttime=ts, endtime=ts + (ts - tp)).data cumsum_u2 = scipy.integrate.cumtrapz(tr_cut**2, dx=tr.stats.delta) epsilonS += cumsum_u2[-1] amp = Amplitude(generic_amplitude=epsilonS, snr=snr, type="A", category="integral", unit="other", time_window=TimeWindow(begin=ts - tp, end=2 * (ts - tp), reference=tp), waveform_id=WaveformStreamID( network_code=tr.stats.network, station_code=tr.stats.station)) event.amplitudes.append(amp) # Estimate Mw for each component Mw_spec_sta = [] Mw_time_sta = [] Q_spec_sta = [] fc_spec_sta = [] for tr in sta_st: # Cut noise window and S waveform noise_len = s_len taper_perc = 0.1 trnoise = tr.copy() trnoise.trim(starttime=tsig - (1 + taper_perc) * noise_len, endtime=tsig - taper_perc * noise_len) trnoise.taper(type="hann", max_percentage=taper_perc, side="both") tr.trim(starttime=ts - taper_perc * s_len, endtime=ts + (1 + taper_perc) * s_len) tr.taper(type="hann", max_percentage=taper_perc, side="both") # Check SNR snr_trace = np.median(tr.slice(starttime=ts, endtime=ts + s_len).data) / \ np.median(trnoise.data) if snr_trace < 3: Logger.info( "SNR < 3, skipping trace for magnitude calculation.") # Poor SNR, skip trace continue # Displacement waveform trdisp = tr.copy() trdisp.integrate() trdisp.detrend() # Estimate magnitude: time method Mw_time, M0_time, omega0_time = estimate_magnitude_time( trdisp, r, disp=False) Mw_time_sta.append(Mw_time) # Estimate magnitude: spectral method Mw_o, M0_o, omega0_o, fc_o, Q_o = estimate_magnitude_spectral( trdisp, r, omega0_time, trnoise=None, disp=False) if not Mw_o: Logger.warning("No magnitude found due to errors.") continue elif fc_o < 2 or Q_o > 40 or Q_o < 1: # Qs Attenuation larger than Sandstone=31, shale=10 # Reject spectral estimate Logger.warning( "Rejecting spectral estimate with: fc = %f, Q = %f" % (fc_o, Q_o)) continue else: Mw_spec_sta.append(Mw_o) Q_spec_sta.append(Q_o) fc_spec_sta.append(fc_o) # Now get average for station as a whole Logger.info( "Found %d estimates of Mw using time method for station %s." % (len(Mw_time_sta), station)) Logger.info( "Found %d estimates of Mw using spectral method for station %s." % (len(Mw_spec_sta), station)) if Mw_time_sta: smagt = StationMagnitude( mag=np.mean(Mw_time_sta), mag_errors=QuantityError(uncertainty=np.std(Mw_time_sta)), station_magnitude_type="Mw_time", comments=[Comment(text="snr = %f" % snr)]) event.station_magnitudes.append(smagt) contrib = StationMagnitudeContribution( station_magnitude_id=smagt.resource_id, weight=snr) magtime_contriblist.append(contrib) Logger.info("Magnitude time estimate = %f" % np.mean(Mw_time_sta)) if Mw_spec_sta: smags = StationMagnitude( mag=np.mean(Mw_spec_sta), mag_errors=QuantityError(uncertainty=np.std(Mw_spec_sta)), station_magnitude_type="Mw_spectral", comments=[ Comment(text="Q_mean = %f, Q_std = %f" % (np.mean(Q_spec_sta), np.std(Q_spec_sta))), Comment(text="Fc_mean = %f, Fc_std = %f" % (np.mean(fc_spec_sta), np.std(fc_spec_sta))), Comment(text="snr = %f" % snr) ]) event.station_magnitudes.append(smags) contrib = StationMagnitudeContribution( station_magnitude_id=smags.resource_id, weight=snr) magspec_contriblist.append(contrib) Logger.info("Magnitude spectral estimate = %f" % np.mean(Mw_spec_sta)) Logger.info("Fc = %f, Q = %f" % (np.mean(fc_spec_sta), np.mean(Q_spec_sta))) # End of for loop over stations # Get magnitude for event if magspec_contriblist: Logger.info( "Found %d station estimates of Mw using spectral method." % len(magspec_contriblist)) wave_num = 0 wave_den = 0 val_list = [] for m in magspec_contriblist: mval = [ sm.mag for sm in event.station_magnitudes if sm.resource_id == m.station_magnitude_id ][0] wave_num += mval * m.weight wave_den += m.weight val_list.append(mval) mag = wave_num / wave_den mags = Magnitude( mag=mag, mag_errors=np.std(val_list), magnitude_type="Mw_spectral", station_count=len(magspec_contriblist), station_magnitude_contributions=magspec_contriblist) event.magnitudes.append(mags) Logger.info( "Event magnitude estimate using spectral method: Mw = %f" % mags.mag) if magtime_contriblist: Logger.info("Found %d station estimates of Mw using time method." % len(magtime_contriblist)) wave_num = 0 wave_den = 0 val_list = [] for m in magtime_contriblist: mval = [ sm.mag for sm in event.station_magnitudes if sm.resource_id == m.station_magnitude_id ][0] wave_num += mval * m.weight wave_den += m.weight val_list.append(mval) mag = wave_num / wave_den magt = Magnitude( mag=mag, mag_errors=np.std(val_list), magnitude_type="Mw_time", station_count=len(magtime_contriblist), station_magnitude_contributions=magtime_contriblist) event.magnitudes.append(magt) Logger.info("Event magnitude estimate using time method: Mw = %f" % magt.mag) # Add event to catalog if verbose: Logger.info( "Adding event to catalog: *******************************************" ) Logger.info(event) catalog.events.append(event) stfilepath = os.path.join("detections_waveforms", det_start.strftime("%Y%m%d")) if not os.path.exists(stfilepath): os.mkdir(stfilepath) det_st_to_save.write(os.path.join( stfilepath, "bhdetect_%s.mseed" % det_start.strftime("%Y%m%d%H%M%S")), format="MSEED") if len(catalog) > 0: # Decluster declustered_catalog = decluster_bh(catalog, trig_int=2.0) if not os.path.exists(os.path.split(fname)[0]): os.mkdir(os.path.split(fname)[0]) declustered_catalog.write(fname, format="QUAKEML")