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 _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 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 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 _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 _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