def _get_pulse_width_and_area(tr, ipick, icross, max_pulse_duration=.08): """ Measure the width & area of the arrival pulse on the displacement trace Start from the displacement peak index (=icross - location of first zero crossing of velocity) :param tr: displacement trace :type tr: obspy.core.trace.Trace or uquake.core.Trace :param ipick: index of pick in trace :type ipick: int :param icross: index of first zero crossing in corresponding velocity trace :type icross: int :param max_pulse_duration: max allowed duration (sec) beyond pick to search for zero crossing of disp pulse :type max_pulse_duration: float return pulse_width, pulse_area :returns: pulse_width, pulse_area: Returns the width and area of the displacement pulse :rtype: float, float """ fname = '_get_pulse_width_and_area' data = tr.data sign = np.sign(data) nmax = int(max_pulse_duration * tr.stats.sampling_rate) iend = ipick + nmax epsilon = 1e-10 if icross >= iend: i = iend - 1 for i in range(icross, iend): diff = np.abs(data[i] - data[ipick]) if diff < epsilon or sign[i] != sign[icross]: break if i == iend - 1: logger.info("%s: Unable to locate termination of displacement " "pulse for tr:%s!" % (fname, tr.get_id())) return 0, 0 istop = i pulse_width = float(istop - ipick) * tr.stats.delta pulse_area = np.trapz(data[ipick:istop], dx=tr.stats.delta) return pulse_width, pulse_area
def calc_velocity_flux(st_in, cat, inventory, phase_list=None, use_fixed_window=True, pre_P=.01, P_len=.05, pre_S=.01, S_len=.1, Q=1e12, correct_attenuation=False, triaxial_only=True, debug=False): """ For each arrival (on phase_list) calculate the velocity flux using the corresponding traces and save to the arrival.vel_flux to be used in the calculation of radiated seismic energy :param st_in: velocity traces :type st_in: obspy.core.Stream or uquake.core.Stream :param cat: obspy.core.event.Catalog :type cat: list of obspy.core.event.Events or uquake.core.event.Events :param phase_list: ['P'], ['S'], or ['P', 'S'] - list of arrival phases to process :type phase_list: list :param triaxial_only: if True --> only calc flux for 3-comp stations :type triaxial_only: boolean :param Q: Anelastic Q to use for attenuation correction to flux :type Q: float :param correct_attenuation: if True, scale spec by e^-pi*f*travel-time/Q before summing :type correct_attenuation: boolean """ fname = "calc_velocity_flux" if phase_list is None: phase_list = ['P', 'S'] # MTH: Try to avoid copying cat - it causes the events to lose their # link to preferred_origin! # cat = cat_in.copy() # Defensive copy - not currently needed since we only trim on copies, # still ... st = st_in.copy().detrend('demean').detrend('linear') for event in cat: origin = event.preferred_origin() if event.preferred_origin() else \ event.origins[0] for arr in origin.arrivals: phase = arr.phase if phase not in phase_list: continue pick = Pick(arr.get_pick()) if pick is None: logger.error( "%s: arr pha:%s id:%s --> Lost reference to " "pick id:%s --> SKIP" % (fname, arr.phase, arr.resource_id.id, arr.pick_id.id)) continue sta = pick.get_sta() trs = st.select(station=sta) if trs is None: logger.warning("%s: sta:%s has a [%s] arrival but no trace " "in stream --> Skip" % (fname, sta, phase)) continue if triaxial_only and len(trs) != 3: logger.info("%s: sta:%s is not 3-comp --> Skip" % (fname, sta)) continue sensor_type = get_sensor_type_from_trace(trs[0]) if sensor_type != "VEL": logger.info("%s: sta:%s sensor_type != VEL --> Skip" % (fname, sta)) continue if use_fixed_window: if phase == 'P': pre = pre_P win_secs = P_len else: pre = pre_S win_secs = S_len starttime = pick.time - pre endtime = starttime + win_secs not_enough_trace = False for tr in trs: if starttime < tr.stats.starttime or endtime > \ tr.stats.endtime: logger.warning("%s: sta:%s pha:%s tr:%s is too short to " "trim --> Don't use" % (fname, sta, phase, tr.get_id())) not_enough_trace = True break if not_enough_trace: continue tr3 = trs.copy() tr3.trim(starttime=starttime, endtime=endtime) dt = tr3[0].stats.delta # flux_t = np.sum( [tr.data**2 for tr in tr3]) * dt tsum = 0 for tr in tr3: tsum += np.sum(tr.data**2) * dt if not correct_attenuation: flux = tsum fsum = None # The only reason to do this in the freq domain is if we # want to apply attenuation correction else: travel_time = pick.time - origin.time fsum = 0. # exp(pi * f * (R/v) * 1/Q) grows so fast with freq that it's out of # control above f ~ 1e3 Hz # We could design Q = Q(f) - e.g., make Q grow fast with f to # counteract this. # Alternatively, we limit the freq range of the ( # attenuation-corrected) energy calc: # To compare the t/f calcs using Parseval's: # 1. Set Q to something like 1e12 in the settings # 2. Set fmin=0 so that the low freqs are included in the summationi sensor_response = inventory.select(arr.get_sta()) poles = np.abs(sensor_response[0].response.get_paz().poles) fmin = np.min(poles) / (2 * np.pi) fmax = 500. # looking at the response in the frequency # domain, beyond 500 Hz, the response is dominated by the # brown environmental noise. # In addition, it's necessary to locate fmin/fmax for each arrival # based on the min/max freqs where the velocity spec exceeds the # noise spec. # Thus, we can't actually include all the radiated energy, just the # energy above the noise level # so these estimates will likely be low fmin = arr.fmin fmax = arr.fmax if fmax / fmin < 3: logger.info("%s: sta:%s fmin:%.1f fmax:%.1f too " "narrowband --> Skip" % (fname, sta, fmin, fmax)) continue for tr in tr3: data = tr.data nfft = 2 * npow2(data.size) df = 1. / (dt * float(nfft)) # df is same as for y, freqs = unpack_rfft(rfft(data, n=nfft), df) y *= dt y[1:-1] *= np.sqrt(2.) tstar = travel_time / Q index = [(freqs >= fmin) & (freqs <= fmax)] freqs = freqs[index] y = y[index] fsum += np.sum( np.abs(y) * np.abs(y) * np.exp(2. * np.pi * freqs * tstar)) * df print("arr sta:%s [%s] tsum=%g fsum=%g fmin=%f fmax=%f" % (sta, arr.phase, tsum, fsum, fmin, fmax)) # exit() flux = fsum # Note we are saving the "flux" but it has not (yet) been scaled # by rho*vel. Instead it is just the sum of the integrals of the # component velocities squared for the corresponding arrival and # scaling is done in calc_energy(..) # arr.vel_flux = flux arr.vel_flux = tsum arr.vel_flux_Q = fsum return cat
def measure_displacement_pulse(st, cat, phase_list=None, debug=False): """ measure displacement pulse (area + width) for each pick on each arrival as needed for moment magnitude calculation All measurements are added to the *arrival* extras dict :param st: velocity traces :type st: obspy.core.Stream or uquake.core.Stream :param cat: obspy.core.event.Catalog :type list: list of obspy.core.event.Events or uquake.core.event.Events """ fname = 'measure_displacement_pulse' if phase_list is None: phase_list = ['P'] traces_info = [] for event in cat: origin = event.preferred_origin() if event.preferred_origin() else \ event.origins[0] arrivals = origin.arrivals for arr in arrivals: phase = arr.phase if phase not in phase_list: continue pk = Pick(arr.get_pick()) if pk is None: logger.error( "%s: arr pha:%s id:%s --> Lost reference to " "pick id:%s --> SKIP" % (fname, arr.phase, arr.resource_id.id, arr.pick_id.id)) continue sta = pk.get_sta() trs = st.select(station=sta) if trs is None: logger.warning("%s: sta:%s has a [%s] arrival but no trace " "in stream --> Skip" % (fname, sta, arr.phase)) continue sensor_type = get_sensor_type_from_trace(trs[0]) if sensor_type != "VEL": logger.info("%s: sta:%s sensor_type != VEL --> Skip" % (fname, sta)) continue for tr in trs: try: tr_dis = tr.copy().detrend("demean").detrend("linear") tr_dis.integrate().detrend("linear") except Exception as e: print(e) continue tr_dis.stats.channel = "%s.dis" % tr.stats.channel dd = {} dd['peak_dis'] = None dd['max_dis'] = None dd['tpeak_dis'] = None dd['tmax_dis'] = None dd['dis_pulse_width'] = None dd['dis_pulse_area'] = None tr_dict = arr.traces[tr.get_id()] polarity = tr_dict['polarity'] t1 = tr_dict.get('t1', None) t2 = tr_dict.get('t2', None) if polarity != 0: if t1 is None or t2 is None: logger.error("%s: t1 or t2 is None --> You shouldn't " "be here!" % (fname)) continue i1 = int( (t1 - tr.stats.starttime) * tr.stats.sampling_rate) i2 = int( (t2 - tr.stats.starttime) * tr.stats.sampling_rate) ipick = int((pk.time - tr.stats.starttime) * tr.stats.sampling_rate) icross = i2 tr_dis.data = tr_dis.data - tr_dis.data[i1] # tr_dis.data = tr_dis.data - tr_dis.data[ipick] dis_polarity = np.sign(tr_dis.data[icross]) pulse_width, pulse_area = _get_pulse_width_and_area( tr_dis, i1, icross) npulse = int(pulse_width * tr.stats.sampling_rate) max_pulse_duration = .08 nmax_len = int(max_pulse_duration * tr.stats.sampling_rate) if pulse_width != 0: ipeak, peak_dis = _get_peak_amp( tr_dis, ipick, ipick + npulse) # max_dis = max within max_pulse_duration of pick time imax, max_dis = _get_peak_amp(tr_dis, ipick, ipick + nmax_len) tmax_dis = tr.stats.starttime + float( imax * tr.stats.delta) tpeak_dis = tr.stats.starttime + float( ipeak * tr.stats.delta) tcross_dis = pk.time + pulse_width dd['peak_dis'] = peak_dis dd['max_dis'] = max_dis dd['tpeak_dis'] = tpeak_dis dd['tmax_dis'] = tmax_dis dd['dis_pulse_width'] = pulse_width dd['dis_pulse_area'] = pulse_area if debug: logger.debug("[%s] Dis pol=%d tpick=%s" % (phase, dis_polarity, pk.time)) logger.debug(" tpeak=%s " "peak_dis=%12.10g" % (tpeak_dis, peak_dis)) logger.debug(" tcross=%s" % tcross_dis) logger.debug(" tmax=%s " "max_dis=%12.10g" % (tmax_dis, max_dis)) logger.debug(" dis pulse width=%.5f" % pulse_width) logger.debug(" dis pulse area=%12.10g" % pulse_area) else: logger.warning( "%s: Got pulse_width=0 for tr:%s pha:%s" % (fname, tr.get_id(), phase)) arr.traces[tr.get_id()] = dict(tr_dict, **dd) dd['trace_id'] = tr.get_id() dd['arrival_id'] = arr.resource_id dd['event_id'] = event.resource_id dd['origin_id'] = origin.resource_id traces_info.append(dd) # Process next tr in trs # Process next arr in arrivals # Process next event in cat return cat
def measure_velocity_pulse( st, cat, phase_list=None, pulse_min_width=.005, pulse_min_snr_P=7, pulse_min_snr_S=5, debug=False, ): """ locate velocity pulse (zero crossings) near pick and measure peak amp, polarity, etc on it All measurements are added to the *arrival* extras dict :param st: velocity traces :type st: obspy.core.Stream or uquake.core.Stream :param cat: obspy.core.event.Catalog :type cat: list of obspy.core.event.Events or uquake.core.event.Events :param phase_list: ['P'], ['S'], or ['P', 'S'] - list of arrival phases to process :type phase_list: list :param pulse_min_width: Measured first pulse must be this wide to be retained :type pulse_min_width: float :param pulse_min_snr_P: Measure first P pulse must have snr greater than this :type pulse_min_snr_P: float :param pulse_min_snr_S: Measure first S pulse must have snr greater than this :type pulse_min_snr_S: float """ fname = 'measure_velocity_pulse' traces_info = [] if phase_list is None: phase_list = ['P'] # Average of P,S min snr used for finding zeros min_pulse_snr = int((pulse_min_snr_P + pulse_min_snr_S) / 2) for event in cat: origin = event.preferred_origin() if event.preferred_origin() else \ event.origins[-1] arrivals = origin.arrivals for arr in arrivals: phase = arr.phase if phase not in phase_list: continue pk = Pick(arr.get_pick()) if pk is None: logger.error( "%s: arr pha:%s id:%s --> Lost reference to " "pick id:%s --> SKIP" % (fname, arr.phase, arr.resource_id.id, arr.pick_id.id)) continue sta = pk.get_sta() trs = st.select(station=sta) if trs is None: logger.warning("%s: sta:%s has a [%s] arrival but no trace " "in stream --> Skip" % (fname, sta, arr.phase)) continue sensor_type = get_sensor_type_from_trace(trs[0]) if sensor_type != "VEL": logger.info("%s: sta:%s sensor_type != VEL --> Skip" % (fname, sta)) continue arr.traces = {} for tr in trs: try: tr.detrend("demean").detrend("linear") except Exception as e: print(e) continue data = tr.data.copy() ipick = int( (pk.time - tr.stats.starttime) * tr.stats.sampling_rate) polarity, vel_zeros = _find_signal_zeros( tr, ipick, nzeros_to_find=3, min_pulse_width=pulse_min_width, min_pulse_snr=min_pulse_snr, debug=debug) dd = {} dd['polarity'] = 0 dd['t1'] = None dd['t2'] = None dd['peak_vel'] = None dd['tpeak_vel'] = None dd['pulse_snr'] = None # A good pick will have the first velocity pulse located # between i1 and i2 if vel_zeros is not None: i1 = vel_zeros[0] i2 = vel_zeros[1] t1 = tr.stats.starttime + float(i1 * tr.stats.delta) t2 = tr.stats.starttime + float(i2 * tr.stats.delta) ipeak, peak_vel = _get_peak_amp(tr, i1, i2) tpeak = tr.stats.starttime + float(ipeak * tr.stats.delta) noise_npts = int(.01 * tr.stats.sampling_rate) noise_end = ipick - int(.005 * tr.stats.sampling_rate) noise = data[noise_end - noise_npts:noise_end] noise1 = np.abs(np.mean(noise)) noise2 = np.abs(np.median(noise)) noise3 = np.abs(np.std(noise)) noise_level = np.max([noise1, noise2, noise3]) pulse_snr = np.abs(peak_vel / noise_level) pulse_width = float((i2 - i1) * tr.stats.delta) pulse_thresh = pulse_min_snr_P if phase == 'S': pulse_thresh = pulse_min_snr_S if pulse_snr < pulse_thresh: logger.debug( "%s: tr:%s pha:%s t1:%s t2:%s " "pulse_snr=%.1f < thresh" % (fname, tr.get_id(), phase, t1, t2, pulse_snr)) polarity = 0 if pulse_width < pulse_min_width: logger.debug("%s: tr:%s pha:%s t1:%s t2:%s " "pulse_width=%f < %f" % (fname, tr.get_id(), phase, t1, t2, pulse_width, pulse_min_width)) polarity = 0 dd['polarity'] = polarity dd['peak_vel'] = peak_vel dd['tpeak_vel'] = tpeak dd['t1'] = t1 dd['t2'] = t2 dd['pulse_snr'] = pulse_snr else: logger.debug( "%s: Unable to locate zeros for tr:%s pha:%s" % (fname, tr.get_id(), phase)) arr.traces[tr.get_id()] = dd dd['trace_id'] = tr.get_id() dd['arrival_id'] = arr.resource_id dd['event_id'] = event.resource_id dd['origin_id'] = origin.resource_id traces_info.append(dd) # Process next phase in phase_list # Process tr in st # Process next event in cat return cat
def calculate_energy_from_flux(cat, inventory, vp, vs, rho=2700., use_sdr_rad=False, use_water_level=False, rad_min=0.2): fname = 'calculate_energy_from_flux' for event in cat: origin = event.preferred_origin() if event.preferred_origin() else \ event.origins[0] use_sdr = False if use_sdr_rad and event.preferred_focal_mechanism() is not None: mech = event.preferred_focal_mechanism() np1 = event.preferred_focal_mechanism().nodal_planes.nodal_plane_1 sdr = (np1.strike, np1.dip, np1.rake) use_sdr = True # for phase in ['P', 'S']: # for arr in [x for x in arrivals if x.phase == phase]: P_energy = [] S_energy = [] for arr in origin.arrivals: pk = Pick(arr.get_pick()) # try: sta = pk.get_sta() sta_response = inventory.select(sta) # except AttributeError: # logger.warning( # f'Cannot get station for arrival "{arr.resource_id}"' # f' for event "{event.resource_id}".') # continue phase = arr.phase if phase.upper() == 'P': velocity = vp.interpolate(sta_response.loc) rad_pat = 4 / 15 elif phase.upper() == 'S': velocity = vs.interpolate(sta_response.loc) rad_pat = 2 / 5 # could check for arr.hypo_dist_in_m here but it's almost identical R = arr.distance # MTH: Setting preferred flux = vel_flux_Q = attenuation tstar # corrected flux flux = 0 if arr.vel_flux_Q is not None: flux = arr.vel_flux_Q logger.info("%s: vel_flux_Q exists in [%s], using this for " "energy, arr for sta:%s" % (fname, phase, sta)) elif arr.vel_flux is not None: flux = arr.vel_flux logger.info("%s: vel_flux exists in [%s], using this for " "energy, arr for sta:%s" % (fname, phase, sta)) else: logger.info("%s: No vel_flux set for arr sta:%s pha:%s --> " "skip energy calc" % (fname, sta, phase)) continue energy = (4. * np.pi * R**2) * rho * velocity * flux scale = 1. if use_sdr: if arr.get('takeoff_angle', None) and arr.get('azimuth', None): takeoff_angle = arr.takeoff_angle takeoff_azimuth = arr.azimuth strike = sdr[0] dip = sdr[1] rake = sdr[2] rad = double_couple_rad_pat(takeoff_angle, takeoff_azimuth, strike, dip, rake, phase=phase) if use_water_level and np.abs(rad) < rad_min: rad = rad_min scale = rad_pat / rad**2 energy *= scale arr.energy = energy if phase == 'P': P_energy.append(energy) else: S_energy.append(energy) if not P_energy: P_energy = [0] logger.warning('No P energy measurements. The P-wave energy will ' 'be set to 0, the total energy will not include ' 'the P-wave energy. This will bias the total ' 'energy value') if not S_energy: S_energy = [0] logger.warning( 'No S energy measurements. The S-wave energy will be set to ' '0, the total energy will not include the P-wave energy. ' 'This will bias the total energy value') energy_p = np.median(P_energy) energy_s = np.median(S_energy) E = energy_p + energy_s nvals = len(S_energy) + len(P_energy) comment = 'Energy [N-m] calculated from sum of median P + median S ' \ 'energy' comment_ep = '"Ep":{}, "std_Ep":{}'.format(np.median(P_energy), np.std(P_energy)) comment_ep = '{' + comment_ep + '}' comment_es = '"Es":{}, "std_Es":{}'.format(np.median(S_energy), np.std(S_energy)) comment_es = '{' + comment_es + '}' if E > 0: # Note: this is not a "mag" (there is no log10). # Just using magnitude class to hold it in quakeml energy_mag = Magnitude( origin_id=origin.resource_id, mag=E, magnitude_type='E', station_count=nvals, evaluation_mode='automatic', comments=[ Comment(text=comment), Comment(text=comment_ep), Comment(text=comment_es) ], ) energy_mag.energy_p_joule = energy_p energy_mag.energy_s_joule = energy_s energy_mag.energy_joule = E event.magnitudes.append(energy_mag) else: logger.warning("%s: Calculated val of Energy E=[%s] nS=%d nP=%d " "is not fit to keep!" % (fname, E, nvals, len(P_energy))) logger.warning("%s: Energy mag not written to Quakeml" % fname) return cat
def calc_magnitudes_from_lambda(cat, vp=5300, vs=3500, density=2700, P_or_S='P', use_smom=False, use_sdr_rad=False, use_free_surface_correction=False, min_dist=20., **kwargs): """ Calculate the moment magnitude at each station from lambda, where lambda is either: 'dis_pulse_area' (use_smom=False) - calculated by integrating arrival displacement pulse in time 'smom' (use_smom=True) - calculated by fiting Brune spectrum to displacement spectrum in frequency """ fname = 'calc_magnitudes_from_lambda' # Don't loop over event here, do it in the calling routine # so that vp/vs can be set for correct source depth event = cat[0] origin = event.preferred_origin() if event.preferred_origin() else \ event.origins[0] ev_loc = origin.loc origin_id = origin.resource_id rad_P, rad_S = 0.52, 0.63 if P_or_S == 'P': v = vp rad = rad_P mag_type = 'Mw_P' else: v = vs rad = rad_S mag_type = 'Mw_S' if use_smom: magnitude_comment = 'station magnitude measured in frequeny-domain (' \ 'smom)' lambda_key = 'smom' else: magnitude_comment = 'station magnitude measured in time-domain (' \ 'dis_pulse_area)' lambda_key = 'dis_pulse_area' if use_free_surface_correction and np.abs(ev_loc[2]) > 0.: logger.warning("%s: Free surface correction requested for event [" "h=%.1f] > 0" % (fname, ev_loc[2])) if use_sdr_rad and 'sdr' not in kwargs: logger.warning("%s: use_sdr_rad requested but NO [sdr] given!" % fname) station_mags = [] Mw_list = [] Mw_P = [] arrivals = [ arr for arr in event.preferred_origin().arrivals if arr.phase == P_or_S ] for arr in arrivals: try: pk = arr.pick_id.get_referred_object() sta = pk.waveform_id.station_code cha = pk.waveform_id.channel_code net = pk.waveform_id.network_code except AttributeError: logger.warning('Missing data on arrival', exc_info=True) continue fs_factor = 1. if use_free_surface_correction: if arr.get('inc_angle', None): inc_angle = arr.inc_angle fs_factor = free_surface_displacement_amplification( inc_angle, vp, vs, incident_wave=P_or_S) # MTH: Not ready to implement this. The reflection coefficients # are expressed in x1,x2,x3 coords # print("inc_angle:%.1f x1:%.1f x3:%.1f" % (inc_angle, fs_factor[0], fs_factor[2])) # MTH: The free surface corrections are returned as <x1,x2,x3>=< fs_factor = 1. else: logger.warning("%s: sta:%s cha:%s pha:%s: inc_angle NOT set " "in arrival dict --> use default" % (fname, sta, cha, arr.phase)) if use_sdr_rad and 'sdr' in kwargs: strike, dip, rake = kwargs['sdr'] if arr.get('takeoff_angle', None) and arr.get('azimuth', None): takeoff_angle = arr.takeoff_angle takeoff_azimuth = arr.azimuth rad = double_couple_rad_pat(takeoff_angle, takeoff_azimuth, strike, dip, rake, phase=P_or_S) rad = np.abs(rad) logger.debug("%s: phase=%s rad=%f" % (fname, P_or_S, rad)) magnitude_comment += ' radiation pattern calculated for (s,' \ 'd,r)= (%.1f,%.1f,%.1f) theta:%.1f ' \ 'az:%.1f pha:%s |rad|=%f' % \ (strike, dip, rake, takeoff_angle, takeoff_azimuth, P_or_S, rad) # logger.info(magnitude_comment) else: logger.warnng( "%s: sta:%s cha:%s pha:%s: " "takeoff_angle/azimuth NOT set in arrival dict --> use default radiation pattenr" % (fname, sta, cha, arr.phase)) _lambda = getattr(arr, lambda_key) if _lambda is not None: M0_scale = 4. * np.pi * density * v**3 / (rad * fs_factor) # R = np.linalg.norm(sta_dict['station'].loc -ev_loc) #Dist in meters # MTH: obspy arrival.distance = *epicentral* distance in degrees # >> Add attribute hypo_dist_in_m to uquake arrival class # to make it clear if arr.distance: R = arr.distance else: R = arr.hypo_dist_in_m if R >= min_dist: M0 = M0_scale * R * np.abs(_lambda) Mw = 2. / 3. * np.log10(M0) - 6.033 # print("MTH: _lambda=%g R=%.1f M0=%g" % (np.abs(_lambda), R, M0)) Mw_list.append(Mw) station_mag = StationMagnitude( origin_id=origin_id, mag=Mw, station_magnitude_type=mag_type, comments=[Comment(text=magnitude_comment)], waveform_id=WaveformStreamID(network_code=net, station_code=sta, channel_code=cha)) station_mags.append(station_mag) else: logger.info("arrival sta:%s pha:%s dist=%.2f < min_dist(" "=%.2f) --> Skip" % (fname, sta, arr.phase, R, min_dist)) # else: # logger.warning("arrival sta:%s cha:%s arr pha:%s lambda_key:%s is NOT SET --> Skip" \ # % (sta, cha, arr.phase, lambda_key)) logger.info( "nmags=%d avg:%.1f med:%.1f std:%.1f" % (len(Mw_list), np.mean(Mw_list), np.median(Mw_list), np.std(Mw_list))) return np.median(Mw_list), station_mags