Coverage for /opt/obspy/update-docs/src/obspy/obspy/signal/spectral_estimation : 60%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
#!/usr/bin/env python #------------------------------------------------------------------------------ # Filename: spectral_estimation.py # Purpose: Various Routines Related to Spectral Estimation # Author: Tobias Megies # Email: tobias.megies@geophysik.uni-muenchen.de # # Copyright (C) 2011-2012 Tobias Megies #------------------------------------------------------------------------------ Various Routines Related to Spectral Estimation
:copyright: The ObsPy Development Team (devs@obspy.org) :license: GNU Lesser General Public License, Version 3 (http://www.gnu.org/copyleft/lesser.html) """
# if matplotlib is not present be silent about it and only raise the # ImportError if matplotlib actually is used (currently in psd() and # PPSD()) msg_matplotlib_ImportError = "Failed to import matplotlib. While this " \ "is no dependency of obspy.signal it is however necessary for a " \ "few routines. Please install matplotlib in order to be able " \ "to use e.g. psd() or PPSD()." # set up two dummy functions. this makes it possible to make the docstring # of psd() look like it should with two functions as default values for # kwargs although matplotlib might not be present and the routines # therefore not usable
def detrend_none(): pass
def window_hanning(): pass
else: # Import matplotlib routines. These are no official dependency of # obspy.signal so an import error should really only be raised if any # routine is used which relies on matplotlib (at the moment: psd, PPSD).
# build colormap as done in paper by mcnamara (0.05, 1.0, 1.0), (0.2, 0.0, 0.0), (0.4, 0.0, 0.0), (0.6, 0.0, 0.0), (0.8, 1.0, 1.0), (1.0, 1.0, 1.0)), 'green': ((0.0, 1.0, 1.0), (0.05, 0.0, 0.0), (0.2, 0.0, 0.0), (0.4, 1.0, 1.0), (0.6, 1.0, 1.0), (0.8, 1.0, 1.0), (1.0, 0.0, 0.0)), 'blue': ((0.0, 1.0, 1.0), (0.05, 1.0, 1.0), (0.2, 1.0, 1.0), (0.4, 1.0, 1.0), (0.6, 0.0, 0.0), (0.8, 0.0, 0.0), (1.0, 0.0, 0.0))} "data", "noise_models.npz") # do not change these variables, otherwise results may differ from PQLX!
noverlap=0): """ Wrapper for :func:`matplotlib.mlab.psd`.
Always returns a onesided psd (positive frequencies only), corrects for this fact by scaling with a factor of 2. Also, always normalizes to dB/Hz by dividing with sampling rate.
This wrapper is intended to intercept changes in :func:`matplotlib.mlab.psd` default behavior which changes with matplotlib version 0.98.4:
* http://matplotlib.sourceforge.net/users/whats_new.html\ #psd-amplitude-scaling * http://matplotlib.sourceforge.net/_static/CHANGELOG (entries on 2009-05-18 and 2008-11-11) * http://matplotlib.svn.sourceforge.net/viewvc/matplotlib\ ?view=revision&revision=6518 * http://matplotlib.sourceforge.net/api/api_changes.html#changes-for-0-98-x
.. note:: For details on all arguments see :func:`matplotlib.mlab.psd`.
.. note:: When using `window=welch_taper` (:func:`obspy.signal.spectral_estimation.welch_taper`) and `detrend=detrend_linear` (:func:`matplotlib.mlab.detrend_linear`) the psd function delivers practically the same results as PITSA. Only DC and the first 3-4 lowest non-DC frequencies deviate very slightly. In contrast to PITSA, this routine also returns the psd value at the Nyquist frequency and therefore is one frequency sample longer. """ # check if matplotlib is available, no official dependency for obspy.signal raise ImportError(msg_matplotlib_ImportError)
# check matplotlib version else: new_matplotlib = False # build up kwargs that do not change with version 0.98.4 # add additional kwargs to control behavior for matplotlib versions higher # than 0.98.4. These settings make sure that the scaling is already done # during the following psd call for newer matplotlib versions. # do the actual call to mlab.psd # do scaling manually for old matplotlib versions Pxx = Pxx / Fs Pxx[1:-1] = Pxx[1:-1] * 2.0
""" Cosine taper, 10 percent at each end (like done by [McNamara2004]_).
.. warning:: Inplace operation, so data should be float. """
""" Applies a welch window to data. See :func:`~obspy.signal.spectral_estimation.welch_window`.
.. warning:: Inplace operation, so data should be float.
:type data: :class:`~numpy.ndarray` :param data: Data to apply the taper to. Inplace operation, but also returns data for convenience. :returns: Tapered data. """
""" Return a welch window for data of length N.
Routine is checked against PITSA for both even and odd values, but not for strange values like N<5.
.. note:: See e.g.: http://www.cg.tuwien.ac.at/hostings/cescg/CESCG99/TTheussl/node7.html
:type N: int :param N: Length of window function. :rtype: :class:`~numpy.ndarray` :returns: Window function for tapering data. """ # first/last sample is zero by definition # even number of samples: two ones in the middle, perfectly symmetric else: # odd number of samples: still two ones in the middle, however, not # perfectly symmetric anymore. right side is shorter by one sample # first/last sample is zero by definition
""" Class to compile probabilistic power spectral densities for one combination of network/station/location/channel/sampling_rate.
Calculations are based on the routine used by [McNamara2004]_. For information on New High/Low Noise Model see [Peterson2003]_.
.. rubric:: Basic Usage
>>> from obspy import read >>> from obspy.signal import PPSD
>>> st = read() >>> tr = st.select(channel="EHZ")[0] >>> paz = {'gain': 60077000.0, ... 'poles': [-0.037004+0.037016j, -0.037004-0.037016j, ... -251.33+0j, -131.04-467.29j, -131.04+467.29j], ... 'sensitivity': 2516778400.0, ... 'zeros': [0j, 0j]}
>>> ppsd = PPSD(tr.stats, paz) >>> print ppsd.id BW.RJOB..EHZ >>> print ppsd.times []
Now we could add data to the probabilistic psd (all processing like demeaning, tapering and so on is done internally) and plot it like ...
>>> ppsd.add(st) # doctest: +SKIP >>> print ppsd.times # doctest: +SKIP >>> ppsd.plot() # doctest: +SKIP
... but the example stream is too short and does not contain enough data.
And/or we could save the ppsd data in a pickled file ...
>>> ppsd.save("myfile.pkl") # doctest: +SKIP
... that later can be loaded again using the `pickle` module in the Python Standard Library, e.g. to add more data or plot it again.
>>> import pickle >>> ppsd = pickle.load("myfile.pkl") # doctest: +SKIP
For a real world example see the `ObsPy Tutorial`_.
.. note::
It is safer (but a bit slower) to provide a :class:`~obspy.xseed.parser.Parser` instance with information from e.g. a Dataless SEED than to just provide a static PAZ dictionary.
.. _`ObsPy Tutorial`: http://docs.obspy.org/tutorial/ """ is_rotational_data=False, db_bins=[-200, -50, 0.5]): """ Initialize the PPSD object setting all fixed information on the station that should not change afterwards to guarantee consistent spectral estimates. The necessary instrument response information can be provided in two ways:
* Providing an `obspy.xseed` :class:`~obspy.xseed.parser.Parser`, e.g. containing metadata from a Dataless SEED file. This is the safer way but it might a bit slower because for every processed time segment the response information is extracted from the parser. * Providing a dictionary containing poles and zeros information. Be aware that this leads to wrong results if the instrument's response is changing with data added to the PPSD. Use with caution!
:note: When using `is_rotational_data=True` the applied processing steps are changed. Differentiation of data (converting velocity to acceleration data) will be omitted and a flat instrument response is assumed, leaving away response removal and only dividing by `paz['sensitivity']` specified in the provided `paz` dictionary (other keys do not have to be present then). For scaling factors that are usually multiplied to the data remember to use the inverse as `paz['sensitivity']`.
:type stats: :class:`~obspy.core.trace.Stats` :param stats: Stats of the station/instrument to process :type paz: dict (optional) :param paz: Response information of instrument. If not specified the information is supposed to be present as stats.paz. :type parser: :class:`obspy.xseed.parser.Parser` (optional) :param parser: Parser instance with response information (e.g. read from a Dataless SEED volume) :type skip_on_gaps: Boolean (optional) :param skip_on_gaps: Determines whether time segments with gaps should be skipped entirely. McNamara & Buland merge gappy traces by filling with zeros. This results in a clearly identifiable outlier psd line in the PPSD visualization. Select `skip_on_gaps=True` for not filling gaps with zeros which might result in some data segments shorter than 1 hour not used in the PPSD. :type is_rotational_data: Boolean (optional) :param is_rotational_data: If set to True adapt processing of data to rotational data. See note for details. :type db_bins: List of three ints/floats :param db_bins: Specify the lower and upper boundary and the width of the db bins. The bin width might get adjusted to fit a number of equally spaced bins in between the given boundaries. """ # check if matplotlib is available, no official dependency for # obspy.signal raise ImportError(msg_matplotlib_ImportError)
msg = "Both paz and parser specified. Using parser object for " \ "metadata." warnings.warn(msg)
# trace length for one hour piece # set paz either from kwarg or try to get it from stats self.merge_method = -1 else: # nfft is determined mimicing the fft setup in McNamara&Buland paper: # (they take 13 segments overlapping 75% and truncate to next lower # power of 2) # - take number of points of whole ppsd segment (currently 1 hour) # - make 13 single segments overlapping by 75% # (1 full segment length + 25% * 12 full segment lengths) # - go to next smaller power of 2 for nfft # - use 75% overlap (we end up with a little more than 13 segments..) # set up the binning for the db scale endpoint=True)
""" Makes an initial dummy psd and thus sets up the bins and all the rest. Should be able to do it without a dummy psd.. """ noverlap=self.nlap)
# leave out first entry (offset)
# calculate left/rigth edge of first period bin, # width of bin is one octave # calculate center period of first period bin # calculate mean of all spectral values in the first bin # we move through the period range at 1/8 octave steps # do this for the whole period range and append the values to our lists
# mid-points of all the period bins self.period_bins[1:]), axis=0)
""" Checks if trace is compatible for use in the current PPSD instance. Returns True if trace can be used or False if not.
:type trace: :class:`~obspy.core.trace.Trace` """ return False return False
""" Inserts the given UTCDateTime at the right position in the list keeping the order intact.
:type utcdatetime: :class:`~obspy.core.utcdatetime.UTCDateTime` """
""" Gets gap information of stream and adds the encountered gaps to the gap list of the PPSD instance.
:type stream: :class:`~obspy.core.stream.Stream` """
""" Gets gap information of stream and adds the encountered gaps to the gap list of the PPSD instance.
:type stream: :class:`~obspy.core.stream.Stream` """ [[tr.stats.starttime, tr.stats.endtime] for tr in stream]
""" Checks if the given UTCDateTime is already part of the current PPSD instance. That is, checks if from utcdatetime to utcdatetime plus 1 hour there is already data in the PPSD. Returns True if adding an one hour piece starting at the given time would result in an overlap of the ppsd data base, False if it is OK to insert this piece of data. """ utcdatetime + PPSD_LENGTH) else:
""" Process all traces with compatible information and add their spectral estimates to the histogram containg the probabilistic psd. Also ensures that no piece of data is inserted twice.
:type stream: :class:`~obspy.core.stream.Stream` or :class:`~obspy.core.trace.Trace` :param stream: Stream or trace with data that should be added to the probabilistic psd histogram. :returns: True if appropriate data were found and the ppsd statistics were changed, False otherwise. """ # return later if any changes were applied to the ppsd statistics # prepare the list of traces to go through stream = Stream([stream]) # select appropriate traces sampling_rate=self.sampling_rate) # save information on available data and gaps # merge depending on skip_on_gaps set during __init__
# the following check should not be necessary due to the select().. msg = "Skipping incompatible trace." warnings.warn(msg) continue "skipping these slices." else: # throw warnings if trace length is different # than one hour..!?! # XXX not good, should be working in place somehow # XXX how to do it with the padding, though? print t1
# enforce time limits, pad zeros if gaps #tr.trim(t, t+PPSD_LENGTH, pad=True)
""" Processes a one-hour segment of data and adds the information to the PPSD histogram. If Trace is compatible (station, channel, ...) has to checked beforehand.
:type tr: :class:`~obspy.core.trace.Trace` :param tr: Compatible Trace with data of one PPSD segment :returns: True if segment was successfully added to histogram, False otherwise. """ # XXX DIRTY HACK!! # one last check.. msg = "Got an non-one-hour piece of data to process. Skipping" warnings.warn(msg) print len(tr), self.len return False # being paranoid, only necessary if in-place operations would follow # if trace has a masked array we fill in zeros # if it is no masked array, we get an AttributeError # and have nothing to do
# get instrument response preferably from parser object msg = "Error getting response from parser:\n%s: %s\n" \ "Skipping time segment(s)." msg = msg % (e.__class__.__name__, e.message) warnings.warn(msg) return False msg = "Missing poles and zeros information for response " \ "removal. Skipping time segment(s)." warnings.warn(msg) return False # restitution: # mcnamara apply the correction at the end in freq-domain, # does it make a difference? # probably should be done earlier on bigger chunk of data?! # in case of rotational data just remove sensitivity tr.data /= paz['sensitivity'] else: paz_simulate=None, simulate_sensitivity=False)
# go to acceleration, do nothing for rotational data: pass else:
# use our own wrapper for mlab.psd to have consistent results on all # matplotlib versions detrend=mlab.detrend_linear, window=fft_taper, noverlap=self.nlap)
# leave out first entry (offset)
# working with the periods not frequencies later so reverse spectrum
# avoid calculating log of zero
# go to dB
# do this for the whole period range and append the values to our lists self.per_octaves_right):
spec_octaves, bins=(self.period_bins, self.spec_bins))
# we have to make sure manually that the bins are always the same! # this is done with the various assert() statements above. # only during first run initialize stack with first histogram
""" Returns periods and approximate psd values for given percentile value.
:type percentile: int :param percentile: percentile for which to return approximate psd value. (e.g. a value of 50 is equal to the median.) :type hist_cum: `numpy.ndarray` (optional) :param hist_cum: if it was already computed beforehand, the normalized cumulative histogram can be provided here (to avoid computing it again), otherwise it is computed from the currently stored histogram. :returns: (periods, percentile_values) """ if hist_cum is None: hist_cum = self.__get_normalized_cumulative_histogram() # go to percent percentile = percentile / 100.0 if percentile == 0: # only for this special case we have to search from the other side # (otherwise we always get index 0 in .searchsorted()) side = "right" else: side = "left" percentile_values = [col.searchsorted(percentile, side=side) \ for col in hist_cum] # map to power db values percentile_values = self.spec_bins[percentile_values] return (self.period_bin_centers, percentile_values)
""" Returns the current histogram in a cumulative version normalized per period column, i.e. going from 0 to 1 from low to high psd values for every period column. """ # sum up the columns to cumulative entries hist_cum = self.hist_stack.cumsum(axis=1) # normalize every column with its overall number of entries # (can vary from the number of self.times because of values outside # the histogram db ranges) norm = hist_cum[:, -1].copy() # avoid zero division norm[norm == 0] = 1 hist_cum = (hist_cum.T / norm).T return hist_cum
""" Saves PPSD instance as a pickled file that can be loaded again using pickle.load(filename).
:type filename: str :param filename: Name of output file with pickled PPSD object """ with open(filename, "w") as file: pickle.dump(self, file)
show_percentiles=False, percentiles=[0, 25, 50, 75, 100], show_noise_models=True, grid=True, show=True): """ Plot the 2D histogram of the current PPSD. If a filename is specified the plot is saved to this file, otherwise a plot window is shown.
:type filename: str (optional) :param filename: Name of output file :type show_coverage: bool (optional) :param show_coverage: Enable/disable second axes with representation of data coverage time intervals. :type show_percentiles: bool (optional) :param show_percentiles: Enable/disable plotting of approximated percentiles. These are calculated from the binned histogram and are not the exact percentiles. :type percentiles: list of ints :param percentiles: percentiles to show if plotting of percentiles is selected. :type show_noise_models: bool (optional) :param show_noise_models: Enable/disable plotting of noise models. :type grid: bool (optional) :param grid: Enable/disable grid in histogram plot. :type show: bool (optional) :param show: Enable/disable immediately showing the plot. """ X, Y = np.meshgrid(self.xedges, self.yedges) hist_stack = self.hist_stack * 100.0 / len(self.times_used)
fig = plt.figure()
if show_coverage: ax = fig.add_axes([0.12, 0.3, 0.90, 0.6]) ax2 = fig.add_axes([0.15, 0.17, 0.7, 0.04]) else: ax = fig.add_subplot(111)
if show_histogram: ppsd = ax.pcolor(X, Y, hist_stack.T, cmap=self.colormap) cb = plt.colorbar(ppsd, ax=ax) cb.set_label("[%]") color_limits = (0, 30) ppsd.set_clim(*color_limits) cb.set_clim(*color_limits) if grid: ax.grid(b=grid, which="major") ax.grid(b=grid, which="minor")
if show_percentiles: hist_cum = self.__get_normalized_cumulative_histogram() # for every period look up the approximate place of the percentiles for percentile in percentiles: periods, percentile_values = \ self.get_percentile(percentile=percentile, hist_cum=hist_cum) ax.plot(periods, percentile_values, color="black")
if show_noise_models: model_periods, high_noise = get_NHNM() ax.plot(model_periods, high_noise, '0.4', linewidth=2) model_periods, low_noise = get_NLNM() ax.plot(model_periods, low_noise, '0.4', linewidth=2)
ax.semilogx() ax.set_xlim(0.01, 179) ax.set_ylim(self.spec_bins[0], self.spec_bins[-1]) ax.set_xlabel('Period [s]') ax.set_ylabel('Amplitude [dB]') ax.xaxis.set_major_formatter(FormatStrFormatter("%.2f")) title = "%s %s -- %s (%i segments)" title = title % (self.id, self.times_used[0].date, self.times_used[-1].date, len(self.times_used)) ax.set_title(title)
if show_coverage: self.__plot_coverage(ax2) # emulating fig.autofmt_xdate(): for label in ax2.get_xticklabels(): label.set_ha("right") label.set_rotation(30)
plt.draw() if filename is not None: plt.savefig(filename) plt.close() elif show: plt.show()
""" Plot the data coverage of the histogram of the current PPSD. If a filename is specified the plot is saved to this file, otherwise a plot window is shown.
:type filename: str (optional) :param filename: Name of output file """ fig = plt.figure() ax = fig.add_subplot(111)
self.__plot_coverage(ax) fig.autofmt_xdate() title = "%s %s -- %s (%i segments)" title = title % (self.id, self.times_used[0].date, self.times_used[-1].date, len(self.times_used)) ax.set_title(title)
plt.draw() if filename is not None: plt.savefig(filename) plt.close() else: plt.show()
""" Helper function to plot coverage into given axes. """ ax.figure ax.clear() ax.xaxis_date() ax.set_yticks([])
# plot data coverage starts = [date2num(t.datetime) for t in self.times_used] ends = [date2num((t + PPSD_LENGTH).datetime) for t in self.times_used] for start, end in zip(starts, ends): ax.axvspan(start, end, 0, 0.7, alpha=0.5, lw=0) # plot data for start, end in self.times_data: start = date2num(start.datetime) end = date2num(end.datetime) ax.axvspan(start, end, 0.7, 1, facecolor="g", lw=0) # plot gaps for start, end in self.times_gaps: start = date2num(start.datetime) end = date2num(end.datetime) ax.axvspan(start, end, 0.7, 1, facecolor="r", lw=0)
ax.autoscale_view()
""" Returns periods and psd values for the New Low Noise Model. For information on New High/Low Noise Model see [Peterson2003]_. """ data = np.load(NOISE_MODEL_FILE) periods = data['model_periods'] nlnm = data['low_noise'] return (periods, nlnm)
""" Returns periods and psd values for the New High Noise Model. For information on New High/Low Noise Model see [Peterson2003]_. """ data = np.load(NOISE_MODEL_FILE) periods = data['model_periods'] nlnm = data['high_noise'] return (periods, nlnm)
if __name__ == '__main__': import doctest doctest.testmod(exclude_empty=True) |