def _synth_spec(config, spec, par, par_err): """Return a stream with one or more synthetic spectra.""" spec_st = Stream() params_opt = [par[key] for key in ('Mw', 'fc', 't_star')] freq = spec.get_freq() spec_synth = spec.copy() spec_synth.stats.channel = spec.stats.channel[:-1] + 'S' spec_synth.stats.par = par spec_synth.stats.par_err = par_err spec_synth.data_mag = spectral_model(freq, *params_opt) spec_synth.data = mag_to_moment(spec_synth.data_mag) spec_st.append(spec_synth) # Add an extra spectrum with no attenuation if config.plot_spectra_no_attenuation: spec_synth = spec.copy() spec_synth.stats.channel = spec.stats.channel[:-1] + 's' _params = list(params_opt) _params[-1] = 0 spec_synth.data_mag = spectral_model(freq, *_params) spec_synth.data = mag_to_moment(spec_synth.data_mag) spec_st.append(spec_synth) if config.plot_spectra_no_fc: spec_synth = spec.copy() spec_synth.stats.channel = spec.stats.channel[:-1] + 't' _params = list(params_opt) _params[1] = 1e999 spec_synth.data_mag = spectral_model(freq, *_params) spec_synth.data = mag_to_moment(spec_synth.data_mag) spec_st.append(spec_synth) return spec_st
def make_synth(config, spec_st, trace_spec=None): import math import numpy as np from copy import deepcopy from sourcespec.spectrum import Spectrum from sourcespec.ssp_spectral_model import spectral_model, objective_func from sourcespec.ssp_util import mag_to_moment, moment_to_mag fdelta = 0.01 fmin = config.options.fmin fmax = config.options.fmax + fdelta residuals = list() n = 0 for fc, mag, Mo, t_star, alpha in zip(config.options.fc, config.options.mag, config.options.Mo, config.options.t_star, config.options.alpha): spec = Spectrum() if trace_spec: spec.stats = deepcopy(trace_spec.stats) else: spec.stats.begin = fmin spec.stats.delta = fdelta spec.stats.npts = int((fmax - fmin) / fdelta) if math.isnan(Mo): Mo = mag_to_moment(mag) else: mag = moment_to_mag(Mo) spec.stats.station = 'S{:02d}'.format(n) n += 1 spec.stats.instrtype = 'Synth' spec.stats.channel = spec.stats.channel[:-1] + 'S' spec.stats.par = { 'Mw': mag, 'fc': fc, 't_star': t_star, 'alpha': alpha } freq = spec.get_freq() freq_log = trace_spec.freq_log spec.freq_log = freq_log spec.data_mag = spectral_model(freq, mag, fc, t_star, alpha) spec.data = mag_to_moment(spec.data_mag) spec.data_log_mag = spectral_model(freq_log, mag, fc, t_star, alpha) spec.data_log = mag_to_moment(spec.data_log_mag) spec_st.append(spec) if trace_spec: objective_func2 = objective_func(freq, trace_spec.data_mag, np.ones_like(trace_spec.data_mag)) print(Mo, mag, fc, t_star, alpha, objective_func2((mag, fc, t_star, alpha))) residuals.append([ Mo, mag, fc, t_star, objective_func2((mag, fc, t_star, alpha)) ])
def make_synth(config, spec_st, trace_spec=None): fdelta = 0.01 fmin = config.options.fmin fmax = config.options.fmax + fdelta residuals = list() for fc, mag, Mo, t_star, alpha in zip(config.options.fc, config.options.mag, config.options.Mo, config.options.t_star, config.options.alpha): spec = Spectrum() if trace_spec: spec.stats = deepcopy(trace_spec.stats) else: spec.stats.begin = fmin spec.stats.delta = fdelta spec.stats.npts = int((fmax - fmin) / fdelta) if math.isnan(Mo): Mo = mag_to_moment(mag) else: mag = moment_to_mag(Mo) spec.stats.station = 'Mw: %.1f fc: %.2fHz t*: %.2fs alpha: %.2f' %\ (mag, fc, t_star, alpha) spec.stats.instrtype = 'Synth' spec.stats.channel = spec.stats.channel[:-1] + 'S' spec.stats.par = { 'Mw': mag, 'fc': fc, 't_star': t_star, 'alpha': alpha } freq = spec.get_freq() spec.data_mag = spectral_model(freq, mag, fc, t_star, alpha) spec.data = mag_to_moment(spec.data_mag) spec_st.append(spec) if trace_spec: objective_func2 = objective_func(freq, trace_spec.data_mag, np.ones_like(trace_spec.data_mag)) print(Mo, mag, fc, t_star, alpha, objective_func2((mag, fc, t_star, alpha))) residuals.append([ Mo, mag, fc, t_star, objective_func2((mag, fc, t_star, alpha)) ])
def spectral_residuals(config, spec_st, sourcepar): """ Compute spectral residuals with respect to an average spectral model. Saves a stream of residuals to disk using pickle. """ # Use weighted means means = sourcepar.means_weight params_name = ('Mw', 'fc', 't_star') sourcepar_mean = dict( zip(params_name, [means['Mw'], means['fc'], means['t_star']])) residuals = Stream() for station in set(x.stats.station for x in spec_st.traces): spec_st_sel = spec_st.select(station=station) for spec in spec_st_sel.traces: if spec.stats.channel[-1] != 'H': continue xdata = spec.get_freq() synth_mean_mag = spectral_model(xdata, **sourcepar_mean) res = spec.copy() res.data_mag = spec.data_mag - synth_mean_mag res.data = mag_to_moment(res.data_mag) residuals.append(res) # Save residuals as pickle file evid = config.hypo.evid res_file = os.path.join(config.options.outdir, evid + '-residuals.pickle') logger.info('Spectral residuals saved to: %s' % res_file) with open(res_file, 'wb') as fp: pickle.dump(residuals, fp)
def _params_text(spec, ax, color, path_effects, stack_plots): global station_text_ypos if stack_plots: params_text_ypos = station_text_ypos - 0.04 else: params_text_ypos = 0.03 color = 'black' fc = spec.stats.par['fc'] Mw = spec.stats.par['Mw'] Mo = mag_to_moment(Mw) t_star = spec.stats.par['t_star'] if 'par_err' in spec.stats.keys(): fc_err_left, fc_err_right = spec.stats.par_err['fc'] Mw_err_left, Mw_err_right = spec.stats.par_err['Mw'] t_star_err_left, t_star_err_right =\ spec.stats.par_err['t_star'] else: fc_err_left = fc_err_right = 0. Mw_err_left = Mw_err_right = 0. t_star_err_left = t_star_err_right = 0. Mo_text = 'Mo: {:.2g}'.format(Mo) Mw_text = 'Mw: {:.2f}'.format(Mw) if round(Mw_err_left, 2) == round(Mw_err_right, 2): Mw_text += '±{:.2f}'.format(Mw_err_left) else: Mw_text += '[-{:.2f},+{:.2f}]'.format( Mw_err_left, Mw_err_right) fc_text = 'fc: {:.2f}'.format(fc) if round(fc_err_left, 2) == round(fc_err_right, 2): fc_text += '±{:.2f}Hz'.format(fc_err_left) else: fc_text += '[-{:.2f},+{:.2f}]Hz'.format( fc_err_left, fc_err_right) t_star_text = 't*: {:.2f}'.format(t_star) if round(t_star_err_left, 2) == round(t_star_err_right, 2): t_star_text += '±{:.2f}s'.format(t_star_err_left) else: t_star_text += '[-{:.2f},+{:.2f}]s'.format( t_star_err_left, t_star_err_right) if len(fc_text+t_star_text) > 38: sep = '\n' params_text_ypos -= 0.01 station_text_ypos += 0.06 else: sep = ' ' params_text = '{} {}\n{}{}{}'.format( Mo_text, Mw_text, fc_text, sep, t_star_text) ax.text( 0.05, params_text_ypos, params_text, horizontalalignment='left', verticalalignment='bottom', color=color, fontsize=9, transform=ax.transAxes, zorder=50, path_effects=path_effects)
def station_correction(spec_st, config): """ Correct spectra using station-average residuals. Residuals are obtained from a previous run. """ res_filepath = config.residuals_filepath if res_filepath is None: return spec_st try: with open(res_filepath, 'rb') as fp: residual = pickle.load(fp) except Exception as msg: logger.error(msg) ssp_exit(1) for spec in [spec for spec in spec_st if (spec.stats.channel[-1] == 'H')]: station = spec.stats.station if station in set(x.stats.station for x in residual): # apply correction corr = residual.select(station=station)[0] freq = spec.get_freq() fmin = freq.min() fmax = freq.max() corr = corr.slice(fmin, fmax) corr.data_mag = moment_to_mag(corr.data) spec_corr = spec.copy() # uncorrected spectrum will have component name 'h' spec.stats.channel = spec.stats.channel[:-1] + 'h' spec_corr.data_mag -= corr.data_mag # interpolate the corrected data_mag to log frequencies f = interp1d(freq, spec_corr.data_mag, fill_value='extrapolate') spec_corr.data_log_mag = f(spec_corr.freq_log) # convert mag to moment spec_corr.data = mag_to_moment(spec_corr.data_mag) spec_corr.data_log = mag_to_moment(spec_corr.data_log_mag) spec_st.append(spec_corr) logger.info( '{} corrected, frequency range is: {:.2f} {:.2f} Hz'.format( spec_corr.id, fmin, fmax)) return spec_st
def station_correction(spec_st, config): """ Correct spectra using station-average residuals. Residuals are obtained from a previous run. """ res_filepath = config.residuals_filepath if res_filepath is None: msg = "'-C' option set, but 'residuals_filepath' not specified " msg += "in config file: ignoring station correction" logger.warning(msg) return spec_st with open(res_filepath, 'rb') as fp: residual = pickle.load(fp) for spec in [spec for spec in spec_st if (spec.stats.channel[-1] == 'H')]: station = spec.stats.station if station in set(x.stats.station for x in residual): # apply correction corr = residual.select(station=station)[0] freq = spec.get_freq() fmin = freq.min() fmax = freq.max() corr = corr.slice(fmin, fmax) corr.data_mag = moment_to_mag(corr.data) spec.data_mag -= corr.data_mag # interpolate the corrected data_mag to log frequencies f = interp1d(freq, spec.data_mag, fill_value='extrapolate') spec.data_log_mag = f(spec.freq_log) # convert mag to moment spec.data = mag_to_moment(spec.data_mag) spec.data_log = mag_to_moment(spec.data_log_mag) logger.info('%s corrected, frequency range is: %f %f' % (spec.id, fmin, fmax)) return spec_st
def _M0_avg_and_std(Mw_mean, Mw_error): """Compute average and standard deviation for Mo starting from Mw.""" Mo_mean = mag_to_moment(Mw_mean) Mo_min = mag_to_moment(Mw_mean - Mw_error) Mo_max = mag_to_moment(Mw_mean + Mw_error) return Mo_mean, (Mo_mean - Mo_min, Mo_max - Mo_mean)
freqs_min = [spec.get_freq().min() for spec in res] freqs_max = [spec.get_freq().max() for spec in res] freq_min = min(freqs_min) freq_max = max(freqs_max) spec_mean = Spectrum() spec_mean.stats.begin = freq_min spec_mean.stats.delta = res[0].stats.delta spec_mean.stats.station = res[0].stats.station spec_mean.data_mag = None for spec in res: spec_slice = spec.slice(freq_min, freq_max, pad=True, fill_value=mag_to_moment(0)) spec_slice.data_mag = moment_to_mag(spec_slice.data) norm = (spec_slice.data_mag != 0).astype(int) if spec_mean.data_mag is None: spec_mean.data_mag = spec_slice.data_mag norm_mean = norm else: spec_mean.data_mag += spec_slice.data_mag norm_mean += norm spec_mean.data_mag /= norm_mean spec_mean.data = mag_to_moment(spec_mean.data_mag) residual_mean.append(spec_mean) # plot traces if options.plot:
def _spec_inversion(config, spec, noise_weight): """Invert one spectrum.""" # azimuth computation coords = spec.stats.coords hypo = spec.stats.hypo stla = coords.latitude stlo = coords.longitude evla = hypo.latitude evlo = hypo.longitude geod = gps2dist_azimuth(evla, evlo, stla, stlo) az = geod[1] freq_log = spec.freq_log ydata = spec.data_log_mag noise_weight = noise_weight.data_log if config.weighting == 'noise': weight = noise_weight # 'curve_fit' interprets 'yerr' as standard deviation vector # and calculates weights as 1/yerr^2 . # Therefore we build yerr as: yerr = 1./np.sqrt(weight) elif config.weighting == 'frequency': # frequency weighting: # config.weight for f<=f_weight # 1 for f> f_weight yerr = np.ones(len(ydata)) yerr[freq_log <= config.f_weight] = 1./math.sqrt(config.weight) weight = 1./np.power(yerr, 2) elif config.weighting is None: weight = yerr = np.ones(len(ydata)) # Find the frequency range to compute Mw_0 and, possibly, t_star_0: # we start where signal-to-noise becomes strong idx0 = np.where(noise_weight > 0.5)[0][0] # we stop at the first max of signal-to-noise (proxy for fc) idx_max = argrelmax(noise_weight)[0] # just keep the indexes for maxima > 0.5 idx_max = [idx for idx in idx_max if noise_weight[idx] > 0.5] if not idx_max: # if idx_max is empty, then the source and/or noise spectrum # is most certainly "strange". In this case, we simply give up. logger.warning('%s: unable to find a frequency range to compute ' 'Mw_0' % spec.id) logger.warning(' This is possibly due to an uncommon ' 'spectrum for the trace (e.g., a resonance).') raise RuntimeError idx1 = idx_max[0] if idx1 == idx0: idx1 = idx_max[1] # first maximum is a proxy for fc, we use it for fc_0: fc_0 = freq_log[idx1] t_star_min = t_star_max = None if config.invert_t_star_0: # fit t_star_0 and Mw on the initial part of the spectrum, # corrected for the effect of fc ydata_corr = ydata - spectral_model(freq_log, Mw=0, fc=fc_0, t_star=0) ydata_corr = smooth(ydata_corr, window_len=18) slope, Mw_0 = np.polyfit( freq_log[idx0: idx1], ydata_corr[idx0: idx1], deg=1) t_star_0 = -3./2 * slope / (np.pi * np.log10(np.e)) t_star_min = t_star_0 * (1 - config.t_star_0_variability) t_star_max = t_star_0 * (1 + config.t_star_0_variability) if not config.invert_t_star_0 or t_star_0 < 0: # we calculate the initial value for Mw as an average Mw_0 = np.nanmean(ydata[idx0: idx1]) t_star_0 = config.t_star_0 initial_values = InitialValues(Mw_0, fc_0, t_star_0) logger.info('%s %s: initial values: %s' % (spec.id, spec.stats.instrtype, str(initial_values))) bounds = Bounds(config, spec, initial_values) bounds.Mw_min = Mw_0 - 0.1 bounds.Mw_max = Mw_0 + 0.1 if t_star_min is not None: bounds.t_star_min = t_star_min if t_star_max is not None: bounds.t_star_max = t_star_max logger.info('%s %s: bounds: %s' % (spec.id, spec.stats.instrtype, str(bounds))) try: params_opt, params_cov = _curve_fit( config, spec, weight, yerr, initial_values, bounds) except (RuntimeError, ValueError) as m: logger.warning(m) logger.warning('%s %s: unable to fit spectral model' % (spec.id, spec.stats.instrtype)) raise params_name = ('Mw', 'fc', 't_star') par = OrderedDict(zip(params_name, params_opt)) par['Mo'] = mag_to_moment(par['Mw']) par['hyp_dist'] = spec.stats.hypo_dist par['epi_dist'] = spec.stats.epi_dist par['az'] = az par['lon'] = spec.stats.coords.longitude par['lat'] = spec.stats.coords.latitude error = np.sqrt(params_cov.diagonal()) par_err = OrderedDict(zip(params_name, error)) return par, par_err
def plot_spectra(config, spec_st, specnoise_st=None, ncols=4, stack_plots=False, plottype='regular', async_plotter=None): """ Plot spectra for signal and noise. Display to screen and/or save to file. """ # Check config, if we need to plot at all if not config.PLOT_SHOW and not config.PLOT_SAVE: return matplotlib.rcParams['pdf.fonttype'] = 42 # to edit text in Illustrator nlines, ncols, freq_minmax, moment_minmax, mag_minmax =\ _nplots(config, spec_st, specnoise_st, config.plot_spectra_maxrows, ncols, plottype) figures = [] fig, axes, ax0 = _make_fig(config, nlines, ncols, freq_minmax, moment_minmax, mag_minmax, stack_plots, plottype) figures.append(fig) # Path effect to contour text in white path_effects = [PathEffects.withStroke(linewidth=3, foreground='white')] # Plot! plotn = 0 if config.plot_spectra_ignored: stalist = sorted( set((sp.stats.hypo_dist, sp.id[:-1]) for sp in spec_st)) else: stalist = sorted( set((sp.stats.hypo_dist, sp.id[:-1]) for sp in spec_st if not sp.stats.ignore)) for t in stalist: plotn += 1 _, specid = t # 'code' is band+instrument code network, station, location, code = specid.split('.') spec_st_sel = spec_st.select(network=network, station=station, location=location) if plotn > nlines * ncols: # Add lables and legend before making a new figure _add_labels(axes, plotn - 1, ncols, plottype) _add_legend(config, ax0, spec_st, specnoise_st, stack_plots, plottype) fig, axes, ax0 = _make_fig(config, nlines, ncols, freq_minmax, moment_minmax, mag_minmax, stack_plots, plottype) figures.append(fig) plotn = 1 ax_text = False ax, ax2 = axes[plotn - 1] sn_text_ypos = 0.95 for spec in spec_st_sel.traces: if spec.stats.channel[:-1] != code: continue if not config.plot_spectra_ignored and spec.stats.ignore: continue orientation = spec.stats.channel[-1] color, linestyle, linewidth =\ _color_lines(config, orientation, plotn, stack_plots) # dim out ignored spectra if spec.stats.ignore: alpha = 0.3 else: alpha = 1.0 if plottype == 'regular': ax.loglog(spec.get_freq(), spec.data, color=color, alpha=alpha, linestyle=linestyle, linewidth=linewidth, zorder=20) # Write spectral S/N for regular Z,N,E components if orientation not in ['S', 's', 't', 'H']: _text = 'S/N: {:.1f}'.format(spec.stats.spectral_snratio) ax.text(0.95, sn_text_ypos, _text, ha='right', va='top', fontsize=8, color=color, path_effects=path_effects, transform=ax.transAxes, zorder=20) sn_text_ypos -= 0.05 if orientation == 'S': fc = spec.stats.par['fc'] if 'par_err' in spec.stats.keys(): fc_err = spec.stats.par_err['fc'] fc_min = fc - fc_err if fc_min < 0: fc_min = 0.01 ax.axvspan(fc_min, fc + fc_err, color='#bbbbbb', alpha=0.3, zorder=1) ax.axvline(fc, color='#999999', linewidth=2., zorder=1) Mw = spec.stats.par['Mw'] if 'par_err' in spec.stats.keys(): Mw_err = spec.stats.par_err['Mw'] ax2.axhspan(Mw - Mw_err, Mw + Mw_err, color='#bbbbbb', alpha=0.3, zorder=1) elif plottype == 'weight': ax.semilogx(spec.get_freq(), spec.data, color=color, alpha=alpha, zorder=20) else: raise ValueError('Unknown plot type: %s' % plottype) # leg = ax.legend(('N', 'E', 'H'), 'lower right') if specnoise_st: if spec.stats.channel[-1] != 'S': specid = spec.get_id() try: sp_noise = specnoise_st.select(id=specid)[0] except IndexError: continue orientation = sp_noise.stats.channel[-1] ax.loglog(sp_noise.get_freq(), sp_noise.data, linestyle=':', linewidth=linewidth, color=color, alpha=alpha, zorder=20) if not ax_text: ax_text = '%s %s' % (spec.id[:-1], spec.stats.instrtype) if stack_plots: text_y = 0.05 + (plotn - 1) * 0.05 else: text_y = 0.15 color = 'black' ax_text += '\n%.1f km (%.1f km)' % (spec.stats.hypo_dist, spec.stats.epi_dist) ax.text(0.05, text_y, ax_text, horizontalalignment='left', verticalalignment='bottom', color=color, transform=ax.transAxes, zorder=50, path_effects=path_effects) ax_text = True if orientation == 'S': if stack_plots: text_y2 = text_y - 0.02 else: text_y2 = 0.03 color = 'black' fc = spec.stats.par['fc'] Mw = spec.stats.par['Mw'] Mo = mag_to_moment(Mw) t_star = spec.stats.par['t_star'] if 'par_err' in spec.stats.keys(): fc_err = spec.stats.par_err['fc'] Mw_err = spec.stats.par_err['Mw'] t_star_err = spec.stats.par_err['t_star'] else: fc_err = Mw_err = t_star_err = 0. ax.text(0.05, text_y2, 'Mo: %.2g Mw: %.2f±%.2f\n' 'fc: %.2f±%.2f Hz t*: %.2f±%.2fs' % (Mo, Mw, Mw_err, fc, fc_err, t_star, t_star_err), horizontalalignment='left', verticalalignment='bottom', color=color, fontsize=9, transform=ax.transAxes, zorder=50, path_effects=path_effects) # Add lables and legend for the last figure _add_labels(axes, plotn, ncols, plottype) _add_legend(config, ax0, spec_st, specnoise_st, stack_plots, plottype) # Turn off the unused axes for ax, ax2 in axes[plotn:]: ax.set_axis_off() if ax2: ax2.set_axis_off() if config.PLOT_SHOW: plt.show() if config.PLOT_SAVE: _savefig(config, plottype, figures, async_plotter)
def main(): usage = 'usage: %prog [options] residuals_dir' parser = OptionParser(usage=usage) parser.add_option('-m', '--min_spectra', dest='min_spectra', action='store', default='20', help='minimum number of spectra to ' 'compute residuals (default=20)', metavar='NUMBER') parser.add_option('-p', '--plot', dest='plot', action='store_true', default=False, help='save residuals plots to file') (options, args) = parser.parse_args() if len(args) < 1: parser.print_usage(file=sys.stderr) sys.stderr.write("\tUse '-h' for help\n\n") sys.exit(1) resdir = args[0] min_spectra = int(options.min_spectra) outdir = 'sspec_residuals' residual_dict = defaultdict(Stream) for resfile in glob(os.path.join(resdir, '*-res*.pickle')): print(resfile) with open(resfile, 'rb') as fp: residual_st = pickle.load(fp) for spec in residual_st: residual_dict[spec.id].append(spec) if not os.path.exists(outdir): os.makedirs(outdir) residual_mean = Stream() for stat_id in sorted(residual_dict.keys()): if len(residual_dict[stat_id]) < min_spectra: continue print(stat_id) res = residual_dict[stat_id] freqs_min = [spec.get_freq().min() for spec in res] freqs_max = [spec.get_freq().max() for spec in res] freq_min = min(freqs_min) freq_max = max(freqs_max) spec_mean = Spectrum() spec_mean.stats.begin = freq_min spec_mean.stats.delta = res[0].stats.delta spec_mean.stats.station = res[0].stats.station spec_mean.data_mag = None for spec in res: spec_slice = spec.slice(freq_min, freq_max, pad=True, fill_value=mag_to_moment(0)) spec_slice.data_mag = moment_to_mag(spec_slice.data) norm = (spec_slice.data_mag != 0).astype(int) if spec_mean.data_mag is None: spec_mean.data_mag = spec_slice.data_mag norm_mean = norm else: spec_mean.data_mag += spec_slice.data_mag norm_mean += norm spec_mean.data_mag /= norm_mean spec_mean.data = mag_to_moment(spec_mean.data_mag) residual_mean.append(spec_mean) # plot traces if options.plot: stnm = spec_mean.stats.station figurefile = os.path.join(outdir, stnm + '-res.png') fig = plt.figure(dpi=160) for spec in res: plt.semilogx(spec.get_freq(), spec.data_mag, 'b-') plt.semilogx(spec_mean.get_freq(), spec_mean.data_mag, 'r-') plt.xlabel('frequency (Hz)') plt.ylabel('residual amplitude (obs - synth) in magnitude units') plt.title('residuals : ' + stnm + ', ' + str(len(res)) + ' records') fig.savefig(figurefile, bbox_inches='tight') plt.close() # writes the mean residuals (the stations corrections) with open(os.path.join(outdir, 'residual_mean.pickle'), 'wb') as fp: pickle.dump(residual_mean, fp)
def _spec_inversion(config, spec, noise_weight): """Invert one spectrum.""" # azimuth computation coords = spec.stats.coords hypo = spec.stats.hypo stla = coords.latitude stlo = coords.longitude evla = hypo.latitude evlo = hypo.longitude geod = gps2dist_azimuth(evla, evlo, stla, stlo) az = geod[1] freq_log = spec.freq_log ydata = spec.data_log_mag statId = '{} {}'.format(spec.id, spec.stats.instrtype) noise_weight = noise_weight.data_log if config.weighting == 'noise': weight = noise_weight # 'curve_fit' interprets 'yerr' as standard deviation vector # and calculates weights as 1/yerr^2 . # Therefore we build yerr as: yerr = 1. / np.sqrt(weight) elif config.weighting == 'frequency': # frequency weighting: # config.weight for f<=f_weight # 1 for f> f_weight yerr = np.ones(len(ydata)) yerr[freq_log <= config.f_weight] = 1. / math.sqrt(config.weight) weight = 1. / np.power(yerr, 2) elif config.weighting is None: weight = yerr = np.ones(len(ydata)) # Find the frequency range to compute Mw_0 and, possibly, t_star_0: # we start where signal-to-noise becomes strong idx0 = np.where(noise_weight > 0.5)[0][0] # we stop at the first max of signal-to-noise (proxy for fc) idx_max = argrelmax(noise_weight)[0] # just keep the indexes for maxima > 0.5 idx_max = [idx for idx in idx_max if noise_weight[idx] > 0.5] if not idx_max: # if idx_max is empty, then the source and/or noise spectrum # is most certainly "strange". In this case, we simply give up. msg = '{}: unable to find a frequency range to compute Mw_0. ' msg += 'This is possibly due to an uncommon spectrum ' msg += '(e.g., a resonance).' msg = msg.format(statId) raise RuntimeError(msg) idx1 = idx_max[0] if idx1 == idx0: try: idx1 = idx_max[1] except IndexError: # if there are no other maxima, just take 5 points idx1 = idx0 + 5 # first maximum is a proxy for fc, we use it for fc_0: fc_0 = freq_log[idx1] t_star_min = t_star_max = None if config.invert_t_star_0: # fit t_star_0 and Mw on the initial part of the spectrum, # corrected for the effect of fc ydata_corr = ydata - spectral_model(freq_log, Mw=0, fc=fc_0, t_star=0) ydata_corr = smooth(ydata_corr, window_len=18) slope, Mw_0 = np.polyfit(freq_log[idx0:idx1], ydata_corr[idx0:idx1], deg=1) t_star_0 = -3. / 2 * slope / (np.pi * np.log10(np.e)) t_star_min = t_star_0 * (1 - config.t_star_0_variability) t_star_max = t_star_0 * (1 + config.t_star_0_variability) if not config.invert_t_star_0 or t_star_0 < 0: # we calculate the initial value for Mw as an average Mw_0 = np.nanmean(ydata[idx0:idx1]) t_star_0 = config.t_star_0 initial_values = InitialValues(Mw_0, fc_0, t_star_0) logger.info('{}: initial values: {}'.format(statId, str(initial_values))) bounds = Bounds(config, spec, initial_values) bounds.Mw_min = Mw_0 - config.Mw_0_variability bounds.Mw_max = Mw_0 + config.Mw_0_variability if t_star_min is not None: bounds.t_star_min = t_star_min if t_star_max is not None: bounds.t_star_max = t_star_max logger.info('{}: bounds: {}'.format(statId, str(bounds))) try: params_opt, params_err, misfit = _curve_fit(config, spec, weight, yerr, initial_values, bounds) except (RuntimeError, ValueError) as m: msg = str(m) + '\n' msg += '{}: unable to fit spectral model'.format(statId) raise RuntimeError(msg) params_name = ('Mw', 'fc', 't_star') par = OrderedDict(zip(params_name, params_opt)) par_str = '; '.join(['{}: {:.4f}'.format(key, par[key]) for key in par]) logger.info('{}: optimal values: {}'.format(statId, par_str)) logger.info('{}: misfit: {:.3f}'.format(statId, misfit)) if np.isclose(par['fc'], bounds.fc_min, rtol=0.1): msg = '{}: optimal fc within 10% of fc_min: {:.3f} ~= {:.3f}: ' msg += 'ignoring inversion results' msg = msg.format(statId, par['fc'], bounds.fc_min) raise ValueError(msg) if np.isclose(par['fc'], bounds.fc_max, rtol=1e-4): msg = '{}: optimal fc within 10% of fc_max: {:.3f} ~= {:.3f}: ' msg += 'ignoring inversion results' msg = msg.format(statId, par['fc'], bounds.fc_max) raise ValueError(msg) misfit_max = config.pi_misfit_max or np.inf if misfit > misfit_max: msg = '{}: misfit larger than pi_misfit_max: {:.3f} > {:.3f}: ' msg += 'ignoring inversion results' msg = msg.format(statId, misfit, misfit_max) raise ValueError(msg) # Check post-inversion bounds for t_star and fc t_star = par['t_star'] pi_t_star_min, pi_t_star_max =\ config.pi_t_star_min_max or (-np.inf, np.inf) if not (pi_t_star_min <= t_star <= pi_t_star_max): msg = '{}: t_star: {:.3f} not in allowed range [{:.3f}, {:.3f}]: ' msg += 'ignoring inversion results' msg = msg.format(statId, t_star, pi_t_star_min, pi_t_star_max) raise ValueError(msg) fc = par['fc'] pi_fc_min, pi_fc_max = config.pi_fc_min_max or (-np.inf, np.inf) if not (pi_fc_min <= fc <= pi_fc_max): msg = '{}: fc: {:.3f} not in allowed range [{:.3f}, {:.3f}]: ' msg += 'ignoring inversion results' msg = msg.format(statId, fc, pi_fc_min, pi_fc_max) raise ValueError(msg) par['hyp_dist'] = spec.stats.hypo_dist par['epi_dist'] = spec.stats.epi_dist par['az'] = az par['lon'] = spec.stats.coords.longitude par['lat'] = spec.stats.coords.latitude # additional parameters, computed from fc, Mw and t_star vs = config.hypo.vs # See if there is a travel-time vs defined vs_tt = config.vs_tt or vs # seismic moment par['Mo'] = mag_to_moment(par['Mw']) # source radius in meters par['ra'] = source_radius(par['fc'], vs * 1e3) # Brune stress drop in MPa par['bsd'] = bsd(par['Mo'], par['ra']) # quality factor par['Qo'] = quality_factor(par['hyp_dist'], vs_tt, par['t_star']) # Check post-inversion bounds for bsd pi_bsd_min, pi_bsd_max = config.pi_bsd_min_max or (-np.inf, np.inf) if not (pi_bsd_min <= par['bsd'] <= pi_bsd_max): msg = '{}: bsd: {:.3e} not in allowed range [{:.3e}, {:.3e}]: ' msg += 'ignoring inversion results' msg = msg.format(statId, par['bsd'], pi_bsd_min, pi_bsd_max) raise ValueError(msg) par_err = OrderedDict(zip(params_name, params_err)) # additional parameter errors, computed from fc, Mw and t_star vs = config.hypo.vs # See if there is a travel-time vs defined vs_tt = config.vs_tt or vs # seismic moment Mw_min = par['Mw'] - par_err['Mw'][0] Mw_max = par['Mw'] + par_err['Mw'][1] Mo_min = mag_to_moment(Mw_min) Mo_max = mag_to_moment(Mw_max) par_err['Mo'] = (par['Mo'] - Mo_min, Mo_max - par['Mo']) # source radius in meters fc_min = par['fc'] - par_err['fc'][0] if fc_min <= 0: fc_min = freq_log[0] fc_max = par['fc'] + par_err['fc'][1] ra_min = source_radius(fc_max, vs * 1e3) ra_max = source_radius(fc_min, vs * 1e3) par_err['ra'] = (par['ra'] - ra_min, ra_max - par['ra']) # Brune stress drop in MPa bsd_min = bsd(Mo_min, ra_max) bsd_max = bsd(Mo_max, ra_min) par_err['bsd'] = (par['bsd'] - bsd_min, bsd_max - par['bsd']) # quality factor t_star_min = par['t_star'] - par_err['t_star'][0] if t_star_min <= 0: t_star_min = 0.001 t_star_max = par['t_star'] + par_err['t_star'][1] Qo_min = quality_factor(par['hyp_dist'], vs_tt, t_star_max) Qo_max = quality_factor(par['hyp_dist'], vs_tt, t_star_min) par_err['Qo'] = (par['Qo'] - Qo_min, Qo_max - par['Qo']) return par, par_err
def write_output(config, sourcepar, sourcepar_err): """Write results to a plain text file and/or to a SQLite database file.""" if len(sourcepar) == 0: logger.info('No source parameter calculated') ssp_exit() means = dict() errors = dict() means_weight = dict() errors_weight = dict() # Compute average source parameters # Mw Mw_values = np.array([x['Mw'] for x in sourcepar.values()]) Mw_err = np.array([x['Mw'] for x in sourcepar_err.values()]) means['Mw'], errors['Mw'] = _avg_and_std(Mw_values) means_weight['Mw'], errors_weight['Mw'] = \ _avg_and_std(Mw_values, errors=Mw_err) # Mo (N.m) means['Mo'], errors['Mo'] = _M0_avg_and_std(means['Mw'], errors['Mw']) means_weight['Mo'], errors_weight['Mo'] = \ _M0_avg_and_std(means_weight['Mw'], errors_weight['Mw']) # fc , hertz fc_values = np.array([x['fc'] for x in sourcepar.values()]) fc_err = np.array([x['fc'] for x in sourcepar_err.values()]) means['fc'], errors['fc'] = _avg_and_std(fc_values, logarithmic=True) means_weight['fc'], errors_weight['fc'] = \ _avg_and_std(fc_values, errors=fc_err, logarithmic=True) # t_star t_star_values = np.array([x['t_star'] for x in sourcepar.values()]) t_star_err = np.array([x['t_star'] for x in sourcepar_err.values()]) means['t_star'], errors['t_star'] = _avg_and_std(t_star_values) means_weight['t_star'], errors_weight['t_star'] = \ _avg_and_std(t_star_values, errors=t_star_err) # ra, radius (meters) vs_m = config.hypo.vs * 1000 ra_values = 0.37 * vs_m / fc_values means['ra'], errors['ra'] = _avg_and_std(ra_values, logarithmic=True) # bsd, Brune stress drop (MPa) Mo_values = mag_to_moment(Mw_values) bsd_values = 0.4375 * Mo_values / np.power(ra_values, 3) * 1e-6 means['bsd'], errors['bsd'] = _avg_and_std(bsd_values, logarithmic=True) # Ml # build Ml_values: use np.nan for missing values Ml_values = np.array([x.get('Ml', np.nan) for x in sourcepar.values()]) Ml_values = Ml_values[~np.isnan(Ml_values)] if Ml_values.size: means['Ml'], errors['Ml'] = _avg_and_std(Ml_values) else: means['Ml'] = None errors['Ml'] = None # Er Er_values = np.array([x['Er'] for x in sourcepar.values()]) means['Er'], errors['Er'] = _avg_and_std(Er_values, logarithmic=True) sourcepar['means'] = means sourcepar['errors'] = errors sourcepar['means_weight'] = means_weight sourcepar['errors_weight'] = errors_weight # Write to parfile _write_parfile(config, sourcepar, sourcepar_err) # Write to database, if requested _write_db(config, sourcepar) # Write to hypo file, if requested _write_hypo(config, sourcepar) params_name = ('Mw', 'fc', 't_star') sourcepar_mean = dict( zip(params_name, [means['Mw'], means['fc'], means['t_star']])) logger.info('params_mean: {}'.format(sourcepar_mean)) sourcepar_mean_weight = dict( zip(params_name, [means_weight['Mw'], means_weight['fc'], means_weight['t_star']])) logger.info('params_mean_weighted: {}'.format(sourcepar_mean_weight)) return sourcepar_mean