def test_matlab_range(): """ Tests the Matlab range command. """ np.testing.assert_array_equal(utils.matlab_range(0, 5, 1), np.arange(6)) np.testing.assert_array_equal(utils.matlab_range(0, 5.5, 1), np.arange(6)) np.testing.assert_array_equal(utils.matlab_range(0, 4.9, 1), np.arange(5))
def time_frequency_cc_difference(t, s1, s2, dt_new, width, threshold): """ Straight port of tfa_cc_new.m :param t: discrete time :param s1: discrete signal 1 :param s2: discrete signal 2 :param dt_new: time increment in the tf domain :param width: width of the Gaussian window :param threshold: fraction of the absolute signal below which the Fourier transform is set to zero in order to reduce computation time """ # New time axis ti = utils.matlab_range(t[0], t[-1], dt_new) # Interpolate both signals to the new time axis si1 = interp1d(t, s1, kind=1)(ti) si2 = interp1d(t, s2, kind=1)(ti) # Extend the time axis, required for the correlation N = len(ti) t_cc = utils.matlab_range(t[0], (2 * N - 2) * dt_new, dt_new) # Rename some variables s1 = si1 s2 = si2 t_min = t_cc[0] # Initialize the meshgrid N = len(t_cc) dnu = 1.0 / (N * dt_new) nu = utils.matlab_range(0, float(N - 1) / (N * dt_new), dnu) tau = t_cc TAU, NU = np.meshgrid(tau, nu) # Compute the time frequency representation tfs = np.zeros(NU.shape, dtype="complex128") for k in xrange(len(tau)): # Window the signals w = utils.gaussian_window(ti - tau[k], width) f1 = w * s1 f2 = w * s2 if np.abs(f1).max() > (threshold * np.abs(s1).max()): cc = utils.cross_correlation(f2, f1) tfs[k, :] = np.fft.fft(cc) / np.sqrt(2.0 * np.pi) * dt_new tfs[k, :] = tfs[k, :] * np.exp(-2.0 * np.pi * 1j * t_min * nu) else: tfs[k, :] = 0.0 return TAU, NU, tfs
def time_frequency_transform(t, s, dt_new, width, threshold): """ :param t: discrete time :param s: discrete signal :param dt_new: time increment in the tf domain :param width: width of the Gaussian window :param threshold: fraction of the absolute signal below which the Fourier transform is set to zero in order to reduce computation time """ # New time axis ti = utils.matlab_range(t[0], t[-1], dt_new) # Interpolate both signals to the new time axis si = interp1d(t, s, kind=1)(ti) # Rename some variables t = ti s = si t_min = t[0] # Initialize the meshgrid N = len(t) nu = np.linspace(0, float(N - 1) / (N * dt_new), len(t)) tau = t TAU, NU = np.meshgrid(tau, nu) # Compute the time frequency representation tfs = np.zeros(NU.shape, dtype="complex128") for k in xrange(len(tau)): # Window the signals w = utils.gaussian_window(t - tau[k], width) f = w * s if np.abs(f).max() > (threshold * np.abs(s).max()): tfs[k, :] = np.fft.fft(f) / np.sqrt(2.0 * np.pi) * dt_new tfs[k, :] = tfs[k, :] * np.exp(-2.0 * np.pi * 1j * t_min * nu) else: tfs[k, :] = 0.0 return TAU, NU, tfs
def adsrc_tf_phase_misfit(t, data, synthetic, min_period, max_period, plot=False): """ :rtype: dictionary :returns: Return a dictionary with three keys: * adjoint_source: The calculated adjoint source as a numpy array * misfit: The misfit value * messages: A list of strings giving additional hints to what happened in the calculation. """ # Assumes that t starts at 0. Pad your data if that is not the case - # Parts with zeros are essentially skipped making it fairly efficient. assert t[0] == 0 messages = [] # Internal sampling interval. Some explanations for this "magic" number. # LASIF's preprocessing allows no frequency content with smaller periods # than min_period / 2.2 (see function_templates/preprocesssing_function.py # for details). Assuming most users don't change this, this is equal to # the Nyquist frequency and the largest possible sampling interval to # catch everything is min_period / 4.4. # # The current choice is historic as changing does (very slightly) chance # the calculated misfit and we don't want to disturb inversions in # progress. The difference is likely minimal in any case. We might have # same aliasing into the lower frequencies but the filters coupled with # the TF-domain weighting will get rid of them in essentially all # realistically occurring cases. dt_new = max(float(int(min_period / 3.0)), t[1] - t[0]) # New time axis ti = utils.matlab_range(t[0], t[-1], dt_new) # Make sure its odd - that avoid having to deal with some issues # regarding frequency bin interpolation. Now positive and negative # frequencies will always be all symmetric. Data is assumed to be # tapered in any case so no problem are to be expected. if not len(ti) % 2: ti = ti[:-1] # Interpolate both signals to the new time axis - this massively speeds # up the whole procedure as most signals are highly oversampled. The # adjoint source at the end is re-interpolated to the original sampling # points. original_data = data original_synthetic = synthetic data = lanczos_interpolation( data=data, old_start=t[0], old_dt=t[1] - t[0], new_start=t[0], new_dt=dt_new, new_npts=len(ti), a=8, window="blackmann") synthetic = lanczos_interpolation( data=synthetic, old_start=t[0], old_dt=t[1] - t[0], new_start=t[0], new_dt=dt_new, new_npts=len(ti), a=8, window="blackmann") original_time = t t = ti # ------------------------------------------------------------------------- # Compute time-frequency representations # Window width is twice the minimal period. width = 2.0 * min_period # Compute time-frequency representation of the cross-correlation _, _, tf_cc = time_frequency.time_frequency_cc_difference( t, data, synthetic, width) # Compute the time-frequency representation of the synthetic tau, nu, tf_synth = time_frequency.time_frequency_transform(t, synthetic, width) # ------------------------------------------------------------------------- # compute tf window and weighting function # noise taper: down-weight tf amplitudes that are very low tf_cc_abs = np.abs(tf_cc) m = tf_cc_abs.max() / 10.0 # NOQA weight = ne.evaluate("1.0 - exp(-(tf_cc_abs ** 2) / (m ** 2))") nu_t = nu.T # highpass filter (periods longer than max_period are suppressed # exponentially) weight *= (1.0 - np.exp(-(nu_t * max_period) ** 2)) # lowpass filter (periods shorter than min_period are suppressed # exponentially) nu_t_large = np.zeros(nu_t.shape) nu_t_small = np.zeros(nu_t.shape) thres = (nu_t <= 1.0 / min_period) nu_t_large[np.invert(thres)] = 1.0 nu_t_small[thres] = 1.0 weight *= (np.exp(-10.0 * np.abs(nu_t * min_period - 1.0)) * nu_t_large + nu_t_small) # normalisation weight /= weight.max() # computation of phase difference, make quality checks and misfit --------- # Compute the phase difference. # DP = np.imag(np.log(m + tf_cc / (2 * m + np.abs(tf_cc)))) DP = np.angle(tf_cc) # Attempt to detect phase jumps by taking the derivatives in time and # frequency direction. 0.7 is an emperical value. abs_weighted_DP = np.abs(weight * DP) _x = abs_weighted_DP.max() # NOQA test_field = ne.evaluate("weight * DP / _x") criterion_1 = np.sum([np.abs(np.diff(test_field, axis=0)) > 0.7]) criterion_2 = np.sum([np.abs(np.diff(test_field, axis=1)) > 0.7]) criterion = np.sum([criterion_1, criterion_2]) if criterion > 7.0: warning = ("Possible phase jump detected. Misfit included. No " "adjoint source computed.") warnings.warn(warning) messages.append(warning) # Compute the phase misfit dnu = nu[1] - nu[0] i = ne.evaluate("sum(weight ** 2 * DP ** 2)") phase_misfit = np.sqrt(i * dt_new * dnu) # Sanity check. Should not occur. if np.isnan(phase_misfit): msg = "The phase misfit is NaN." raise LASIFAdjointSourceCalculationError(msg) # compute the adjoint source when no phase jump detected ------------------ if criterion <= 7.0: # Make kernel for the inverse tf transform idp = ne.evaluate( "weight ** 2 * DP * tf_synth / (m + abs(tf_synth) ** 2)") # Invert tf transform and make adjoint source ad_src, it, I = time_frequency.itfa(tau, idp, width) # Interpolate both signals to the new time axis ad_src = lanczos_interpolation( # Pad with a couple of zeros in case some where lost in all # these resampling operations. The first sample should not # change the time. data=np.concatenate([ad_src.imag, np.zeros(100)]), old_start=tau[0], old_dt=tau[1] - tau[0], new_start=original_time[0], new_dt=original_time[1] - original_time[0], new_npts=len(original_time), a=8, window="blackmann") # Divide by the misfit and change sign. ad_src /= (phase_misfit + eps) ad_src = -1.0 * np.diff(ad_src) / (t[1] - t[0]) # Taper at both ends. Exploit ObsPy to not have to deal with all the # nasty things. ad_src = \ obspy.Trace(ad_src).taper(max_percentage=0.05, type="hann").data # Reverse time and add a leading zero so the adjoint source has the # same length as the input time series. ad_src = ad_src[::-1] ad_src = np.concatenate([[0.0], ad_src]) else: # Criterion failed, no misfit and adjoint source calculated. raise LASIFAdjointSourceCalculationError( "Criterion failed, no misfit has been calculated.") # Plot if requested. ------------------------------------------------------ if plot: import matplotlib as mpl import matplotlib.pyplot as plt plt.style.use("seaborn-whitegrid") from lasif.colors import get_colormap if isinstance(plot, mpl.figure.Figure): fig = plot else: fig = plt.gcf() # Manually set-up the axes for full control. l, b, w, h = 0.1, 0.05, 0.80, 0.22 rect = l, b + 3 * h, w, h waveforms_axis = fig.add_axes(rect) rect = l, b + h, w, 2 * h tf_axis = fig.add_axes(rect) rect = l, b, w, h adj_src_axis = fig.add_axes(rect) rect = l + w + 0.02, b, 1.0 - (l + w + 0.02) - 0.05, 4 * h cm_axis = fig.add_axes(rect) # Plot the weighted phase difference. weighted_phase_difference = (DP * weight).transpose() mappable = tf_axis.pcolormesh( tau, nu, weighted_phase_difference, vmin=-1.0, vmax=1.0, cmap=get_colormap("tomo_full_scale_linear_lightness_r"), shading="gouraud", zorder=-10) tf_axis.grid(True) tf_axis.grid(True, which='minor', axis='both', linestyle='-', color='k') cm = fig.colorbar(mappable, cax=cm_axis) cm.set_label("Phase difference in radian", fontsize="large") # Various texts on the time frequency domain plot. text = "Misfit: %.4f" % phase_misfit tf_axis.text(x=0.99, y=0.02, s=text, transform=tf_axis.transAxes, fontsize="large", color="#C25734", fontweight=900, verticalalignment="bottom", horizontalalignment="right") txt = "Weighted Phase Difference - red is a phase advance of the " \ "synthetics" tf_axis.text(x=0.99, y=0.95, s=txt, fontsize="large", color="0.1", transform=tf_axis.transAxes, verticalalignment="top", horizontalalignment="right") if messages: message = "\n".join(messages) tf_axis.text(x=0.99, y=0.98, s=message, transform=tf_axis.transAxes, bbox=dict(facecolor='red', alpha=0.8), verticalalignment="top", horizontalalignment="right") # Adjoint source. adj_src_axis.plot(original_time, ad_src[::-1], color="0.1", lw=2, label="Adjoint source (non-time-reversed)") adj_src_axis.legend() # Waveforms. waveforms_axis.plot(original_time, original_data, color="0.1", lw=2, label="Observed") waveforms_axis.plot(original_time, original_synthetic, color="#C11E11", lw=2, label="Synthetic") waveforms_axis.legend() # Set limits for all axes. tf_axis.set_ylim(0, 2.0 / min_period) tf_axis.set_xlim(0, tau[-1]) adj_src_axis.set_xlim(0, tau[-1]) waveforms_axis.set_xlim(0, tau[-1]) waveforms_axis.set_ylabel("Velocity [m/s]", fontsize="large") tf_axis.set_ylabel("Period [s]", fontsize="large") adj_src_axis.set_xlabel("Seconds since event", fontsize="large") # Hack to keep ticklines but remove the ticks - there is probably a # better way to do this. waveforms_axis.set_xticklabels([ "" for _i in waveforms_axis.get_xticks()]) tf_axis.set_xticklabels(["" for _i in tf_axis.get_xticks()]) _l = tf_axis.get_ylim() _r = _l[1] - _l[0] _t = tf_axis.get_yticks() _t = _t[(_l[0] + 0.1 * _r < _t) & (_t < _l[1] - 0.1 * _r)] tf_axis.set_yticks(_t) tf_axis.set_yticklabels(["%.1fs" % (1.0 / _i) for _i in _t]) waveforms_axis.get_yaxis().set_label_coords(-0.08, 0.5) tf_axis.get_yaxis().set_label_coords(-0.08, 0.5) fig.suptitle("Time Frequency Phase Misfit and Adjoint Source", fontsize="xx-large") ret_dict = { "adjoint_source": ad_src, "misfit_value": phase_misfit, "details": {"messages": messages} } return ret_dict
def adsrc_tf_phase_misfit(t, data, synthetic, min_period, max_period, plot=False, max_criterion=7.0): """ :rtype: dictionary :returns: Return a dictionary with three keys: * adjoint_source: The calculated adjoint source as a numpy array * misfit: The misfit value * messages: A list of strings giving additional hints to what happened in the calculation. """ # Assumes that t starts at 0. Pad your data if that is not the case - # Parts with zeros are essentially skipped making it fairly efficient. assert t[0] == 0 messages = [] # Internal sampling interval. Some explanations for this "magic" number. # LASIF's preprocessing allows no frequency content with smaller periods # than min_period / 2.2 (see function_templates/preprocesssing_function.py # for details). Assuming most users don't change this, this is equal to # the Nyquist frequency and the largest possible sampling interval to # catch everything is min_period / 4.4. # # The current choice is historic as changing does (very slightly) chance # the calculated misfit and we don't want to disturb inversions in # progress. The difference is likely minimal in any case. We might have # same aliasing into the lower frequencies but the filters coupled with # the TF-domain weighting will get rid of them in essentially all # realistically occurring cases. dt_new = max(float(int(min_period / 3.0)), t[1] - t[0]) # New time axis ti = utils.matlab_range(t[0], t[-1], dt_new) # Make sure its odd - that avoid having to deal with some issues # regarding frequency bin interpolation. Now positive and negative # frequencies will always be all symmetric. Data is assumed to be # tapered in any case so no problem are to be expected. if not len(ti) % 2: ti = ti[:-1] # Interpolate both signals to the new time axis - this massively speeds # up the whole procedure as most signals are highly oversampled. The # adjoint source at the end is re-interpolated to the original sampling # points. original_data = data original_synthetic = synthetic data = lanczos_interpolation(data=data, old_start=t[0], old_dt=t[1] - t[0], new_start=t[0], new_dt=dt_new, new_npts=len(ti), a=8, window="blackmann") synthetic = lanczos_interpolation(data=synthetic, old_start=t[0], old_dt=t[1] - t[0], new_start=t[0], new_dt=dt_new, new_npts=len(ti), a=8, window="blackmann") original_time = t t = ti # ------------------------------------------------------------------------- # Compute time-frequency representations # Window width is twice the minimal period. width = 2.0 * min_period # Compute time-frequency representation of the cross-correlation _, _, tf_cc = time_frequency.time_frequency_cc_difference( t, data, synthetic, width) # Compute the time-frequency representation of the synthetic tau, nu, tf_synth = time_frequency.time_frequency_transform( t, synthetic, width) # ------------------------------------------------------------------------- # compute tf window and weighting function # noise taper: down-weight tf amplitudes that are very low tf_cc_abs = np.abs(tf_cc) m = tf_cc_abs.max() / 10.0 # NOQA weight = ne.evaluate("1.0 - exp(-(tf_cc_abs ** 2) / (m ** 2))") nu_t = nu.T # highpass filter (periods longer than max_period are suppressed # exponentially) weight *= (1.0 - np.exp(-(nu_t * max_period)**2)) # lowpass filter (periods shorter than min_period are suppressed # exponentially) nu_t_large = np.zeros(nu_t.shape) nu_t_small = np.zeros(nu_t.shape) thres = (nu_t <= 1.0 / min_period) nu_t_large[np.invert(thres)] = 1.0 nu_t_small[thres] = 1.0 weight *= (np.exp(-10.0 * np.abs(nu_t * min_period - 1.0)) * nu_t_large + nu_t_small) # normalisation weight /= weight.max() # computation of phase difference, make quality checks and misfit --------- # Compute the phase difference. # DP = np.imag(np.log(m + tf_cc / (2 * m + np.abs(tf_cc)))) DP = np.angle(tf_cc) # Attempt to detect phase jumps by taking the derivatives in time and # frequency direction. 0.7 is an emperical value. abs_weighted_DP = np.abs(weight * DP) _x = abs_weighted_DP.max() # NOQA test_field = ne.evaluate("weight * DP / _x") criterion_1 = np.sum([np.abs(np.diff(test_field, axis=0)) > 0.7]) criterion_2 = np.sum([np.abs(np.diff(test_field, axis=1)) > 0.7]) criterion = np.sum([criterion_1, criterion_2]) # Compute the phase misfit dnu = nu[1] - nu[0] i = ne.evaluate("sum(weight ** 2 * DP ** 2)") phase_misfit = np.sqrt(i * dt_new * dnu) # Sanity check. Should not occur. if np.isnan(phase_misfit): msg = "The phase misfit is NaN." raise LASIFAdjointSourceCalculationError(msg) # The misfit can still be computed, even if not adjoint source is # available. if criterion > max_criterion: warning = ("Possible phase jump detected. Misfit included. No " "adjoint source computed. Criterion: %.1f - Max allowed " "criterion: %.1f" % (criterion, max_criterion)) warnings.warn(warning) messages.append(warning) ret_dict = { "adjoint_source": None, "misfit_value": phase_misfit, "details": { "messages": messages } } return ret_dict # Make kernel for the inverse tf transform idp = ne.evaluate("weight ** 2 * DP * tf_synth / (m + abs(tf_synth) ** 2)") # Invert tf transform and make adjoint source ad_src, it, I = time_frequency.itfa(tau, idp, width) # Interpolate both signals to the new time axis ad_src = lanczos_interpolation( # Pad with a couple of zeros in case some where lost in all # these resampling operations. The first sample should not # change the time. data=np.concatenate([ad_src.imag, np.zeros(100)]), old_start=tau[0], old_dt=tau[1] - tau[0], new_start=original_time[0], new_dt=original_time[1] - original_time[0], new_npts=len(original_time), a=8, window="blackmann") # Divide by the misfit and change sign. ad_src /= (phase_misfit + eps) ad_src = -1.0 * np.diff(ad_src) / (t[1] - t[0]) # Taper at both ends. Exploit ObsPy to not have to deal with all the # nasty things. ad_src = \ obspy.Trace(ad_src).taper(max_percentage=0.05, type="hann").data # Reverse time and add a leading zero so the adjoint source has the # same length as the input time series. ad_src = ad_src[::-1] ad_src = np.concatenate([[0.0], ad_src]) # Plot if requested. ------------------------------------------------------ if plot: import matplotlib as mpl import matplotlib.pyplot as plt plt.style.use("seaborn-whitegrid") from lasif.colors import get_colormap if isinstance(plot, mpl.figure.Figure): fig = plot else: fig = plt.gcf() # Manually set-up the axes for full control. l, b, w, h = 0.1, 0.05, 0.80, 0.22 rect = l, b + 3 * h, w, h waveforms_axis = fig.add_axes(rect) rect = l, b + h, w, 2 * h tf_axis = fig.add_axes(rect) rect = l, b, w, h adj_src_axis = fig.add_axes(rect) rect = l + w + 0.02, b, 1.0 - (l + w + 0.02) - 0.05, 4 * h cm_axis = fig.add_axes(rect) # Plot the weighted phase difference. weighted_phase_difference = (DP * weight).transpose() mappable = tf_axis.pcolormesh( tau, nu, weighted_phase_difference, vmin=-1.0, vmax=1.0, cmap=get_colormap("tomo_full_scale_linear_lightness_r"), shading="gouraud", zorder=-10) tf_axis.grid(True) tf_axis.grid(True, which='minor', axis='both', linestyle='-', color='k') cm = fig.colorbar(mappable, cax=cm_axis) cm.set_label("Phase difference in radian", fontsize="large") # Various texts on the time frequency domain plot. text = "Misfit: %.4f" % phase_misfit tf_axis.text(x=0.99, y=0.02, s=text, transform=tf_axis.transAxes, fontsize="large", color="#C25734", fontweight=900, verticalalignment="bottom", horizontalalignment="right") txt = "Weighted Phase Difference - red is a phase advance of the " \ "synthetics" tf_axis.text(x=0.99, y=0.95, s=txt, fontsize="large", color="0.1", transform=tf_axis.transAxes, verticalalignment="top", horizontalalignment="right") if messages: message = "\n".join(messages) tf_axis.text(x=0.99, y=0.98, s=message, transform=tf_axis.transAxes, bbox=dict(facecolor='red', alpha=0.8), verticalalignment="top", horizontalalignment="right") # Adjoint source. adj_src_axis.plot(original_time, ad_src[::-1], color="0.1", lw=2, label="Adjoint source (non-time-reversed)") adj_src_axis.legend() # Waveforms. waveforms_axis.plot(original_time, original_data, color="0.1", lw=2, label="Observed") waveforms_axis.plot(original_time, original_synthetic, color="#C11E11", lw=2, label="Synthetic") waveforms_axis.legend() # Set limits for all axes. tf_axis.set_ylim(0, 2.0 / min_period) tf_axis.set_xlim(0, tau[-1]) adj_src_axis.set_xlim(0, tau[-1]) waveforms_axis.set_xlim(0, tau[-1]) waveforms_axis.set_ylabel("Velocity [m/s]", fontsize="large") tf_axis.set_ylabel("Period [s]", fontsize="large") adj_src_axis.set_xlabel("Seconds since event", fontsize="large") # Hack to keep ticklines but remove the ticks - there is probably a # better way to do this. waveforms_axis.set_xticklabels( ["" for _i in waveforms_axis.get_xticks()]) tf_axis.set_xticklabels(["" for _i in tf_axis.get_xticks()]) _l = tf_axis.get_ylim() _r = _l[1] - _l[0] _t = tf_axis.get_yticks() _t = _t[(_l[0] + 0.1 * _r < _t) & (_t < _l[1] - 0.1 * _r)] tf_axis.set_yticks(_t) tf_axis.set_yticklabels(["%.1fs" % (1.0 / _i) for _i in _t]) waveforms_axis.get_yaxis().set_label_coords(-0.08, 0.5) tf_axis.get_yaxis().set_label_coords(-0.08, 0.5) fig.suptitle("Time Frequency Phase Misfit and Adjoint Source", fontsize="xx-large") ret_dict = { "adjoint_source": ad_src, "misfit_value": phase_misfit, "details": { "messages": messages } } return ret_dict