Exemple #1
0
    def test_lanczos_interpolation_units(self):
        """
        Regression test for a bug that manifested when the original sampling
        rate is not 1 Hertz and new and old start times are not identical.
        """
        # Generate a highly oversampled sine curve. Upsample and downsample
        # it again and it should not change a lot except at the boundaries.
        # Also shift a bit to trigger the bug.
        original_dt = 13.333
        new_dt = 17.23

        data = np.sin(np.linspace(0, 2 * np.pi, 1000))

        output = lanczos_interpolation(data,
                                       old_dt=original_dt,
                                       new_start=10 * original_dt,
                                       old_start=0.0,
                                       a=20,
                                       new_dt=new_dt,
                                       new_npts=int(990 * original_dt /
                                                    new_dt))
        output = lanczos_interpolation(
            output,
            old_dt=new_dt,
            new_start=10 * original_dt,
            old_start=0.0,
            a=20,
            new_dt=original_dt,
            new_npts=int(980 * original_dt / new_dt) - 1)

        np.testing.assert_allclose(data[220:620],
                                   output[200:600],
                                   atol=1E-4,
                                   rtol=1E-4)
Exemple #2
0
    def test_lanczos_interpolation_units(self):
        """
        Regression test for a bug that manifested when the original sampling
        rate is not 1 Hertz and new and old start times are not identical.
        """
        # Generate a highly oversampled sine curve. Upsample and downsample
        # it again and it should not change a lot except at the boundaries.
        # Also shift a bit to trigger the bug.
        original_dt = 13.333
        new_dt = 17.23

        data = np.sin(np.linspace(0, 2 * np.pi, 1000))

        output = lanczos_interpolation(
            data,
            old_dt=original_dt,
            new_start=10 * original_dt,
            old_start=0.0,
            a=20,
            new_dt=new_dt,
            new_npts=int(990 * original_dt / new_dt),
        )
        output = lanczos_interpolation(
            output,
            old_dt=new_dt,
            new_start=10 * original_dt,
            old_start=0.0,
            a=20,
            new_dt=original_dt,
            new_npts=int(980 * original_dt / new_dt) - 1,
        )

        np.testing.assert_allclose(data[220:620], output[200:600], atol=1e-4, rtol=1e-4)
Exemple #3
0
    def test_lanczos_interpolation(self):
        """
        Tests against the instaseis implementation which should work well
        enough.
        """
        data = np.array([
            0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
            0.5955447, 0.96451452, 0.6531771, 0.74890664, 0.65356987
        ])
        dt = 1.0

        # Scenario 1.
        new_dt = 0.45
        a = 1

        expected_output = np.array([
            0.92961609, 0.55712768, 0.31720733, 0.24275977, 0.17825931,
            0.16750234, 0.17561933, 0.20626905, 0.37726064, 0.5647072,
            0.47145546, 0.59222238, 0.58665834, 0.91241347, 0.79909224,
            0.61631275, 0.61258393, 0.61611633, 0.73239733, 0.56371682,
            0.65356987
        ])

        output = lanczos_interpolation(data,
                                       old_dt=dt,
                                       new_start=0.0,
                                       old_start=0.0,
                                       new_dt=new_dt,
                                       new_npts=21,
                                       a=a)
        np.testing.assert_allclose(output, expected_output, atol=1E-9)

        # Scenario 2.
        new_dt = 0.72
        a = 12

        expected_output = np.array([
            0.92961609, 0.54632548, 0.14335148, 0.19675436, 0.19030867,
            0.41722415, 0.60644459, 0.6018648, 0.88751628, 0.90970863,
            0.58602723, 0.71521445, 0.83288791
        ])

        output = lanczos_interpolation(data,
                                       old_dt=dt,
                                       new_start=0.0,
                                       old_start=0.0,
                                       new_dt=new_dt,
                                       new_npts=13,
                                       a=a)
        np.testing.assert_allclose(output, expected_output, atol=1E-9)
def process_trace(trace, taper, sos, pad_factor, old_t0, old_sampling_rate,
                  new_sampling_rate, new_npts):

    # taper
    trace *= taper

    # pad
    n = len(trace)
    n_pad = int(pad_factor * n)
    padded_trace = np.zeros(n_pad)
    padded_trace[n_pad // 2:n_pad // 2 + n] = trace

    # filter
    if process_filter['zerophase']:
        firstpass = sosfilt(sos, padded_trace)
        padded_trace = sosfilt(sos, firstpass[::-1])[::-1]
    else:
        padded_trace = sosfilt(sos, padded_trace)
    # undo padding
    trace = padded_trace[n_pad // 2:n_pad // 2 + n]
    trace = np.asarray(trace, order='C')
    trace = lanczos_interpolation(trace,
                                  old_start=old_t0,
                                  old_dt=1. / old_sampling_rate,
                                  new_start=old_t0,
                                  new_dt=1. / new_sampling_rate,
                                  new_npts=new_npts,
                                  a=process_decimate['lanczos_window_width'],
                                  window='lanczos')
    return (trace)
Exemple #5
0
    def test_lanczos_interpolation(self):
        """
        Tests against the instaseis implementation which should work well
        enough.
        """
        data = np.array([0.92961609, 0.31637555, 0.18391881, 0.20456028,
                         0.56772503, 0.5955447, 0.96451452, 0.6531771,
                         0.74890664, 0.65356987])
        dt = 1.0

        # Scenario 1.
        new_dt = 0.45
        a = 1

        expected_output = np.array([
            0.92961609, 0.55712768, 0.31720733, 0.24275977, 0.17825931,
            0.16750234, 0.17561933, 0.20626905, 0.37726064, 0.5647072,
            0.47145546, 0.59222238, 0.58665834, 0.91241347, 0.79909224,
            0.61631275, 0.61258393, 0.61611633, 0.73239733, 0.56371682,
            0.65356987])

        output = lanczos_interpolation(
            data, old_dt=dt, new_start=0.0, old_start=0.0, new_dt=new_dt,
            new_npts=21, a=a)
        np.testing.assert_allclose(output, expected_output, atol=1E-9)

        # Scenario 2.
        new_dt = 0.72
        a = 12

        expected_output = np.array([
            0.92961609, 0.54632548, 0.14335148, 0.19675436, 0.19030867,
            0.41722415, 0.60644459, 0.6018648,  0.88751628, 0.90970863,
            0.58602723, 0.71521445, 0.83288791])

        output = lanczos_interpolation(
            data, old_dt=dt, new_start=0.0, old_start=0.0, new_dt=new_dt,
            new_npts=13, a=a)
        np.testing.assert_allclose(output, expected_output, atol=1E-9)
Exemple #6
0
    def get_seismograms_finite_source(self, sources, receiver,
                                      components=None,
                                      kind='displacement', dt=None,
                                      kernelwidth=12, correct_mu=False,
                                      progress_callback=None):
        """
        Extract seismograms for a finite source from an Instaseis database.

        :param sources: A collection of point sources.
        :type sources: :class:`~instaseis.source.FiniteSource` or list of
            :class:`~instaseis.source.Source` objects.
        :param receiver: The seismic receiver.
        :type receiver: :class:`instaseis.source.Receiver`
        :type components: tuple of str, optional
        :param components: Which components to calculate. Must be a tuple
            containing any combination of ``"Z"``, ``"N"``, ``"E"``,
            ``"R"``, and ``"T"``. Defaults to ``["Z", "N", "E"]`` for two
            component databases, to ``["N", "E"]`` for horizontal only
            databases, and to ``["Z"]`` for vertical only databases.
        :type kind: str, optional
        :param kind: The desired units of the seismogram:
            ``"displacement"``, ``"velocity"``, or ``"acceleration"``.
        :type dt: float, optional
        :param dt: Desired sampling rate of the seismograms. Resampling is done
            using a Lanczos kernel.
        :type kernelwidth: int, optional
        :param kernelwidth: The width of the sinc kernel used for resampling in
            terms of the original sampling interval. Best choose something
            between 10 and 20.
        :type correct_mu: bool, optional
        :param correct_mu: Correct the source magnitude for the actual shear
            modulus from the model.
        :type progress_callback: function, optional
        :param progress_callback: Optional callback function that will be
            called with current source number and the number of total
            sources for each calculated source. Useful for integration into
            user interfaces to provide some kind of progress information. If
            the callback returns ``True``, the calculation will be cancelled.

        :returns: Multi component finite source seismogram.
        :rtype: :class:`obspy.core.stream.Stream`
        """
        if components is None:
            components = self.default_components

        if not self.info.is_reciprocal:
            raise NotImplementedError

        data_summed = {}
        count = len(sources)
        for _i, source in enumerate(sources):
            # Don't perform the diff/integration here, but after the
            # resampling later on.
            data = self.get_seismograms(
                source, receiver, components, reconvolve_stf=True,
                # Effectively results in nothing happening.
                kind=INV_KIND_MAP[STF_MAP[self.info.stf]],
                return_obspy_stream=False, remove_source_shift=False)

            if correct_mu:
                corr_fac = data["mu"] / DEFAULT_MU,
            else:
                corr_fac = 1

            for comp in components:
                if comp in data_summed:
                    data_summed[comp] += data[comp] * corr_fac
                else:
                    data_summed[comp] = data[comp] * corr_fac
            # Only used for the GUI.
            if progress_callback:  # pragma: no cover
                cancel = progress_callback(_i + 1, count)
                if cancel:
                    return None

        if dt is not None:
            for comp in components:
                # We don't need to align a sample to the peak of the source
                # time function here.
                new_npts = int(round(
                    (len(data[comp]) - 1) * self.info.dt / dt, 6) + 1)
                data_summed[comp] = lanczos_interpolation(
                    data=np.require(data_summed[comp], requirements=["C"]),
                    old_start=0, old_dt=self.info.dt, new_start=0, new_dt=dt,
                    new_npts=new_npts, a=kernelwidth, window="blackman")

                # The resampling assumes zeros outside the data range. This
                # does not introduce any errors at the beginning as the data is
                # actually zero there but it does affect the end. We will
                # remove all samples that are affected by the boundary
                # conditions here.
                #
                # Also don't cut it for the "identify" interpolation which is
                # important for testing.
                if round(dt / self.info.dt, 6) != 1.0:
                    affected_area = kernelwidth * self.info.dt
                    data_summed[comp] = \
                        data_summed[comp][:-int(np.ceil(affected_area / dt))]

        if dt is None:
            dt_out = self.info.dt
        else:
            dt_out = dt

        # Integrate/differentiate here. No need to do it for every single
        # seismogram and stack the errors.
        n_derivative = KIND_MAP[kind] - STF_MAP[self.info.stf]
        if n_derivative:
            for comp in data_summed.keys():
                _diff_and_integrate(n_derivative=n_derivative,
                                    data=data_summed, comp=comp, dt_out=dt_out)

        # Convert to an ObsPy Stream object.
        st = Stream()
        band_code = get_band_code(dt_out)
        for comp in components:
            tr = Trace(data=data_summed[comp],
                       header={"delta": dt_out,
                               "station": receiver.station,
                               "network": receiver.network,
                               "location": receiver.location,
                               "channel": "%sX%s" % (band_code, comp)})
            st += tr
        return st
Exemple #7
0
    def get_seismograms(self, source, receiver, components=None,
                        kind='displacement', remove_source_shift=True,
                        reconvolve_stf=False, return_obspy_stream=True,
                        dt=None, kernelwidth=12):
        """
        Extract seismograms from the Green's function database.

        :param source: The source definition.
        :type source: :class:`instaseis.source.Source` or
            :class:`instaseis.source.ForceSource`
        :param receiver: The seismic receiver.
        :type receiver: :class:`instaseis.source.Receiver`
        :type components: tuple of str, optional
        :param components: Which components to calculate. Must be a tuple
            containing any combination of ``"Z"``, ``"N"``, ``"E"``,
            ``"R"``, and ``"T"``. Defaults to ``["Z", "N", "E"]`` for two
            component databases, to ``["N", "E"]`` for horizontal only
            databases, and to ``["Z"]`` for vertical only databases.
        :type kind: str, optional
        :param kind: The desired units of the seismogram:
            ``"displacement"``, ``"velocity"``, or ``"acceleration"``.
        :type remove_source_shift: bool, optional
        :param remove_source_shift: Cut all samples before the peak of the
            source time function. This has the effect that the first sample
            is the origin time of the source.
        :type reconvolve_stf: bool, optional
        :param reconvolve_stf: Deconvolve the source time function used in
            the AxiSEM run and convolve with the STF attached to the source.
            For this to be stable, the new STF needs to bandlimited.
        :type return_obspy_stream: bool, optional
        :param return_obspy_stream: Return format is either an
            :class:`obspy.core.stream.Stream` object or a dictionary
            containing the raw NumPy arrays.
        :type dt: float, optional
        :param dt: Desired sampling rate of the seismograms. Resampling is done
            using a Lanczos kernel.
        :type kernelwidth: int, optional
        :param kernelwidth: The width of the sinc kernel used for resampling in
            terms of the original sampling interval. Best choose something
            between 10 and 20.

        :returns: Multi component seismograms.
        :rtype: A :class:`obspy.core.stream.Stream` object or a dictionary
            with NumPy arrays as values.
        """
        if components is None:
            components = self.default_components

        source, receiver = self._get_seismograms_sanity_checks(
            source=source, receiver=receiver, components=components,
            kind=kind, dt=dt)

        # Call the _get_seismograms() method of the respective implementation.
        data = self._get_seismograms(source=source, receiver=receiver,
                                     components=components)

        if dt is None:
            dt_out = self.info.dt
        else:
            dt_out = dt

        stf_deconv_map = {
            0: self.info.sliprate,
            1: self.info.slip}

        # Can never be negative with the current logic.
        n_derivative = KIND_MAP[kind] - STF_MAP[self.info.stf]

        if isinstance(source, ForceSource):
            n_derivative += 1

        if reconvolve_stf and remove_source_shift:
            raise ValueError("'remove_source_shift' argument not "
                             "compatible with 'reconvolve_stf'.")

        # Calculate the final time information about the seismograms.
        time_information = _get_seismogram_times(
            info=self.info, origin_time=source.origin_time, dt=dt,
            kernelwidth=kernelwidth, remove_source_shift=remove_source_shift,
            reconvolve_stf=reconvolve_stf)

        for comp in components:
            if reconvolve_stf:
                # We assume here that the sliprate is well-behaved,
                # e.g. zeros at the boundaries and no energy above the mesh
                # resolution.
                if source.dt is None or source.sliprate is None:
                    raise ValueError("source has no source time function")

                if STF_MAP[self.info.stf] not in [0, 1]:
                    raise NotImplementedError(
                        'deconvolution not implemented for stf %s'
                        % (self.info.stf))

                stf_deconv_f = np.fft.rfft(
                    stf_deconv_map[STF_MAP[self.info.stf]],
                    n=self.info.nfft)

                if abs((source.dt - self.info.dt) / self.info.dt) > 1e-7:
                    raise ValueError("dt of the source not compatible")

                stf_conv_f = np.fft.rfft(source.sliprate,
                                         n=self.info.nfft)

                if source.time_shift is not None:
                    stf_conv_f *= \
                        np.exp(- 1j * rfftfreq(self.info.nfft) *
                               2. * np.pi * source.time_shift / self.info.dt)

                # Apply a 5 percent, at least 5 samples taper at the end.
                # The first sample is guaranteed to be zero in any case.
                tlen = max(int(math.ceil(0.05 * len(data[comp]))), 5)
                taper = np.ones_like(data[comp])
                taper[-tlen:] = scipy.signal.hann(tlen * 2)[tlen:]
                dataf = np.fft.rfft(taper * data[comp], n=self.info.nfft)

                # Ensure numerical stability by not dividing with zero.
                f = stf_conv_f
                _l = np.abs(stf_deconv_f)
                _idx = np.where(_l > 0.0)
                f[_idx] /= stf_deconv_f[_idx]
                f[_l == 0] = 0 + 0j

                data[comp] = np.fft.irfft(dataf * f)[:self.info.npts]

            if dt is not None:
                data[comp] = lanczos_interpolation(
                    data=np.require(data[comp], requirements=["C"]),
                    old_start=0, old_dt=self.info.dt,
                    new_start=time_information["time_shift_at_beginning"],
                    new_dt=dt,
                    new_npts=time_information["npts_before_shift_removal"],
                    a=kernelwidth,
                    window="blackman")

            # Integrate/differentiate before removing the source shift in
            # order to reduce boundary effects at the start of the signal.
            #
            # NEVER to this before the resampling! The error can be really big.
            if n_derivative:
                _diff_and_integrate(n_derivative=n_derivative, data=data,
                                    comp=comp, dt_out=dt_out)

            # If desired, remove the samples before the peak of the source
            # time function.
            if remove_source_shift:
                data[comp] = data[comp][time_information["ref_sample"]:]

        if return_obspy_stream:
            return self._convert_to_stream(
                receiver=receiver, components=components, data=data,
                dt_out=dt_out, starttime=time_information["starttime"])
        else:
            return data
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
Exemple #10
0
def _parse_validate_and_resample_stf(request, db_info):
    """
    Parses the JSON based STF, validates it, and resamples it.

    :param request: The request.
    :param db_info: Information about the current database.
    """
    if not request.body:
        msg = ("The source time function must be given in the body of the "
               "POST request.")
        return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    # Try to parse it as a JSON file.
    with io.BytesIO(request.body) as buf:
        try:
            j = json.loads(buf.read().decode())
        except Exception:
            msg = "The body of the POST request is not a valid JSON file."
            return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    # Validate it.
    try:
        json_validate(j, _json_schema)
    except JSONValidationError as e:
        # Replace the u'' unicode string specifier for consistent error
        # messages.
        msg = "Validation Error in JSON file: " + re.sub(r"u'", "'", e.message)
        return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    # Make sure the sampling rate is ok.
    if j["sample_spacing_in_sec"] < db_info.dt:
        msg = ("'sample_spacing_in_sec' in the JSON file must not be smaller "
               "than the database dt [%.3f seconds]." % db_info.dt)
        return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    # Convert to numpy array.
    j["data"] = np.array(j["data"], np.float64)

    # A couple more custom validations.
    message = None

    # Make sure its not all zeros.
    if np.abs(j["data"]).max() < 1e-20:
        message = ("All zero (or nearly all zero) source time functions don't "
                   "make any sense.")

    # The data must begin and end with zero. The user is responsible for the
    # tapering.
    if j["data"][0] != 0.0 or j["data"][-1] != 0.0:
        message = "Must begin and end with zero."

    if message:
        msg = "STF data did not validate: %s" % message
        return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    missing_length = (db_info.length -
                      (len(j["data"]) - 1) * j["sample_spacing_in_sec"])
    missing_samples = max(
        int(missing_length / j["sample_spacing_in_sec"]) + 1, 0)

    # Add a buffer of 20 samples at the beginning and at the end.
    data = np.concatenate(
        [np.zeros(20), j["data"],
         np.zeros(missing_samples + 20)])

    # Resample it using sinc reconstruction.
    data = lanczos_interpolation(
        data,
        # Account for the additional samples at the beginning.
        old_start=-20 * j["sample_spacing_in_sec"],
        old_dt=j["sample_spacing_in_sec"],
        new_start=0.0,
        new_dt=db_info.dt,
        new_npts=db_info.npts,
        # The large a is okay because we add zeros at the beginning and the
        # end.
        a=12,
        window="blackman",
    )

    # There is potentially some numerical noise on the first sample.
    assert data[0] < 1e-10 * np.abs(data.ptp())
    data[0] = 0.0

    # Normalize the integral to one.
    data /= np.trapz(np.abs(data), dx=db_info.dt)
    j["data"] = data

    return j
Exemple #11
0
    def get_seismograms_finite_source(self, sources, receiver,
                                      components=None,
                                      kind='displacement', dt=None,
                                      kernelwidth=12, correct_mu=False,
                                      progress_callback=None):
        """
        Extract seismograms for a finite source from an Instaseis database.

        :param sources: A collection of point sources.
        :type sources: :class:`~instaseis.source.FiniteSource` or list of
            :class:`~instaseis.source.Source` objects.
        :param receiver: The seismic receiver.
        :type receiver: :class:`instaseis.source.Receiver`
        :type components: tuple of str, optional
        :param components: Which components to calculate. Must be a tuple
            containing any combination of ``"Z"``, ``"N"``, ``"E"``,
            ``"R"``, and ``"T"``. Defaults to ``["Z", "N", "E"]`` for two
            component databases, to ``["N", "E"]`` for horizontal only
            databases, and to ``["Z"]`` for vertical only databases.
        :type kind: str, optional
        :param kind: The desired units of the seismogram:
            ``"displacement"``, ``"velocity"``, or ``"acceleration"``.
        :type dt: float, optional
        :param dt: Desired sampling rate of the seismograms. Resampling is done
            using a Lanczos kernel.
        :type kernelwidth: int, optional
        :param kernelwidth: The width of the sinc kernel used for resampling in
            terms of the original sampling interval. Best choose something
            between 10 and 20.
        :type correct_mu: bool, optional
        :param correct_mu: Correct the source magnitude for the actual shear
            modulus from the model.
        :type progress_callback: function, optional
        :param progress_callback: Optional callback function that will be
            called with current source number and the number of total
            sources for each calculated source. Useful for integration into
            user interfaces to provide some kind of progress information. If
            the callback returns ``True``, the calculation will be cancelled.

        :returns: Multi component finite source seismogram.
        :rtype: :class:`obspy.core.stream.Stream`
        """
        if components is None:
            components = self.default_components

        if not self.info.is_reciprocal:
            raise NotImplementedError

        data_summed = {}
        count = len(sources)
        for _i, source in enumerate(sources):
            # Don't perform the diff/integration here, but after the
            # resampling later on.
            data = self.get_seismograms(
                source, receiver, components, reconvolve_stf=True,
                # Effectively results in nothing happening.
                kind=INV_KIND_MAP[STF_MAP[self.info.stf]],
                return_obspy_stream=False, remove_source_shift=False)

            if correct_mu:
                corr_fac = data["mu"] / DEFAULT_MU,
            else:
                corr_fac = 1

            for comp in components:
                if comp in data_summed:
                    data_summed[comp] += data[comp] * corr_fac
                else:
                    data_summed[comp] = data[comp] * corr_fac
            # Only used for the GUI.
            if progress_callback:  # pragma: no cover
                cancel = progress_callback(_i + 1, count)
                if cancel:
                    return None

        if dt is not None:
            for comp in components:
                # We don't need to align a sample to the peak of the source
                # time function here.
                new_npts = int(round(
                    (len(data[comp]) - 1) * self.info.dt / dt, 6) + 1)
                data_summed[comp] = lanczos_interpolation(
                    data=np.require(data_summed[comp], requirements=["C"]),
                    old_start=0, old_dt=self.info.dt, new_start=0, new_dt=dt,
                    new_npts=new_npts, a=kernelwidth, window="blackman")

                # The resampling assumes zeros outside the data range. This
                # does not introduce any errors at the beginning as the data is
                # actually zero there but it does affect the end. We will
                # remove all samples that are affected by the boundary
                # conditions here.
                #
                # Also don't cut it for the "identify" interpolation which is
                # important for testing.
                if round(dt / self.info.dt, 6) != 1.0:
                    affected_area = kernelwidth * self.info.dt
                    data_summed[comp] = \
                        data_summed[comp][:-int(np.ceil(affected_area / dt))]

        if dt is None:
            dt_out = self.info.dt
        else:
            dt_out = dt

        # Integrate/differentiate here. No need to do it for every single
        # seismogram and stack the errors.
        n_derivative = KIND_MAP[kind] - STF_MAP[self.info.stf]
        if n_derivative:
            for comp in data_summed.keys():
                _diff_and_integrate(n_derivative=n_derivative,
                                    data=data_summed, comp=comp, dt_out=dt_out)

        # Convert to an ObsPy Stream object.
        st = Stream()
        band_code = get_band_code(dt_out)
        for comp in components:
            tr = Trace(data=data_summed[comp],
                       header={"delta": dt_out,
                               "station": receiver.station,
                               "network": receiver.network,
                               "location": receiver.location,
                               "channel": "%sX%s" % (band_code, comp)})
            st += tr
        return st
Exemple #12
0
    def get_seismograms(self, source, receiver, components=None,
                        kind='displacement', remove_source_shift=True,
                        reconvolve_stf=False, return_obspy_stream=True,
                        dt=None, kernelwidth=12):
        """
        Extract seismograms from the Green's function database.

        :param source: The source definition.
        :type source: :class:`instaseis.source.Source` or
            :class:`instaseis.source.ForceSource`
        :param receiver: The seismic receiver.
        :type receiver: :class:`instaseis.source.Receiver`
        :type components: tuple of str, optional
        :param components: Which components to calculate. Must be a tuple
            containing any combination of ``"Z"``, ``"N"``, ``"E"``,
            ``"R"``, and ``"T"``. Defaults to ``["Z", "N", "E"]`` for two
            component databases, to ``["N", "E"]`` for horizontal only
            databases, and to ``["Z"]`` for vertical only databases.
        :type kind: str, optional
        :param kind: The desired units of the seismogram:
            ``"displacement"``, ``"velocity"``, or ``"acceleration"``.
        :type remove_source_shift: bool, optional
        :param remove_source_shift: Cut all samples before the peak of the
            source time function. This has the effect that the first sample
            is the origin time of the source.
        :type reconvolve_stf: bool, optional
        :param reconvolve_stf: Deconvolve the source time function used in
            the AxiSEM run and convolve with the STF attached to the source.
            For this to be stable, the new STF needs to bandlimited.
        :type return_obspy_stream: bool, optional
        :param return_obspy_stream: Return format is either an
            :class:`obspy.core.stream.Stream` object or a dictionary
            containing the raw NumPy arrays.
        :type dt: float, optional
        :param dt: Desired sampling rate of the seismograms. Resampling is done
            using a Lanczos kernel.
        :type kernelwidth: int, optional
        :param kernelwidth: The width of the sinc kernel used for resampling in
            terms of the original sampling interval. Best choose something
            between 10 and 20.

        :returns: Multi component seismograms.
        :rtype: A :class:`obspy.core.stream.Stream` object or a dictionary
            with NumPy arrays as values.
        """
        if components is None:
            components = self.default_components

        source, receiver = self._get_seismograms_sanity_checks(
            source=source, receiver=receiver, components=components,
            kind=kind, dt=dt)

        # Call the _get_seismograms() method of the respective implementation.
        data = self._get_seismograms(source=source, receiver=receiver,
                                     components=components)

        if dt is None:
            dt_out = self.info.dt
        else:
            dt_out = dt

        stf_deconv_map = {
            0: self.info.sliprate,
            1: self.info.slip}

        # Can never be negative with the current logic.
        n_derivative = KIND_MAP[kind] - STF_MAP[self.info.stf]

        if isinstance(source, ForceSource):
            n_derivative += 1

        if reconvolve_stf and remove_source_shift:
            raise ValueError("'remove_source_shift' argument not "
                             "compatible with 'reconvolve_stf'.")

        # Calculate the final time information about the seismograms.
        time_information = _get_seismogram_times(
            info=self.info, origin_time=source.origin_time, dt=dt,
            kernelwidth=kernelwidth, remove_source_shift=remove_source_shift,
            reconvolve_stf=reconvolve_stf)

        for comp in components:
            if reconvolve_stf:
                # We assume here that the sliprate is well-behaved,
                # e.g. zeros at the boundaries and no energy above the mesh
                # resolution.
                if source.dt is None or source.sliprate is None:
                    raise ValueError("source has no source time function")

                if STF_MAP[self.info.stf] not in [0, 1]:
                    raise NotImplementedError(
                        'deconvolution not implemented for stf %s'
                        % (self.info.stf))

                stf_deconv_f = np.fft.rfft(
                    stf_deconv_map[STF_MAP[self.info.stf]],
                    n=self.info.nfft)

                if abs((source.dt - self.info.dt) / self.info.dt) > 1e-7:
                    raise ValueError("dt of the source not compatible")

                stf_conv_f = np.fft.rfft(source.sliprate,
                                         n=self.info.nfft)

                if source.time_shift is not None:
                    stf_conv_f *= \
                        np.exp(- 1j * rfftfreq(self.info.nfft) *
                               2. * np.pi * source.time_shift / self.info.dt)

                # Apply a 5 percent, at least 5 samples taper at the end.
                # The first sample is guaranteed to be zero in any case.
                tlen = max(int(math.ceil(0.05 * len(data[comp]))), 5)
                taper = np.ones_like(data[comp])
                taper[-tlen:] = scipy.signal.hann(tlen * 2)[tlen:]
                dataf = np.fft.rfft(taper * data[comp], n=self.info.nfft)

                # Ensure numerical stability by not dividing with zero.
                f = stf_conv_f
                _l = np.abs(stf_deconv_f)
                _idx = np.where(_l > 0.0)
                f[_idx] /= stf_deconv_f[_idx]
                f[_l == 0] = 0 + 0j

                data[comp] = np.fft.irfft(dataf * f)[:self.info.npts]

            if dt is not None:
                data[comp] = lanczos_interpolation(
                    data=np.require(data[comp], requirements=["C"]),
                    old_start=0, old_dt=self.info.dt,
                    new_start=time_information["time_shift_at_beginning"],
                    new_dt=dt,
                    new_npts=time_information["npts_before_shift_removal"],
                    a=kernelwidth,
                    window="blackman")

            # Integrate/differentiate before removing the source shift in
            # order to reduce boundary effects at the start of the signal.
            #
            # NEVER to this before the resampling! The error can be really big.
            if n_derivative:
                _diff_and_integrate(n_derivative=n_derivative, data=data,
                                    comp=comp, dt_out=dt_out)

            # If desired, remove the samples before the peak of the source
            # time function.
            if remove_source_shift:
                data[comp] = data[comp][time_information["ref_sample"]:]

        if return_obspy_stream:
            return self._convert_to_stream(
                receiver=receiver, components=components, data=data,
                dt_out=dt_out, starttime=time_information["starttime"])
        else:
            return data
def calculate_adjoint_source(
    observed,
    synthetic,
    window,
    min_period,
    max_period,
    adjoint_src,
    plot=False,
    max_criterion=7.0,
    taper=True,
    taper_ratio=0.15,
    taper_type="cosine",
    **kwargs
):
    """
    :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.
    t = observed.times(type="relative")
    assert t[0] == 0

    ret_dict = {}

    if window:
        if len(window) == 2:
            window_weight = 1.0
        else:
            window_weight = window[2]
    else:
        window_weight = 1.0

    # Work on copies of the original data
    observed = observed.copy()
    synthetic = synthetic.copy()

    if window:
        observed = utils.window_trace(
            trace=observed,
            window=window,
            taper=taper,
            taper_ratio=taper_ratio,
            taper_type=taper_type,
            **kwargs
        )
        synthetic = utils.window_trace(
            trace=synthetic,
            window=window,
            taper=taper,
            taper_ratio=taper_ratio,
            taper_type=taper_type,
            **kwargs
        )

    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 / 4.0)), t[1] - t[0])
    dt_old = 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.
    data = lanczos_interpolation(
        data=observed.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.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",
    )
    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) * window_weight

    # Sanity check. Should not occur.
    if np.isnan(phase_misfit):
        msg = "The phase misfit is NaN."
        raise Exception(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": obspy.Trace(
                data=np.zeros_like(observed.data), header=observed.stats
            ),
            "misfit": phase_misfit * 2.0,
            "details": {"messages": messages},
        }

        return ret_dict
    if adjoint_src:
        # 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 = ad_src / ((t[1] - t[0]) ** 2) * dt_old

        # 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]

        # Calculate actual adjoint source. Not time reversed
        adj_src = obspy.Trace(
            data=ad_src * window_weight, header=observed.stats
        )
        if window:
            adj_src = utils.window_trace(
                trace=adj_src,
                window=window,
                taper=taper,
                taper_ratio=taper_ratio,
                taper_type=taper_type,
                **kwargs
            )

    ret_dict = {
        "adjoint_source": adj_src,
        "misfit": phase_misfit,
        "details": {"messages": messages},
    }

    return ret_dict
Exemple #14
0
def _parse_validate_and_resample_stf(request, db_info):
    """
    Parses the JSON based STF, validates it, and resamples it.

    :param request: The request.
    :param db_info: Information about the current database.
    """
    if not request.body:
        msg = "The source time function must be given in the body of the " \
              "POST request."
        return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    # Try to parse it as a JSON file.
    with io.BytesIO(request.body) as buf:
        try:
            j = json.loads(buf.read().decode())
        except Exception:
            msg = "The body of the POST request is not a valid JSON file."
            return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    # Validate it.
    try:
        json_validate(j, _json_schema)
    except JSONValidationError as e:
        # Replace the u'' unicode string specifier for consistent error
        # messages.
        msg = "Validation Error in JSON file: " + re.sub(r"u'", "'", e.message)
        return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    # Make sure the sampling rate is ok.
    if j["sample_spacing_in_sec"] < db_info.dt:
        msg = "'sample_spacing_in_sec' in the JSON file must not be smaller " \
              "than the database dt [%.3f seconds]." % db_info.dt
        return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    # Convert to numpy array.
    j["data"] = np.array(j["data"], np.float64)

    # A couple more custom validations.
    message = None

    # Make sure its not all zeros.
    if np.abs(j["data"]).max() < 1E-20:
        message = ("All zero (or nearly all zero) source time functions don't "
                   "make any sense.")

    # The data must begin and end with zero. The user is responsible for the
    # tapering.
    if j["data"][0] != 0.0 or j["data"][-1] != 0.0:
        message = "Must begin and end with zero."

    if message:
        msg = "STF data did not validate: %s" % message
        return tornado.web.HTTPError(400, log_message=msg, reason=msg)

    missing_length = db_info.length - (
        len(j["data"]) - 1) * j["sample_spacing_in_sec"]
    missing_samples = max(int(missing_length / j["sample_spacing_in_sec"]) + 1,
                          0)

    # Add a buffer of 20 samples at the beginning and at the end.
    data = np.concatenate([
        np.zeros(20), j["data"], np.zeros(missing_samples + 20)])

    # Resample it using sinc reconstruction.
    data = lanczos_interpolation(
        data,
        # Account for the additional samples at the beginning.
        old_start=-20 * j["sample_spacing_in_sec"],
        old_dt=j["sample_spacing_in_sec"],
        new_start=0.0,
        new_dt=db_info.dt,
        new_npts=db_info.npts,
        # The large a is okay because we add zeros at the beginning and the
        # end.
        a=12, window="blackman")

    # There is potentially some numerical noise on the first sample.
    assert data[0] < 1E-10 * np.abs(data.ptp())
    data[0] = 0.0

    # Normalize the integral to one.
    data /= np.trapz(np.abs(data), dx=db_info.dt)
    j["data"] = data

    return j