EQcorrscan_plotting — EQcorrscan 0.1.0 documentation

# Source code for EQcorrscan_plotting

    """
    Utility code for most of the plots used as part of the EQcorrscan package.

    Copyright 2015 Calum Chamberlain

    This file is part of EQcorrscan.

        EQcorrscan is free software: you can redistribute it and/or modify
        it under the terms of the GNU General Public License as published by
        the Free Software Foundation, either version 3 of the License, or
        (at your option) any later version.

        EQcorrscan is distributed in the hope that it will be useful,
        but WITHOUT ANY WARRANTY; without even the implied warranty of
        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
        GNU General Public License for more details.

        You should have received a copy of the GNU General Public License
        along with EQcorrscan.  If not, see <http://www.gnu.org/licenses/>.

    """
    from __future__ import absolute_import
    from __future__ import division
    from __future__ import print_function
    from __future__ import unicode_literals
    import numpy as np
    import matplotlib.pylab as plt

    def chunk_data(tr, samp_rate, state='mean'):
        r"""Function to downsample data for plotting by computing the maximum of \
        data within chunks, useful for plotting waveforms or cccsums, large \
        datasets that would otherwise exceed the complexity allowed, and overflow.

        :type tr: obspy.Trace
        :param tr: Trace to be chunked
        :type samp_rate: float
        :param samp_rate: Desired sampling rate in Hz
        :type state: str
        :param state: Either 'Min', 'Max', 'Mean' or 'Maxabs' to return one of \
            these for the chunks. Maxabs will return the largest (positive or \
            negative) for that chunk.

        :returns: :class: obspy.Trace
        """
        trout = tr.copy()  # Don't do it inplace on data
        x = np.arange(len(tr.data))
        y = tr.data

        chunksize = int(round(tr.stats.sampling_rate / samp_rate))
        # Wrap the array into a 2D array of chunks, truncating the last chunk if
        # chunksize isn't an even divisor of the total size.
        # (This part won't use _any_ additional memory)
        numchunks = y.size // chunksize
        ychunks = y[:chunksize*numchunks].reshape((-1, chunksize))
        xchunks = x[:chunksize*numchunks].reshape((-1, chunksize))

        # Calculate the max, min, and means of chunksize-element chunks...
        if state == 'Max':
            trout.data = ychunks.max(axis=1)
        elif state == 'Min':
            trout.data = ychunks.min(axis=1)
        elif state == 'Mean':
            trout.data = ychunks.mean(axis=1)
        elif state == 'Maxabs':
            max_env = ychunks.max(axis=1)
            min_env = ychunks.min(axis=1)
            indeces = np.argmax(np.vstack([np.abs(max_env), np.abs(min_env)]),
                                axis=0)
            stack = np.vstack([max_env, min_env]).T
            trout.data = np.array([stack[i][indeces[i]]
                                  for i in xrange(len(stack))])
        xcenters = xchunks.mean(axis=1)
        trout.stats.starttime = tr.stats.starttime + xcenters[0] /\
            tr.stats.sampling_rate
        trout.stats.sampling_rate = samp_rate
        return trout

    def triple_plot(cccsum, cccsum_hist, trace, threshold, save=False,
                    savefile=''):
        r"""Main function to make a triple plot with a day-long seismogram, \
        day-long correlation sum trace and histogram of the correlation sum to \
        show normality.

        :type cccsum: numpy.ndarray
        :param cccsum: Array of the cross-channel cross-correlation sum
        :type cccsum_hist: numpy.ndarray
        :param cccsum_hist: cccsum for histogram plotting, can be the same as \
            cccsum but included if cccsum is just an envelope.
        :type trace: obspy.Trace
        :param trace: A sample trace from the same time as cccsum
        :type threshold: float
        :param threshold: Detection threshold within cccsum
        :type save: bool, optional
        :param save: If True will svae and not plot to screen, vice-versa if False
        :type savefile: str, optional
        :param savefile: Path to save figure to, only required if save=True
        """
        if len(cccsum) != len(trace.data):
            print('cccsum is: ' +
                  str(len(cccsum))+' trace is: '+str(len(trace.data)))
            msg = ' '.join(['cccsum and trace must have the',
                            'same number of data points'])
            raise ValueError(msg)
        df = trace.stats.sampling_rate
        npts = trace.stats.npts
        t = np.arange(npts, dtype=np.float32) / (df * 3600)
        # Generate the subplot for the seismic data
        ax1 = plt.subplot2grid((2, 5), (0, 0), colspan=4)
        ax1.plot(t, trace.data, 'k')
        ax1.axis('tight')
        ax1.set_ylim([-15 * np.mean(np.abs(trace.data)),
                      15 * np.mean(np.abs(trace.data))])
        # Generate the subplot for the correlation sum data
        ax2 = plt.subplot2grid((2, 5), (1, 0), colspan=4, sharex=ax1)
        # Plot the threshold values
        ax2.plot([min(t), max(t)], [threshold, threshold], color='r', lw=1,
                 label="Threshold")
        ax2.plot([min(t), max(t)], [-threshold, -threshold], color='r', lw=1)
        ax2.plot(t, cccsum, 'k')
        ax2.axis('tight')
        ax2.set_ylim([-1.7 * threshold, 1.7 * threshold])
        ax2.set_xlabel("Time after %s [hr]" % trace.stats.starttime.isoformat())
        # ax2.legend()
        # Generate a small subplot for the histogram of the cccsum data
        ax3 = plt.subplot2grid((2, 5), (1, 4), sharey=ax2)
        ax3.hist(cccsum_hist, 200, normed=1, histtype='stepfilled',
                 orientation='horizontal', color='black')
        ax3.set_ylim([-5, 5])
        fig = plt.gcf()
        fig.suptitle(trace.id)
        fig.canvas.draw()
        if not save:
            plt.show()
            plt.close()
        else:
            plt.savefig(savefile)
        return

    def peaks_plot(data, starttime, samp_rate, save=False, peaks=[(0, 0)],
                   savefile=''):
        r"""Simple utility code to plot the correlation peaks to check that the \
        peak finding routine is running correctly, used in debugging for the \
        EQcorrscan module.

        :type data: numpy.array
        :param data: Numpy array of the data within which peaks have been found
        :type starttime: obspy.UTCDateTime
        :param starttime: Start time for the data
        :type samp_rate: float
        :param samp_rate: Sampling rate of data in Hz
        :type save: Boolean, optional
        :param save: Save figure or plot to screen (False)
        :type peaks: list of Tuple, optional
        :param peaks: List of peak locations and amplitudes (loc, amp)
        :type savefile: String, optional
        :param savefile: Path to save to, only used if save=True
        """
        npts = len(data)
        t = np.arange(npts, dtype=np.float32) / (samp_rate * 3600)
        fig = plt.figure()
        ax1 = fig.add_subplot(111)
        ax1.plot(t, data, 'k')
        ax1.scatter(peaks[0][1] / (samp_rate * 3600), abs(peaks[0][0]),
                    color='r', label='Peaks')
        for peak in peaks:
            ax1.scatter(peak[1] / (samp_rate * 3600), abs(peak[0]), color='r')
        ax1.legend()
        ax1.set_xlabel("Time after %s [hr]" % starttime.isoformat())
        ax1.axis('tight')
        fig.suptitle('Peaks')
        if not save:
            plt.show()
            plt.close()
        else:
            plt.savefig(savefile)
        return

    def cumulative_detections(dates, template_names, save=False, savefile=''):
        r"""Simple plotting function to take a list of datetime objects and plot \
        a cumulative detections list.  Can take dates as a list of lists and will \
        plot each list seperately, e.g. if you have dates from more than one \
        template it will overlay them in different colours.

        :type dates: list of lists of datetime.datetime
        :param dates: Must be a list of lists of datetime.datetime objects
        :type template_names: list of strings
        :param template_names: List of the template names in order of the dates
        :type save: bool
        :param save: Save figure or show to screen, optional
        :type savefile: str
        :param savefile: String to save to, optional
        """
        # Set up a default series of parameters for lines
        colors = ['blue', 'green', 'red', 'cyan', 'magenta', 'yellow', 'black',
                  'firebrick', 'purple', 'darkgoldenrod', 'gray']
        linestyles = ['-', '-.',  '--',  ':']
        # Check that dates is a list of lists
        if type(dates[0]) != list:
            dates = [dates]
        i = 0
        j = 0
        # This is an ugly way of looping through colours and linestyles, it would
        # be better with itertools functions...
        plothandles = []
        for k, template_dates in enumerate(dates):
            template_dates.sort()
            counts = np.arange(0, len(template_dates))
            print(str(i)+' '+str(j)+' '+str(k))
            filename = plt.plot(template_dates, counts, linestyles[j],
                                color=colors[i], label=template_names[k],
                                linewidth=3.0)
            plothandles.append(filename)
            if i < len(colors) - 1:
                i += 1
            else:
                i = 0
                if j < len(linestyles) - 1:
                    j += 1
                else:
                    j = 0
        plt.xlabel('Date')
        plt.ylabel('Cumulative detections')
        plt.title('Cumulative detections for all templates')
        plt.legend(loc=2, prop={'size': 8}, ncol=2)  # handles=plothandles)
        if save:
            plt.savefig(savefile)
            plt.close()
        else:
            plt.show()
        return

    def threeD_gridplot(nodes, save=False, savefile=''):
        r"""Function to plot in 3D a series of grid points.

        :type nodes: list of tuples
        :param nodes: List of tuples of the form (lat, long, depth)
        :type save: bool
        :param save: if True will save without plotting to screen, if False \
            (default) will plot to screen but not save
        :type savefile: str
        :param savefile: required if save=True, path to save figure to.
        """
        lats = []
        longs = []
        depths = []
        for node in nodes:
            lats.append(float(node[0]))
            longs.append(float(node[1]))
            depths.append(float(node[2]))
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
        ax.scatter(lats, longs, depths)
        ax.set_ylabel("Latitude (deg)")
        ax.set_xlabel("Longitude (deg)")
        ax.set_zlabel("Depth(km)")
        ax.get_xaxis().get_major_formatter().set_scientific(False)
        ax.get_yaxis().get_major_formatter().set_scientific(False)
        if not save:
            plt.show()
            plt.close()
        else:
            plt.savefig(savefile)
        return

    def multi_event_singlechan(streams, catalog, clip=10.0, pre_pick=2.0,
                               freqmin=False, freqmax=False, realign=False,
                               cut=(-3.0, 5.0), PWS=False, title=False):
        r"""Function to plot data from a single channel at a single station for \
        multiple events - data will be alligned by their pick-time given in the \
        picks.

        :type streams: list of :class:obspy.stream
        :param streams: List of the streams to use, can contain more traces than \
            you plan on plotting
        :type catalog: obspy.core.event.Catalog
        :param catalog: Catalog of events, one for each trace, with a single pick
        :type clip: float
        :param clip: Length in seconds to plot, defaults to 10.0
        :type pre_pick: float
        :param pre_pick: Length in seconds to extract and plot before the pick, \
            defaults to 2.0
        :type freqmin: float
        :param freqmin: Low cut for bandpass in Hz
        :type freqmax: float
        :param freqmax: High cut for bandpass in Hz
        :type realign: bool
        :param realign: To compute best alignement based on correlation or not.
        :type cut: tuple
        :param cut: tuple of start and end times for cut in seconds from the pick
        :type PWS: bool
        :param PWS: compute Phase Weighted Stack, if False, will compute linear \
            stack.
        :type title: str
        :param title: Plot title.

        :returns: Alligned and cut traces, and new picks
        """
        from eqcorrscan.utils import stacking
        import copy
        from eqcorrscan.core.match_filter import normxcorr2
        from obspy import Stream
        import warnings
        fig, axes = plt.subplots(len(catalog)+1, 1, sharex=True, figsize=(7, 12))
        axes = axes.ravel()
        traces = []
        al_traces = []
        # Keep input safe
        clist = copy.deepcopy(catalog)
        st_list = copy.deepcopy(streams)
        for i, event in enumerate(clist):
            if st_list[i].select(station=event.picks[0].waveform_id.station_code,
                                 channel='*' +
                                 event.picks[0].waveform_id.channel_code[-1]):
                tr = st_list[i].select(station=event.picks[0].waveforms_id.
                                       station_code,
                                       channel='*' +
                                       event.picks[0].waveform_id.
                                       channel_code[-1])[0]
            else:
                print('No data for '+event.pick[0].waveform_id)
                continue
            tr.detrend('linear')
            if freqmin:
                tr.filter('bandpass', freqmin=freqmin, freqmax=freqmax)
            if realign:
                tr_cut = tr.copy()
                tr_cut.trim(event.picks[0].time + cut[0],
                            event.picks[0].time + cut[1],
                            nearest_sample=False)
                if len(tr_cut.data) <= (0.5 * (cut[1] - cut[0]) *
                                        tr_cut.stats.sampling_rate):
                    msg = ''.join(['Not enough in the trace for ',
                                   tr.stats.station,
                                   '.', tr.stats.channel, '\n',
                                   'Suggest removing pick from sfile at time ',
                                   str(event.picks[0].time)])
                    warnings.warn(msg)
                else:
                    al_traces.append(tr_cut)
            else:
                tr.trim(event.picks[0].time - pre_pick,
                        event.picks[0].time + clip - pre_pick,
                        nearest_sample=False)
            if len(tr.data) == 0:
                msg = ''.join(['No data in the trace for ', tr.stats.station,
                               '.', tr.stats.channel, '\n',
                               'Suggest removing pick from sfile at time ',
                               str(event.picks[0].time)])
                warnings.warn(msg)
                continue
            traces.append(tr)
        if realign:
            shift_len = int(0.25 * (cut[1] - cut[0]) *
                            al_traces[0].stats.sampling_rate)
            shifts = stacking.align_traces(al_traces, shift_len)
            for i in xrange(len(shifts)):
                print('Shifting by '+str(shifts[i])+' seconds')
                event.picks[0].time -= shifts[i]
                traces[i].trim(event.picks[0].time - pre_pick,
                               event.picks[0].time + clip-pre_pick,
                               nearest_sample=False)
        # We now have a list of traces
        traces = [(trace, trace.stats.starttime.datetime) for trace in traces]
        traces.sort(key=lambda tup: tup[1])
        traces = [trace[0] for trace in traces]
        # Plot the traces
        for i, tr in enumerate(traces):
            y = tr.data
            x = np.arange(len(y))
            x = x / tr.stats.sampling_rate  # convert to seconds
            axes[i+1].plot(x, y, 'k', linewidth=1.1)
            axes[i+1].yaxis.set_ticks([])
        traces = [Stream(trace) for trace in traces]
        if PWS:
            linstack = stacking.PWS_stack(traces)
        else:
            linstack = stacking.linstack(traces)
        tr = linstack.select(station=event[0].picks[0].waveform_id.station_code,
                             channel='*' +
                             event[0].picks[0].waveform_id.channel_code[-1])[0]
        y = tr.data
        x = np.arange(len(y))
        x = x / tr.stats.sampling_rate
        axes[0].plot(x, y, 'r', linewidth=2.0)
        axes[0].set_ylabel('Stack', rotation=0)
        axes[0].yaxis.set_ticks([])
        for i, slave in enumerate(traces):
            cc = normxcorr2(tr.data, slave[0].data)
            axes[i+1].set_ylabel('cc='+str(round(np.max(cc), 2)), rotation=0)
            axes[i+1].text(0.9, 0.15, str(round(np.max(slave[0].data))),
                           bbox=dict(facecolor='white', alpha=0.95),
                           transform=axes[i+1].transAxes)
            axes[i+1].text(0.7, 0.85, slave[0].stats.starttime.datetime.
                           strftime('%Y/%m/%d %H:%M:%S'),
                           bbox=dict(facecolor='white', alpha=0.95),
                           transform=axes[i+1].transAxes)
        axes[-1].set_xlabel('Time (s)')
        if title:
            axes[0].set_title(title)
        plt.subplots_adjust(hspace=0)
        plt.show()
        return traces, clist

    def detection_multiplot(stream, template, times, streamcolour='k',
                            templatecolour='r'):
        r"""Function to plot the stream of data that has been detected in, with\
        the template on top of it timed according to a list of given times, just\
        a pretty way to show a detection!

        :type stream: obspy.Stream
        :param stream: Stream of data to be plotted as the base (black)
        :type template: obspy.Stream
        :param template: Template to be plotted on top of the base stream (red)
        :type times: list of datetime.datetime
        :param times: list of times of detections in the order of the channels in
                    template.
        :type streamcolour: str
        :param streamcolour: String of matplotlib colour types for the stream
        :type templatecolour: str
        :param templatecolour: Colour to plot the template in.
        """
        import datetime as dt
        from obspy import UTCDateTime
        fig, axes = plt.subplots(len(template), 1, sharex=True)
        axes = axes.ravel()
        mintime = min([tr.stats.starttime for tr in template])
        for i, template_tr in enumerate(template):
            image = stream.select(station=template_tr.stats.station,
                                  channel='*'+template_tr.stats.channel[-1])
            if not image:
                msg = ' '.join(['No data for', template_tr.stats.station,
                                template_tr.stats.channel])
                print(msg)
                continue
            image = image.merge()[0]
            # Downsample if needed
            if image.stats.sampling_rate > 20:
                image.decimate(image.stats.sampling_rate // 20)
            if template_tr.stats.sampling_rate > 20:
                template_tr.decimate(template_tr.stats.sampling_rate // 20)
            # Get a list of datetime objects
            image_times = [image.stats.starttime.datetime +
                           dt.timedelta((j * image.stats.delta) / 86400)
                           for j in range(len(image.data))]
            axes[i].plot(image_times, image.data / max(image.data),
                         streamcolour, linewidth=1.2)
            for k, time in enumerate(times):
                lagged_time = UTCDateTime(time) + (template_tr.stats.starttime -
                                                   mintime)
                lagged_time = lagged_time.datetime
                template_times = [lagged_time +
                                  dt.timedelta((j * template_tr.stats.delta) /
                                               86400)
                                  for j in range(len(template_tr.data))]
                axes[i].plot(template_times,
                             template_tr.data / max(template_tr.data),
                             templatecolour, linewidth=1.2)
            ylab = '.'.join([template_tr.stats.station,
                             template_tr.stats.channel])
            axes[i].set_ylabel(ylab, rotation=0,
                               horizontalalignment='right')
            axes[i].yaxis.set_ticks([])
        axes[len(axes) - 1].set_xlabel('Time')
        plt.subplots_adjust(hspace=0, left=0.175, right=0.95, bottom=0.07)
        plt.show()
        return

    def interev_mag_sfiles(sfiles):
        r"""Function to plot interevent-time versus magnitude for series of events.
        **thin** Wrapper for interev_mag.

        :type sfiles: list
        :param sfiles: List of sfiles to read from
        """
        from eqcorrscan.utils import Sfile_util
        times = [Sfile_util.readheader(sfile)[0].origins[0].time
                 for sfile in sfiles]
        mags = [Sfile_util.readheader(sfile)[0].magnitudes[0].mag
                for sfile in sfiles]
        interev_mag(times, mags)

    def interev_mag(times, mags):
        r"""Function to plot interevent times against magnitude for given times
        and magnitudes.

        :type times: list of datetime
        :param times: list of the detection times, must be sorted the same as mags
        :type mags: list of float
        :param mags: list of magnitudes
        """
        l = [(times[i], mags[i]) for i in xrange(len(times))]
        l.sort(key=lambda tup: tup[0])
        times = [x[0] for x in l]
        mags = [x[1] for x in l]
        # Make two subplots next to each other of time before and time after
        fig, axes = plt.subplots(1, 2, sharey=True)
        axes = axes.ravel()
        pre_times = []
        post_times = []
        for i in range(len(times)):
            if i > 0:
                pre_times.append((times[i] - times[i - 1]) / 60)
            if i < len(times) - 1:
                post_times.append((times[i + 1] - times[i]) / 60)
        axes[0].scatter(pre_times, mags[1:])
        axes[0].set_title('Pre-event times')
        axes[0].set_ylabel('Magnitude')
        axes[0].set_xlabel('Time (Minutes)')
        plt.setp(axes[0].xaxis.get_majorticklabels(), rotation=30)
        axes[1].scatter(pre_times, mags[:-1])
        axes[1].set_title('Post-event times')
        axes[1].set_xlabel('Time (Minutes)')
        plt.setp(axes[1].xaxis.get_majorticklabels(), rotation=30)
        plt.show()

    def threeD_seismplot(stations, nodes):
        r"""Function to plot seismicity and stations in a 3D, movable, zoomable \
        space using matplotlibs Axes3D package.

        :type stations: list of tuple
        :param stations: list of one tuple per station of (lat, long, elevation), \
            with up positive.
        :type nodes: list of tuple
        :param nodes: list of one tuple per event of (lat, long, depth) with down \
            positive.
        """
        stalats, stalongs, staelevs = zip(*stations)
        evlats, evlongs, evdepths = zip(*nodes)
        evdepths = [-1 * depth for depth in evdepths]
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
        ax.scatter(evlats, evlongs, evdepths, marker="x", c="k")
        ax.scatter(stalats, stalongs, staelevs, marker="v", c="r")
        ax.set_ylabel("Latitude (deg)")
        ax.set_xlabel("Longitude (deg)")
        ax.set_zlabel("Depth(km)")
        ax.get_xaxis().get_major_formatter().set_scientific(False)
        ax.get_yaxis().get_major_formatter().set_scientific(False)
        plt.show()
        return

    def pretty_template_plot(template, size=(18.5, 10.5), save=False, title=False,
                             background=False, picks=False):
        r"""Function to make a pretty plot of a single template, designed to work \
        better than the default obspy plotting routine for short data lengths.

        :type template: :class: obspy.Stream
        :param template: Template stream to plot
        :type size: tuple
        :param size: tuple of plot size
        :type save: bool
        :param save: if False will plot to screen, if True will save
        :type title: bool
        :param title: String if set will be the plot title
        :type background: :class: obspy.stream
        :param background: Stream to plot the template within.
        :type picks: list of :class: eqcorrscan.utils.Sfile_util.PICK
        :param picks: List of eqcorrscan type picks.
        """
        fig, axes = plt.subplots(len(template), 1, sharex=True, figsize=size)
        if len(template) > 1:
            axes = axes.ravel()
        else:
            return
        if not background:
            mintime = template.sort(['starttime'])[0].stats.starttime
        else:
            mintime = background.sort(['starttime'])[0].stats.starttime
        template.sort(['network', 'station', 'starttime'])
        lengths = []
        for i, tr in enumerate(template):
            delay = tr.stats.starttime - mintime
            y = tr.data
            x = np.linspace(0, len(y) * tr.stats.delta, len(y))
            x += delay
            if background:
                btr = background.select(station=tr.stats.station,
                                        channel=tr.stats.channel)[0]
                bdelay = btr.stats.starttime - mintime
                by = btr.data
                bx = np.linspace(0, len(by) * btr.stats.delta, len(by))
                bx += bdelay
                axes[i].plot(bx, by, 'k', linewidth=1)
                axes[i].plot(x, y, 'r', linewidth=1.1)
                lengths.append(max(bx[-1], x[-1]))
            else:
                axes[i].plot(x, y, 'k', linewidth=1.1)
                lengths.append(x[-1])
            # print(' '.join([tr.stats.station, str(len(x)), str(len(y))]))
            axes[i].set_ylabel('.'.join([tr.stats.station, tr.stats.channel]),
                               rotation=0, horizontalalignment='right')
            axes[i].yaxis.set_ticks([])
            # Plot the picks if they are given
            if picks:
                tr_picks = [pick for pick in picks if
                            pick.station == tr.stats.station and
                            pick.channel[0] + pick.channel[-1] ==
                            tr.stats.channel[0] + tr.stats.channel[-1]]
                for pick in tr_picks:
                    if pick.phase == 'P':
                        pcolor = 'red'
                    elif pick.phase == 'S':
                        pcolor = 'blue'
                    else:
                        pcolor = 'k'
                    pdelay = pick.time - mintime
                    # print(pdelay)
                    axes[i].axvline(x=pdelay, color=pcolor, linewidth=2)
                    # axes[i].plot([pdelay, pdelay], [])
        axes[i].set_xlim([0, max(lengths)])
        axes[len(template)-1].set_xlabel('Time (s) from start of template')
        plt.subplots_adjust(hspace=0, left=0.175, right=0.95, bottom=0.07)
        if title:
            axes[0].set_title(title)
        else:
            plt.subplots_adjust(top=0.98)
        if not save:
            plt.show()
            plt.close()
        else:
            plt.savefig(save)

    def NR_plot(stream, NR_stream, detections, false_detections=False,
                size=(18.5, 10), save=False, title=False):
        r"""Function to plot the Network response alongside the streams used -\
        highlights detection times in the network response.

        :type stream: :class: obspy.Stream
        :param stream: Stream to plot
        :type NR_stream: :class: obspy.Stream
        :param NR_stream: Stream for the network response
        :type detections: list of datetime objects
        :param detections: List of the detections
        :type false_detections: list of datetime
        :param false_detections: Either False (default) or list of false detection\
         times
        :type size: tuple
        :param size: Size of figure, default is (18.5,10)
        :type save: bool
        :param save: Save figure or plot to screen, if not False, must be string\
            of save path.
        :type title: str
        :param title: String for the title of the plot, set to False
        """
        import datetime as dt
        import matplotlib.dates as mdates
        fig, axes = plt.subplots(len(stream)+1, 1, sharex=True, figsize=size)
        if len(stream) > 1:
            axes = axes.ravel()
        else:
            return
        mintime = stream.sort(['starttime'])[0].stats.starttime
        stream.sort(['network', 'station', 'starttime'])
        for i, tr in enumerate(stream):
            delay = tr.stats.starttime - mintime
            delay *= tr.stats.sampling_rate
            y = tr.data
            x = [tr.stats.starttime + dt.timedelta(seconds=s /
                                                   tr.stats.sampling_rate)
                 for s in xrange(len(y))]
            x = mdates.date2num(x)
            axes[i].plot(x, y, 'k', linewidth=1.1)
            axes[i].set_ylabel(tr.stats.station+'.'+tr.stats.channel, rotation=0)
            axes[i].yaxis.set_ticks([])
            axes[i].set_xlim(x[0], x[-1])
        # Plot the network response
        tr = NR_stream[0]
        delay = tr.stats.starttime - mintime
        delay *= tr.stats.sampling_rate
        y = tr.data
        x = [tr.stats.starttime + dt.timedelta(seconds=s / tr.stats.sampling_rate)
             for s in range(len(y))]
        x = mdates.date2num(x)
        axes[i].plot(x, y, 'k', linewidth=1.1)
        axes[i].set_ylabel(tr.stats.station+'.'+tr.stats.channel, rotation=0)
        axes[i].yaxis.set_ticks([])
        axes[-1].set_xlabel('Time')
        axes[-1].set_xlim(x[0], x[-1])
        # Plot the detections!
        ymin, ymax = axes[-1].get_ylim()
        if false_detections:
            for detection in false_detections:
                xd = mdates.date2num(detection)
                axes[-1].plot((xd, xd), (ymin, ymax), 'k--', linewidth=0.5,
                              alpha=0.5)
        for detection in detections:
            xd = mdates.date2num(detection)
            axes[-1].plot((xd, xd), (ymin, ymax), 'r--', linewidth=0.75)
        # Set formatters for x-labels
        mins = mdates.MinuteLocator()
        timedif = tr.stats.endtime.datetime - tr.stats.starttime.datetime
        if timedif.total_seconds() >= 10800 and timedif.total_seconds() <= 25200:
            hours = mdates.MinuteLocator(byminute=[0, 15, 30, 45])
        elif timedif.total_seconds() <= 1200:
            hours = mdates.MinuteLocator(byminute=range(0, 60, 2))
        elif timedif.total_seconds > 25200 and timedif.total_seconds() <= 172800:
            hours = mdates.HourLocator(byhour=range(0, 24, 3))
        elif timedif.total_seconds() > 172800:
            hours = mdates.DayLocator()
        else:
            hours = mdates.MinuteLocator(byminute=range(0, 60, 5))
        hrFMT = mdates.DateFormatter('%Y/%m/%d %H:%M:%S')
        axes[-1].xaxis.set_major_locator(hours)
        axes[-1].xaxis.set_major_formatter(hrFMT)
        axes[-1].xaxis.set_minor_locator(mins)
        plt.gcf().autofmt_xdate()
        axes[-1].fmt_xdata = mdates.DateFormatter('%Y/%m/%d %H:%M:%S')
        plt.subplots_adjust(hspace=0)
        if title:
            axes[0].set_title(title)
        if not save:
            plt.show()
            plt.close()
        else:
            plt.savefig(save)
        return

    def SVD_plot(SVStreams, SValues, stachans, title=False):
        r"""Function to plot the singular vectors from the clustering routines, one\
        plot for each stachan

        :type SVStreams: list of :class:Obspy.Stream
        :param SVStreams: See clustering.SVD_2_Stream - will assume these are\
                ordered by power, e.g. first singular vector in the first stream
        :type SValues: list of float
        :param SValues: List of the singular values corresponding to the SVStreams
        :type stachans: list
        :param stachans: List of station.channel
        """
        for stachan in stachans:
            print(stachan)
            plot_traces = [SVStream.select(station=stachan.split('.')[0],
                                           channel=stachan.split('.')[1])[0]
                           for SVStream in SVStreams]
            fig, axes = plt.subplots(len(plot_traces), 1, sharex=True)
            axes = axes.ravel()
            for i, tr in enumerate(plot_traces):
                y = tr.data
                x = np.linspace(0, len(y) * tr.stats.delta, len(y))
                axes[i].plot(x, y, 'k', linewidth=1.1)
                ylab = 'SV '+str(i+1)+'='+str(round(SValues[i] / len(SValues), 2))
                axes[i].set_ylabel(ylab, rotation=0)
                axes[i].yaxis.set_ticks([])
                print(i)
            axes[-1].set_xlabel('Time (s)')
            plt.subplots_adjust(hspace=0)
            if title:
                axes[0].set_title(title)
            else:
                axes[0].set_title(stachan)
            plt.show()
        return

    def plot_synth_real(real_template, synthetic, channels=False):
        r"""Plot multiple channels of data for real data and synthetic.

        :type real_template: obspy.Stream
        :param real_template: Stream of the real template
        :type synthetic: obspy.Stream
        :param synthetic: Stream of synthetic template
        :type channels: list of str
        :param channels: List of tuples of (station, channel) to plot, default is\
                False, which plots all.
        """
        from obspy.signal.cross_correlation import xcorr
        from obspy import Stream
        colours = ['k', 'r']
        labels = ['Real', 'Synthetic']
        if channels:
            real = []
            synth = []
            for stachan in channels:
                real.append(real_template.select(station=stachan[0],
                                                 channel=stachan[1]))
                synth.append(synthetic.select(station=stachan[0],
                                              channel=stachan[1]))
            real_template = Stream(real)
            synthetic = Stream(synth)

        # Extract the station and channels
        stachans = list(set([(tr.stats.station, tr.stats.channel)
                             for tr in real_template]))
        fig, axes = plt.subplots(len(stachans), 1, sharex=True, figsize=(5, 10))
        axes = axes.ravel()
        for i, stachan in enumerate(stachans):
            real_tr = real_template.select(station=stachan[0],
                                           channel=stachan[1])[0]
            synth_tr = synthetic.select(station=stachan[0],
                                        channel=stachan[1])[0]
            shift, corr = xcorr(real_tr, synth_tr, 2)
            print('Shifting by: '+str(shift)+' samples')
            if corr < 0:
                synth_tr.data = synth_tr.data * -1
                corr = corr * -1
            if shift < 0:
                synth_tr.data = synth_tr.data[abs(shift):]
                real_tr.data = real_tr.data[0:len(synth_tr.data)]
            elif shift > 0:
                real_tr.data = real_tr.data[abs(shift):]
                synth_tr.data = synth_tr.data[0:len(real_tr.data)]
            for j, tr in enumerate([real_tr, synth_tr]):
                y = tr.data
                y = y / float(max(abs(y)))
                x = np.linspace(0, len(y) * tr.stats.delta, len(y))
                axes[i].plot(x, y, colours[j], linewidth=2.0, label=labels[j])
                axes[i].get_yaxis().set_ticks([])
            ylab = stachan[0]+'.'+stachan[1]+' cc='+str(round(corr, 2))
            axes[i].set_ylabel(ylab, rotation=0)
        plt.subplots_adjust(hspace=0)
        # axes[0].legend()
        axes[-1].set_xlabel('Time (s)')
        plt.show()

    def freq_mag(magnitudes, completeness, max_mag, binsize=0.2):
        r"""Function to make a frequency-magnitude histogram and cumulative \
        density plot.  This can compute a b-value, but not a completeness at \
        the moment.  B-value is computed by linear fitting to section of curve \
        between completeness and max_mag.

        :type magnitudes: list
        :param magnitudes: list of float of magnitudes
        :type completeness: float
        :param completeness: Level to compute the b-value above
        :type max_mag: float
        :param max_mag: Maximum magnitude to try and fit a b-value to
        :type binsize: float
        :param binsize: Width of histogram bins, defaults to 0.2
        """
        # Ensure magnitudes are sorted
        magnitudes.sort()
        fig, ax1 = plt.subplots()
        # Set up the bins, the bin-size could be a variables
        bins = np.arange(min(magnitudes), max(magnitudes), binsize)
        n, bins, patches = ax1.hist(magnitudes, bins, facecolor='Black',
                                    alpha=0.5, label='Magnitudes')
        ax1.set_ylabel('Frequency')
        ax1.set_ylim([0, max(n) + 0.5 * max(n)])
        plt.xlabel('Magnitude')
        # Now make the cumulative density function
        cdf = np.arange(len(magnitudes)) / float(len(magnitudes))
        cdf = ((cdf * -1.0) + 1.0) * len(magnitudes)
        ax2 = ax1.twinx()
        ax2.scatter(magnitudes, np.log10(cdf), c='k', marker='+', s=20, lw=2,
                    label='Magnitude cumulative density')
        # Now we want to calculate the b-value and plot the fit
        x = []
        y = []
        for i, magnitude in enumerate(magnitudes):
            if magnitude >= completeness <= max_mag:
                x.append(magnitude)
                y.append(cdf[i])
        fit = np.polyfit(x, np.log10(y), 1)
        fit_fn = np.poly1d(fit)
        ax2.plot(magnitudes, fit_fn(magnitudes), '--k',
                 label='GR trend, b-value = ' + str(abs(fit[0]))[0:4] +
                 '\n $M_C$ = ' + str(completeness))
        ax2.set_ylabel('$Log_{10}$ of cumulative density')
        plt.xlim([min(magnitudes) - 0.5, max(np.log10(cdf)) + 0.2])
        plt.ylim([min(magnitudes) - 0.5, max(np.log10(cdf)) + 1.0])
        plt.legend(loc=2)
        plt.show()

    def spec_trace(traces, cmap=None, wlen=0.4, log=False, trc='k',
                   tralpha=0.9, size=(10, 13), Fig=None, title=None, show=True):
        r"""Wrapper for _spec_trace, take a stream or list of traces and plots \
        the trace with the spectra beneath it - this just does the overseeing to \
        work out if it needs to add subplots or not.

        :type traces: either stream or list of traces
        :param traces: Traces to be plotted, can be a single obspy.Stream, or a \
            list of obspy.Trace
        :type cmap: str
        :param cmap: [Matplotlib colormap](http://matplotlib.org/examples/color/ \
            colormaps_reference.html)
        :type wlen: float
        :param wlen: Window length for fft in seconds
        :type log: bool
        :param log: Use a log frequency scale
        :type trc: str
        :param trc: Color for the trace.
        :type tralpha: float
        :param tralpha: Opacity level for the seismogram, from transparent (0.0) \
            to opaque (1.0).
        :type size: tuple
        :param size: Plot size, tuple of floats, inches
        :type Fig: matplotlib Fig
        :param axes: Figure to plot onto, defaults to self generating.
        :type show: bool
        :param show: To show plot or not, if false, will return Fig.
        """
        from obspy import Stream
        if isinstance(traces, Stream):
            traces.sort(['station', 'channel'])
        if not Fig:
            Fig = plt.figure(figsize=size)
        for i, tr in enumerate(traces):
            if i == 0:
                ax = Fig.add_subplot(len(traces), 1, i+1)
            else:
                ax = Fig.add_subplot(len(traces), 1, i+1, sharex=ax)
            ax1, ax2 = _spec_trace(tr, wlen=wlen, log=log, trc=trc,
                                   tralpha=tralpha, axes=ax)
            ax2.set_yticks([])
            if i < len(traces) - 1:
                plt.setp(ax1.get_xticklabels(), visible=False)
            if type(traces) == list:
                ax2.text(0.005, 0.85, tr.stats.starttime.datetime.
                         strftime('%Y/%m/%d %H:%M:%S'),
                         bbox=dict(facecolor='white', alpha=0.8),
                         transform=ax2.transAxes)
            else:
                ax2.text(0.005, 0.85, '.'.join([tr.stats.station,
                                                tr.stats.channel]),
                         bbox=dict(facecolor='white', alpha=0.8),
                         transform=ax2.transAxes)
            ax2.text(0.005, 0.02, str(np.max(tr.data).round(1)),
                     bbox=dict(facecolor='white', alpha=0.95),
                     transform=ax2.transAxes)
        ax1.set_xlabel('Time (s)')
        Fig.subplots_adjust(hspace=0)
        Fig.text(0.04, 0.5, 'Frequency (Hz)', va='center', rotation='vertical')
        if title:
            plt.suptitle(title)
        if show:
            plt.show()
        else:
            return Fig

    def _spec_trace(trace, cmap=None, wlen=0.4, log=False, trc='k',
                    tralpha=0.9, size=(10, 2.5), axes=None, title=None):
        r"""Function to plot a trace over that traces spectrogram.
        Uses obspys spectrogram routine.

        :type trace: obspy.Trace
        :param trace: trace to plot
        :type cmap: str
        :param cmap: [Matplotlib colormap](http://matplotlib.org/examples/color/
            colormaps_reference.html)
        :type wlen: float
        :param wlen: Window length for fft in seconds
        :type log: bool
        :param log: Use a log frequency scale
        :type trc: str
        :param trc: Color for the trace.
        :type tralpha: float
        :param tralpha: Opacity level for the seismogram, from transparent (0.0) \
            to opaque (1.0).
        :type size: tuple
        :param size: Plot size, tuple of floats, inches
        :type axes: matplotlib axes
        :param axes: Axes to plot onto, defaults to self generating.
        :type title: str
        :param title: Title for the plot.
        """
        if not axes:
            Fig = plt.figure(figsize=size)
            ax1 = Fig.add_subplot(111)
        else:
            ax1 = axes
        trace.spectrogram(wlen=wlen, log=log, show=False, cmap=cmap, axes=ax1)
        Fig = plt.gcf()
        ax2 = ax1.twinx()
        y = trace.data
        x = np.linspace(0, len(y) / trace.stats.sampling_rate, len(y))
        ax2.plot(x, y, color=trc, linewidth=2.0, alpha=tralpha)
        ax2.set_xlim(min(x), max(x))
        ax2.set_ylim(min(y) * 2, max(y) * 2)
        if title:
            ax1.set_title(' '.join([trace.stats.station, trace.stats.channel,
                                    trace.stats.starttime.datetime.
                                    strftime('%Y/%m/%d %H:%M:%S')]))
        if not axes:
            Fig.set_size_inches(size)
            Fig.show()
        else:
            return ax1, ax2

    if __name__ == "__main__":
        import doctest
        doctest.testmod()