Пример #1
0
def segy(ar, outfile_abspath, verbose=False):
    """
    segy output is not yet available
    """
    fx.printmsg(
        'ERROR: SEG-Y is not yet supported, please choose another format.')
    raise NotImplementedError('SEG-Y is not yet supported.')
Пример #2
0
def spectrogram(ar, header, freq, tr='auto', verbose=True):
    """
    Displays a spectrogram of the center trace of the array. This is for testing purposes and not accessible from the command prompt.

    :param numpy.ndarray ar: The radar array
    :param dict header: The file header dictionary
    :type tr: int or str
    :param tr: The trace to display the spectrogram for. Defaults to "auto" but can be an integer representing the trace number to plot. "auto" will pick a trace roughly halfway through the array.
    :param bool verbose: Verbose, defaults to False
    """
    import obspy.imaging.spectrogram as sg  # buried here, to avoid obspy compatibility issues
    if tr == 'auto':
        tr = int(ar.shape[1] / 2)
    if verbose:
        fx.printmsg(
            'converting trace %s to frequency domain and drawing spectrogram...'
            % (tr))
    samp_rate = header['samp_freq']
    trace = ar.T[tr]
    sg.spectrogram(
        data=trace,
        samp_rate=samp_rate,
        wlen=samp_rate / 1000,
        per_lap=0.99,
        dbscale=True,
        title=
        'Trace %s Spectrogram - Antenna Frequency: %.2E Hz - Sampling Frequency: %.2E Hz'
        % (tr, freq, samp_rate))
Пример #3
0
def flip(ar, verbose=False):
    """
    flip radargram horizontally (read backwards)
    """
    if verbose:
        fx.printmsg('flipping radargram...')
    return ar.T[::-1].T
Пример #4
0
def json_header(header, outfile_abspath, verbose=False):
    """
    save header values as a .json so another script can take what it needs
    """
    with open('%s.json' % (outfile_abspath), 'w') as f:
        if verbose:
            fx.printmsg('serializing header as %s' % (f.name))
        json.dump(obj=header, fp=f, indent=4, sort_keys=True, default=str)
Пример #5
0
def reducex(ar, by=1, chnum=1, number=1, verbose=False):
    """
    reduce the number of traces in the array by a number

    not the same as stacking since it doesn't sum adjacent traces
    """
    if verbose:
        fx.printmsg('%s/%s reducing %sx%s chunk by a factor of %s...' % (chnum, number, ar.shape[0], ar.shape[1], by))
    return ar[:,::by]
Пример #6
0
def distance_normalize(header, ar, gps, verbose=False):
    """
    Distance normalization algorithm. Uses a GPS array to calculate expansion and contraction needed to convert from time-triggered to distance-normalized sampling interval. Then, the samples per meter is recalculated and inserted into the header for proper plotting.

    Usage described in the :ref:`Distance normalization` section of the tutorial.

    :param dict header: Input data array
    :param numpy.ndarray ar: Input data array
    :param pandas.DataFrame gps: GPS data from :py:func:`readgssi.gps.readdzg`. This is used to calculate the expansion and compression needed to normalize traces to distance.
    :param bool verbose: Verbose, defaults to False.
    :rtype: header (:py:class:`dict`), radar array (:py:class:`numpy.ndarray`), gps (False or :py:class:`pandas.DataFrame`)

    """
    if ar[2] == []:
        if verbose:
            fx.printmsg('no gps information for distance normalization')
    else:
        if verbose:
            fx.printmsg('normalizing GPS velocity records...')
        while np.min(gps['velocity']) < 0.01: # fix zero and negative velocity values
            gps['velocity'].replace(gps['velocity'].min(), 0.01, inplace=True)
        norm_vel = (gps['velocity'] * (1/gps['velocity'].max())*100).to_frame('normalized') # should end up as dataframe with one column
        # upsample to match radar array shape
        nanosec_samp_rate = int((1/header['rhf_sps'])*10**9) # nanoseconds
        start = np.datetime64(str(norm_vel.index[0])) - np.timedelta64(nanosec_samp_rate*(gps.iloc[0,0]), 'ns')
        newdf = pd.DataFrame(index=pd.date_range(start=start, periods=ar.shape[1], freq=str(nanosec_samp_rate)+'N', tz='UTC'))
        norm_vel = pd.concat([norm_vel, newdf], axis=1).interpolate('time').bfill()
        del newdf
        norm_vel = norm_vel.round().astype(int, casting='unsafe')

        try:
            rm = int(round(ar.shape[1] / (norm_vel.shape[0] - ar.shape[1])))
            norm_vel = norm_vel.drop(norm_vel.index[::rm])
        except ZeroDivisionError as e:
            fx.printmsg('equal length radar & velocity arrays; no size adjustment')
        for i in range(0,abs(norm_vel.shape[0]-ar.shape[1])):
            s = pd.DataFrame({'normalized':[norm_vel['normalized'].iloc[-1]]}) # hacky, but necessary
            norm_vel = pd.concat([norm_vel, s])

        nvm = int(round(norm_vel['normalized'].mean()))
        proc = np.ndarray((ar.shape[0], 0))
        if verbose:
            fx.printmsg('expanding array using mean of normalized velocity %.2f' % (norm_vel['normalized'].mean()))
        on, i = 0, 0
        for c in np.array_split(ar, nvm, axis=1):
            # takes (array, [transform values to broadcast], axis)
            p = np.repeat(c, norm_vel['normalized'].astype(int, casting='unsafe').values[on:on+c.shape[1]], axis=1)
            p = reducex(p, by=nvm, chnum=i, number=nvm, verbose=verbose)
            proc = np.concatenate((proc, p), axis=1)
            on = on + c.shape[1]
            i += 1
        if verbose:
            fx.printmsg('replacing old traces per meter value of %s with %s' % (header['rhf_spm'],
                                                                            ar.shape[1] / gps['meters'].iloc[-1]))
        header['rhf_spm'] = proc.shape[1] / gps['meters'].iloc[-1]
    return header, proc, gps
Пример #7
0
def stack(ar, stack='auto', verbose=False):
    """
    Stacking algorithm. Stacking is the process of summing adjacent traces in order to reduce noise --- the thought being that random noise around zero will cancel out and data will either add or subtract, making it easier to discern.

    It is also useful for displaying long lines on a computer screen. Usage is covered in the :ref:`stacking` section of the tutorial.

    :py:data:`stack='auto'` results in an approximately 2.5:1 x:y axis ratio. :py:data:`stack=3` sums three adjacent traces into a single trace across the width of the array.

    :param numpy.ndarray ar: Input data array
    :param int by: Factor to stack by. Default is "auto".
    :rtype: radar array (:py:class:`numpy.ndarray`)

    """
    stack0 = stack
    if str(stack).lower() in 'auto':
        am = '(automatic)'
        ratio = (ar.shape[1] / ar.shape[0]) / (75 / 30)
        if ratio > 1:
            stack = int(round(ratio))
        else:
            stack = 1
    else:
        am = '(manually set)'
        try:
            stack = int(stack)
        except ValueError:
            fx.printmsg(
                'NOTE: stacking must be indicated with an integer greater than 1, "auto", or None.'
            )
            fx.printmsg(
                '      a stacking value of 1 equates to None. "auto" will attempt to stack to'
            )
            fx.printmsg(
                '      about a 2.5:1 x to y axis ratio. the result will not be stacked.'
            )
            stack = 1
    if stack > 1:
        if verbose:
            fx.printmsg('stacking %sx %s...' % (stack, am))
        i = list(range(stack))
        l = list(range(int(ar.shape[1] / stack)))
        arr = np.copy(reducex(ar=ar, by=stack, verbose=verbose))
        for s in l:
            arr[:, s] = arr[:, s] + ar[:, s * stack + 1:s * stack +
                                       stack].sum(axis=1)
    else:
        arr = ar
        if str(stack0).lower(
        ) in 'auto':  # this happens when distance normalization reduces the file
            pass
        else:
            fx.printmsg(
                'WARNING: no stacking applied. this can result in very large and awkwardly-shaped figures.'
            )
    return arr, stack
Пример #8
0
def flip(ar, verbose=False):
    """
    Read the array backwards. Used to reverse line direction. Usage covered in the :ref:`Reversing` tutorial section.
    
    :param numpy.ndarray ar: Input data array
    :param bool verbose: Verbose, defaults to False
    :rtype: radar array (:py:class:`numpy.ndarray`)

    """
    if verbose:
        fx.printmsg('flipping radargram...')
    return ar.T[::-1].T
Пример #9
0
def json_header(header, outfile_abspath, verbose=False):
    """
    Save header values as a .json so another script can take what it needs. This is used to export to `GPRPy <https://github.com/NSGeophysics/gprpy>`_.

    :param dict header: The file header dictionary
    :param str outfile_abspath: Output file path
    :param bool verbose: Verbose, defaults to False
    """
    with open('%s.json' % (outfile_abspath), 'w') as f:
        if verbose:
            fx.printmsg('serializing header as %s' % (f.name))
        json.dump(obj=header, fp=f, indent=4, sort_keys=True, default=str)
Пример #10
0
def bgr(ar, verbose=False):
    """
    Instrument background removal (BGR)
    Subtracts off row averages
    """
    if verbose:
        fx.printmsg('removing horizontal background...')
    i = 0
    for row in ar:          # each row
        mean = np.mean(row)
        ar[i] = row - mean
        i += 1
    return ar
Пример #11
0
def segy(ar, outfile_abspath, header, verbose=False):
    """
    .. warning:: SEGY output is not yet available.

    In the future, this function will output to SEGY format.

    :param numpy.ndarray ar: Radar array
    :param str outfile_abspath: Output file path
    :param dict header: File header dictionary to write, if desired. Defaults to None.
    :param bool verbose: Verbose, defaults to False
    """
    fx.printmsg(
        'ERROR: SEG-Y is not yet supported, please choose another format.')
    raise NotImplementedError('SEG-Y is not yet supported.')
Пример #12
0
def dewow(ar, verbose=False):
    """
    Polynomial dewow filter
    """
    if verbose:
        fx.printmsg('dewowing data...')
    signal = list(zip(*ar))[10]
    model = np.polyfit(range(len(signal)), signal, 3)
    predicted = list(np.polyval(model, range(len(signal))))
    i = 0
    for column in ar.T:      # each column
        ar.T[i] = column + predicted
        i += 1
    return ar
Пример #13
0
def numpy(ar, outfile_abspath, header=None, verbose=False):
    """
    save as .npy binary numpy file
    """
    if verbose:
        t = ''
        if header:
            t = ' with json header (compatible with GPRPy)'
        fx.printmsg('output format is numpy binary%s' % t)
        fx.printmsg('writing data to %s.npy' % outfile_abspath)
    np.save('%s.npy' % outfile_abspath, ar, allow_pickle=False)
    if header:
        json_header(header=header,
                    outfile_abspath=outfile_abspath,
                    verbose=verbose)
Пример #14
0
def csv(ar, outfile_abspath, header=None, verbose=False):
    """
    outputting to csv is simple. we read into a dataframe, then use pandas' to_csv() builtin function.
    """
    if verbose:
        t = ''
        if header:
            t = ' with json header'
        fx.printmsg('output format is csv%s. writing data to: %s.csv' %
                    (t, outfile_abspath))
    data = pd.DataFrame(ar)  # using pandas to output csv
    data.to_csv('%s.csv' % (outfile_abspath))  # write
    if header:
        json_header(header=header,
                    outfile_abspath=outfile_abspath,
                    verbose=verbose)
Пример #15
0
def distance_normalize(header, ar, gps, verbose=False):
    """
    distance normalization (not pretty but gets the job done)
    """
    if ar[2] == []:
        if verbose:
            fx.printmsg('no gps information for distance normalization')
    else:
        if verbose:
            fx.printmsg('normalizing GPS velocity records...')
        while np.min(gps['velocity']) < 0.01: # fix zero and negative velocity values
            gps['velocity'].replace(gps['velocity'].min(), 0.01, inplace=True)
        norm_vel = (gps['velocity'] * (1/gps['velocity'].max())*100).to_frame('normalized') # should end up as dataframe with one column
        # upsample to match radar array shape
        nanosec_samp_rate = int((1/header['rhf_sps'])*10**9) # nanoseconds
        start = np.datetime64(str(norm_vel.index[0])) - np.timedelta64(nanosec_samp_rate*(gps.iloc[0,0]), 'ns')
        newdf = pd.DataFrame(index=pd.date_range(start=start, periods=ar.shape[1], freq=str(nanosec_samp_rate)+'N', tz='UTC'))
        norm_vel = pd.concat([norm_vel, newdf], axis=1).interpolate('time').bfill()
        del newdf
        norm_vel = norm_vel.round().astype(int, casting='unsafe')

        try:
            rm = int(round(ar.shape[1] / (norm_vel.shape[0] - ar.shape[1])))
            norm_vel = norm_vel.drop(norm_vel.index[::rm])
        except ZeroDivisionError as e:
            fx.printmsg('equal length radar & velocity arrays; no size adjustment')
        for i in range(0,abs(norm_vel.shape[0]-ar.shape[1])):
            s = pd.DataFrame({'normalized':[norm_vel['normalized'].iloc[-1]]}) # hacky, but necessary
            norm_vel = pd.concat([norm_vel, s])

        nvm = int(round(norm_vel['normalized'].mean()))
        proc = np.ndarray((ar.shape[0], 0))
        if verbose:
            fx.printmsg('expanding array using mean of normalized velocity %.2f' % (norm_vel['normalized'].mean()))
        on, i = 0, 0
        for c in np.array_split(ar, nvm, axis=1):
            # takes (array, [transform values to broadcast], axis)
            c = np.repeat(c, norm_vel['normalized'].astype(int, casting='unsafe').values[on:on+c.shape[1]], axis=1)
            c = reducex(c, by=nvm, chnum=i, number=nvm, verbose=verbose)
            proc = np.concatenate((proc, c), axis=1)
            on += c.shape[1]
            i += 1
        if verbose:
            fx.printmsg('replacing old traces per meter value of %s with %s' % (header['rhf_spm'],
                                                                            ar.shape[1] / gps['meters'].iloc[-1]))
        header['rhf_spm'] = ar.shape[1] / gps['meters'].iloc[-1]
    return header, ar, gps
Пример #16
0
def reducex(ar, by=1, chnum=1, number=1, verbose=False):
    """
    Reduce the number of traces in the array by a number. Not the same as :py:func:`stack` since it doesn't sum adjacent traces, however :py:func:`stack` uses it to resize the array prior to stacking.

    Used by :py:func:`stack` and :py:func:`distance_normalize` but not accessible from the command line or :py:func:`readgssi.readgssi`.

    :param numpy.ndarray ar: Input data array
    :param int by: Factor to reduce by. Default is 1.
    :param int chnum: Chunk number to display in console. Default is 1.
    :param int number: Number of chunks to display in console. Default is 1.
    :param bool verbose: Verbose, defaults to False.
    :rtype: radar array (:py:class:`numpy.ndarray`)

    """
    if verbose:
        if chnum/10 == int(chnum/10):
            fx.printmsg('%s/%s reducing %sx%s chunk by a factor of %s...' % (chnum, number, ar.shape[0], ar.shape[1], by))
    return ar[:,::by]
Пример #17
0
def triangular(ar, header, freqmin, freqmax, zerophase=True, verbose=False):
    """
    Vertical triangular FIR bandpass. This filter is designed to closely emulate that of RADAN.

    Filter design is implemented by :py:func:`scipy.signal.firwin` with :code:`numtaps=25` and implemented with :py:func:`scipy.signal.lfilter`.

    .. note:: This function is not compatible with scipy versions prior to 1.3.0.

    :param np.ndarray ar: The radar array
    :param dict header: The file header dictionary
    :param int freqmin: The lower corner of the bandpass
    :param int freqmax: The upper corner of the bandpass
    :param bool zerophase: Whether to run the filter forwards and backwards in order to counteract the phase shift
    :param bool verbose: Verbose, defaults to False
    :rtype: :py:class:`numpy.ndarray`
    """
    if verbose:
        fx.printmsg('vertical triangular FIR bandpass filter')
    #samp_freq = 1 / ((header['rhf_depth'] * 2) / header['cr'] / header['rh_nsamp'])
    samp_freq = header['samp_freq']
    freqmin = freqmin * 10**6
    freqmax = freqmax * 10**6

    numtaps = 25

    if verbose:
        fx.printmsg('sampling frequency:       %.2E Hz' % samp_freq)
        fx.printmsg('minimum filter frequency: %.2E Hz' % freqmin)
        fx.printmsg('maximum filter frequency: %.2E Hz' % freqmax)
        fx.printmsg('numtaps: %s, zerophase: %s' % (numtaps, zerophase))

    filt = firwin(numtaps=numtaps,
                  cutoff=[freqmin, freqmax],
                  window='triangle',
                  pass_zero='bandpass',
                  fs=samp_freq)

    far = lfilter(filt, 1.0, ar, axis=0).copy()
    if zerophase:
        far = lfilter(filt, 1.0, far[::-1], axis=0)[::-1]

    return far
Пример #18
0
def numpy(ar, outfile_abspath, header=None, verbose=False):
    """
    Output to binary numpy binary file (.npy) with the option of writing the header to .json as well.

    :param numpy.ndarray ar: Radar array
    :param str outfile_abspath: Output file path
    :param dict header: File header dictionary to write, if desired. Defaults to None.
    :param bool verbose: Verbose, defaults to False
    """
    if verbose:
        t = ''
        if header:
            t = ' with json header (compatible with GPRPy)'
        fx.printmsg('output format is numpy binary%s' % t)
        fx.printmsg('writing data to %s.npy' % outfile_abspath)
    np.save('%s.npy' % outfile_abspath, ar, allow_pickle=False)
    if header:
        json_header(header=header,
                    outfile_abspath=outfile_abspath,
                    verbose=verbose)
Пример #19
0
def histogram(ar, verbose=True):
    """
    shows a y-log histogram of data value distribution
    """
    mean = np.mean(ar)
    std = np.std(ar)
    ll = mean - (std * 3)  # lower color limit
    ul = mean + (std * 3)  # upper color limit

    if verbose:
        fx.printmsg('drawing log histogram...')
        fx.printmsg(
            'mean:               %s (if high, use background removal)' % mean)
        fx.printmsg('stdev:              %s' % std)
        fx.printmsg('lower limit:        %s [mean - (3 * stdev)]' % ll)
        fx.printmsg('upper limit:        %s [mean + (3 * stdev)]' % ul)
    fig = plt.figure()
    hst = plt.hist(ar.ravel(), bins=256, range=(ll, ul), fc='k', ec='k')
    plt.yscale('log', nonposy='clip')
    plt.show()
Пример #20
0
def dewow(ar, verbose=False):
    """
    Polynomial dewow filter. Written by fxsimon.
    
    .. warning:: This filter is still experimental.

    :param numpy.ndarray ar: The radar array
    :param bool verbose: Verbose, default is False
    :rtype: :py:class:`numpy.ndarray`
    """
    fx.printmsg('WARNING: dewow filter is experimental')
    if verbose:
        fx.printmsg('dewowing data...')
    signal = list(zip(*ar))[10]
    model = np.polyfit(range(len(signal)), signal, 3)
    predicted = list(np.polyval(model, range(len(signal))))
    i = 0
    for column in ar.T:  # each column
        ar.T[i] = column + predicted
        i += 1
    return ar
Пример #21
0
def csv(ar, outfile_abspath, header=None, verbose=False):
    """
    Output to csv. Data is read into a :py:class:`pandas.DataFrame`, then written using :py:func:`pandas.DataFrame.to_csv`.

    :param numpy.ndarray ar: Radar array
    :param str outfile_abspath: Output file path
    :param dict header: File header dictionary to write, if desired. Defaults to None.
    :param bool verbose: Verbose, defaults to False
    """
    if verbose:
        t = ''
        if header:
            t = ' with json header'
        fx.printmsg('output format is csv%s. writing data to: %s.csv' %
                    (t, outfile_abspath))
    data = pd.DataFrame(ar)  # using pandas to output csv
    data.to_csv('%s.csv' % (outfile_abspath))  # write
    if header:
        json_header(header=header,
                    outfile_abspath=outfile_abspath,
                    verbose=verbose)
Пример #22
0
def spectrogram(ar, header, freq, verbose=True):
    """
    displays a spectrogram of the center trace of the array

    this is for testing purposes and not accessible from the command prompt
    """
    tr = int(ar.shape[1] / 2)
    if verbose:
        fx.printmsg(
            'converting trace %s to frequency domain and drawing spectrogram...'
            % (tr))
    samp_rate = 1 / (header['rhf_depth'] / header['cr'] / header['rh_nsamp'])
    trace = ar.T[tr]
    sg.spectrogram(
        data=trace,
        samp_rate=samp_rate,
        wlen=samp_rate / 1000,
        per_lap=0.99,
        dbscale=True,
        title=
        'Trace %s Spectrogram - Antenna Frequency: %.2E Hz - Sampling Frequency: %.2E Hz'
        % (tr, freq, samp_rate))
Пример #23
0
def stack(ar, stack='auto', verbose=False):
    """
    stacking algorithm

    stack='auto' results in an approximately 2.5:1 x:y axis ratio
    """
    stack0 = stack
    if str(stack).lower() in 'auto':
        am = '(automatic)'
        ratio = (ar.shape[1]/ar.shape[0])/(75/30)
        if ratio > 1:
            stack = int(round(ratio))
        else:
            stack = 1
    else:
        am = '(manually set)'
        try:
            stack = int(stack)
        except ValueError:
            fx.printmsg('ERROR: stacking must be indicated with an integer greater than 1, "auto", or None.')
            fx.printmsg('a stacking value of 1 equates to None. "auto" will attempt to stack to about a 2.5:1 x to y axis ratio.')
            fx.printmsg('the result will not be stacked.')
            stack = 1
    if stack > 1:
        if verbose:
            fx.printmsg('stacking %sx %s...' % (stack, am))
        i = list(range(stack))
        l = list(range(int(ar.shape[1]/stack)))
        arr = np.copy(reducex(ar=ar, by=stack, verbose=verbose))
        for s in l:
            arr[:,s] = arr[:,s] + ar[:,s*stack+1:s*stack+stack].sum(axis=1)
    else:
        arr = ar
        if str(stack0).lower() in 'auto': # this happens when distance normalization reduces the file
            pass
        else:
            fx.printmsg('WARNING: no stacking applied. be warned: this can result in very large and awkwardly-shaped figures.')
    return arr, stack
Пример #24
0
def bgr(ar, header, win=0, verbose=False):
    """
    Horizontal background removal (BGR). Subtracts off row averages for full-width or window-length slices. For usage see :ref:`Getting rid of horizontal noise`.

    :param numpy.ndarray ar: The radar array
    :param dict header: The file header dictionary
    :param int win: The window length to process. 0 resolves to full-width, whereas positive integers dictate the window size in post-stack traces.
    :rtype: :py:class:`numpy.ndarray`
    """
    if (int(win) > 1) & (int(win) < ar.shape[1]):
        window = int(win)
        how = 'boxcar (%s trace window)' % window
    else:
        how = 'full only'
    if verbose:
        fx.printmsg('removing horizontal background using method=%s...' %
                    (how))
    i = 0
    for row in ar:  # each row
        mean = np.mean(row)
        ar[i] = row - mean
        i += 1
    if how != 'full only':
        if window < 10:
            fx.printmsg(
                'WARNING: BGR window size is very short. be careful, this may obscure horizontal layering'
            )
        if window < 3:
            window = 3
        elif (window / 2. == int(window / 2)):
            window = window + 1
        ar -= uniform_filter1d(ar,
                               size=window,
                               mode='constant',
                               cval=0,
                               axis=1)

    return ar
Пример #25
0
def bp(ar, header, freqmin, freqmax, verbose=False):
    """
    Vertical frequency domain bandpass
    """
    if verbose:
        fx.printmsg('vertical frequency filtering...')
    samp_freq = 1 / (header['rhf_depth'] / header['cr'] / header['rh_nsamp'])
    freqmin = freqmin * 10 ** 6
    freqmax = freqmax * 10 ** 6
    
    if verbose:
        fx.printmsg('Sampling frequency:       %.2E Hz' % samp_freq)
        fx.printmsg('Minimum filter frequency: %.2E Hz' % freqmin)
        fx.printmsg('Maximum filter frequency: %.2E Hz' % freqmax)
    
    i = 0
    for t in ar.T:
        f = bandpass(data=t, freqmin=freqmin, freqmax=freqmax, df=samp_freq, corners=2, zerophase=False)
        ar[:,i] = f
        i += 1
    return ar
Пример #26
0
def readdzg(fi, frmt, header, verbose=False):
    """
    A parser to extract gps data from DZG file format. DZG contains raw NMEA sentences, which should include at least RMC and GGA.

    NMEA RMC sentence string format:
    :py:data:`$xxRMC,UTC hhmmss,status,lat DDmm.sss,lon DDDmm.sss,SOG,COG,date ddmmyy,checksum \*xx`

    NMEA GGA sentence string format:
    :py:data:`$xxGGA,UTC hhmmss.s,lat DDmm.sss,lon DDDmm.sss,fix qual,numsats,hdop,mamsl,wgs84 geoid ht,fix age,dgps sta.,checksum \*xx`
    
    Shared message variables between GGA and RMC: timestamp, latitude, and longitude

    RMC contains a datestamp which makes it preferable, but this parser will read either.

    :param str fi: File containing gps information
    :param str frmt: GPS information format ('dzg' = DZG file containing gps sentence strings (see below); 'csv' = comma separated file with: lat,lon,elev,time)
    :param dict header: File header produced by :py:func:`readgssi.dzt.readdzt`
    :param bool verbose: Verbose, defaults to False
    :rtype: GPS data (pandas.DataFrame)

        The dataframe contains the following fields:
        * datetimeutc (:py:class:`datetime.datetime`)
        * trace (:py:class:`int` trace number)
        * longitude (:py:class:`float`)
        * latitude (:py:class:`float`)
        * altitude (:py:class:`float`)
        * velocity (:py:class:`float`)
        * sec_elapsed (:py:class:`float`)
        * meters (:py:class:`float` meters traveled)

    """
    if header['rhf_spm'] == 0:
        spu = header['rhf_sps']
    else:
        spu = header['rhf_spm']
    array = pd.DataFrame(columns=['datetimeutc', 'trace', 'longitude', 'latitude', # our dataframe
                                  'altitude', 'velocity', 'sec_elapsed', 'meters'])

    trace = 0 # the elapsed number of traces iterated through
    tracenum = 0 # the sequential increase in trace number
    rownp = 0 # array row number
    rowrmc = 0 # rmc record iterated through (gps file)
    rowgga = 0 # gga record
    sec_elapsed = 0 # number of seconds since the start of the line
    m = 0 # meters traveled over entire line
    m0, m1 = 0, 0 # meters traveled as of last, current loop
    u = 0 # velocity
    u0 = 0 # velocity on last loop
    timestamp = False
    prevtime = False
    init_time = False
    td = False
    prevtrace = False
    rmc, gga = False, False
    rmcwarn = True
    lathem = 'north'
    lonhem = 'east'
    x0, x1, y0, y1 = False, False, False, False # coordinates
    z0, z1 = 0, 0
    x2, y2, z2, sec2 = 0, 0, 0, 0
    with open(fi, 'r') as gf:
        if verbose:
            fx.printmsg('using gps file:     %s' % (fi))
        if frmt == 'dzg': # if we're working with DZG format
            for ln in gf: # loop through the first few sentences, check for RMC
                if 'RMC' in ln: # check to see if RMC sentence (should occur before GGA)
                    rmc = True
                    if rowrmc == 0:
                        msg = pynmea2.parse(ln.rstrip()) # convert gps sentence to pynmea2 named tuple
                        ts0 = TZ.localize(datetime.combine(msg.datestamp, msg.timestamp)) # row 0's timestamp (not ideal)
                    if rowrmc == 1:
                        msg = pynmea2.parse(ln.rstrip())
                        ts1 = TZ.localize(datetime.combine(msg.datestamp, msg.timestamp)) # row 1's timestamp (not ideal)
                        td = ts1 - ts0 # timedelta = datetime1 - datetime0
                    rowrmc += 1
                if 'GGA' in ln:
                    gga = True
                    if rowgga == 0:
                        msg = pynmea2.parse(ln.rstrip()) # convert gps sentence to pynmea2 named tuple
                        ts0 = TZ.localize(datetime.combine(datetime(1980, 1, 1), msg.timestamp)) # row 0's timestamp (not ideal)
                    if rowgga == 1:
                        msg = pynmea2.parse(ln.rstrip())
                        ts1 = TZ.localize(datetime.combine(datetime(1980, 1, 1), msg.timestamp)) # row 1's timestamp (not ideal)
                        td = ts1 - ts0 # timedelta = datetime1 - datetime0
                    rowgga += 1
            gpssps = 1 / td.total_seconds() # GPS samples per second
            if (rmcwarn) and (rowrmc == 0):
                fx.printmsg('WARNING: no RMC sentences found in GPS records. this could become an issue if your file goes through 00:00:00.')
                fx.printmsg("         if you get a time jump error please open a github issue at https://github.com/iannesbitt/readgssi/issues")
                fx.printmsg("         and attach the verbose output of this script plus a zip of the DZT and DZG files you're working with.")
                rmcwarn = False
            if (rmc and gga) and (rowrmc != rowgga):
                fx.printmsg('WARNING: GGA and RMC sentences are not recorded at the same rate! This could cause unforseen problems!')
                fx.printmsg('    rmc: %i records' % rowrmc)
                fx.printmsg('    gga: %i records' % rowgga)
            if verbose:
                ss0, ss1, ss2 = '', '', ''
                if gga:
                    ss0 = 'GGA'
                if rmc:
                    ss2 = 'RMC'
                if gga and rmc:
                    ss1 = ' and '
                fx.printmsg('found %i %s%s%s GPS epochs at rate of ~%.2f Hz' % (rowrmc, ss0, ss1, ss2, gpssps))
                fx.printmsg('reading gps locations to data frame...')

            gf.seek(0) # back to beginning of file
            rowgga, rowrmc = 0, 0
            for ln in gf: # loop over file line by line
                if '$GSSIS' in ln:
                    # if it's a GSSI sentence, grab the scan/trace number
                    trace = int(ln.split(',')[1])

                if (rmc and gga) and ('GGA' in ln):
                    # RMC doesn't use altitude so if it exists we include it from a neighboring GGA
                    z1 = pynmea2.parse(ln.rstrip()).altitude
                    if rowrmc != rowgga:
                        # this takes care of the case where RMC lines occur above GGA
                        z0 = array['altitude'].iat[rowgga]
                        array['altitude'].iat[rowgga] = z1
                    rowgga += 1

                if rmc == True: # if there is RMC, we can use the full datestamp but there is no altitude
                    if 'RMC' in ln:
                        msg = pynmea2.parse(ln.rstrip())
                        timestamp = TZ.localize(datetime.combine(msg.datestamp, msg.timestamp)) # set t1 for this loop
                        u = msg.spd_over_grnd * 0.514444444 # convert from knots to m/s

                        sec1 = timestamp.timestamp()
                        x1, y1 = float(msg.longitude), float(msg.latitude)
                        if msg.lon_dir in 'W':
                            lonhem = 'west'
                        if msg.lat_dir in 'S':
                            lathem = 'south'
                        if rowrmc != 0:
                            elapsedelta = timestamp - prevtime # t1 - t0 in timedelta format
                            elapsed = float((timestamp-init_time).total_seconds()) # seconds elapsed
                            m += u * elapsedelta.total_seconds()
                        else:
                            u = 0
                            m = 0
                            elapsed = 0
                            if verbose:
                                fx.printmsg('record starts in %s and %s hemispheres' % (lonhem, lathem))
                        x0, y0, z0, sec0, m0 = x1, y1, z1, sec1, m # set xyzs0 for next loop
                        prevtime = timestamp # set t0 for next loop
                        if rowrmc == 0:
                            init_time = timestamp
                        prevtrace = trace
                        array = array.append({'datetimeutc':timestamp.strftime('%Y-%m-%d %H:%M:%S.%f %z'),
                                              'trace':trace, 'longitude':x1, 'latitude':y1, 'altitude':z1,
                                              'velocity':u, 'sec_elapsed':elapsed, 'meters':m}, ignore_index=True)
                        rowrmc += 1

                else: # if no RMC, we hope there is no UTC 00:00:00 in the file.........
                    if 'GGA' in ln:
                        msg = pynmea2.parse(ln.rstrip())
                        timestamp = TZ.localize(datetime.combine(header['rhb_cdt'], msg.timestamp)) # set t1 for this loop

                        sec1 = timestamp.timestamp()
                        x1, y1 = float(msg.longitude), float(msg.latitude)
                        try:
                            z1 = float(msg.altitude)
                        except AttributeError:
                            z1 = 0
                        if msg.lon_dir in 'W':
                            lonhem = 'west'
                        if msg.lat_dir in 'S':
                            lathem = 'south'
                        if rowgga != 0:
                            m += geodesic((y1, x1, z1), (y0, x0, z0)).meters
                            if rmc == False:
                                u = float((m - m0) / (sec1 - sec0))
                            elapsedelta = timestamp - prevtime # t1 - t0 in timedelta format
                            elapsed = float((timestamp-init_time).total_seconds()) # seconds elapsed
                            if elapsed > 3600.0:
                                fx.printmsg("WARNING: Time jumps by more than an hour in this GPS dataset and there are no RMC sentences to anchor the datestamp!")
                                fx.printmsg("         This dataset may cross over the UTC midnight dateline!\nprevious timestamp: %s\ncurrent timestamp:  %s" % (prevtime, timestamp))
                                fx.printmsg("         trace number:       %s" % trace)
                        else:
                            u = 0
                            m = 0
                            elapsed = 0
                            if verbose:
                                fx.printmsg('record starts in %s and %s hemispheres' % (lonhem, lathem))
                        x0, y0, z0, sec0, m0 = x1, y1, z1, sec1, m # set xyzs0 for next loop
                        prevtime = timestamp # set t0 for next loop
                        if rowgga == 0:
                            init_time = timestamp
                        prevtrace = trace
                        array = array.append({'datetimeutc':timestamp.strftime('%Y-%m-%d %H:%M:%S.%f %z'),
                                              'trace':trace, 'longitude':x1, 'latitude':y1, 'altitude':z1,
                                              'velocity':u, 'sec_elapsed':elapsed, 'meters':m}, ignore_index=True)
                        rowgga += 1

            if verbose:
                if rmc:
                    fx.printmsg('processed %i gps epochs (RMC)' % (rowrmc))
                else:
                    fx.printmsg('processed %i gps epochs (GGA)' % (rowgga))

        elif frmt == 'csv':
            with open(fi, 'r') as f:
                gps = np.fromfile(f)

    array['datetimeutc'] = pd.to_datetime(array['datetimeutc'], format='%Y-%m-%d %H:%M:%S.%f +0000', utc=True)
    array.set_index('datetimeutc', inplace=True)
    ## testing purposes
    # if True:
    #     if verbose:
    #         fx.printmsg('writing GPS to %s-gps.csv' % (fi))
    #     array.to_csv('%s-gps.csv' % (fi))
    return array
Пример #27
0
def main():
    """
    This function gathers and parses command line arguments with which to create function calls. It is not for use from the python console.
    """

    verbose = True
    title = True
    stack = 1
    win = 0
    dpi = 150
    zero = [None, None, None, None]
    zoom = [0, 0, 0, 0]
    infile, outfile, antfreq, frmt, plotting, figsize, histogram, colorbar, dewow, bgr, noshow = None, None, None, None, None, None, None, None, None, None, None
    reverse, freqmin, freqmax, specgram, normalize, spm, epsr = None, None, None, None, None, None, None
    colormap = 'gray'
    x, z = 'seconds', 'nanoseconds'
    gain = 1

    # some of this needs to be tweaked to formulate a command call to one of the main body functions
    # variables that can be passed to a body function: (infile, outfile, antfreq=None, frmt, plotting=False, stack=1)
    try:
        opts, args = getopt.getopt(
            sys.argv[1:], 'hVqd:i:a:o:f:p:s:r:RNwnmc:bg:Z:E:t:x:z:Te:D:', [
                'help', 'version', 'quiet', 'spm=', 'input=', 'antfreq=',
                'output=', 'format=', 'plot=', 'stack=', 'bgr=', 'reverse',
                'normalize', 'dewow', 'noshow', 'histogram', 'colormap=',
                'colorbar', 'gain=', 'zero=', 'epsr=', 'bandpass='******'xscale=',
                'zscale=', 'titleoff', 'zoom=', 'dpi='
            ])
    # the 'no option supplied' error
    except getopt.GetoptError as e:
        fx.printmsg('ERROR: invalid argument(s) supplied')
        fx.printmsg('error text: %s' % e)
        fx.printmsg(config.help_text)
        sys.exit(2)
    for opt, arg in opts:
        if opt in ('-h', '--help'):  # the help case
            fx.printmsg(config.help_text)
            sys.exit()
        if opt in ('-V', '--version'):  # the help case
            print(config.version_text)
            sys.exit()
        if opt in ('-q', '--quiet'):
            verbose = False
        if opt in ('-i', '--input'):  # the input file
            if arg:
                infile = arg
                if '~' in infile:
                    infile = os.path.expanduser(
                        infile
                    )  # if using --input=~/... tilde needs to be expanded
        if opt in ('-o', '--output'):  # the output file
            if arg:
                outfile = arg
                if '~' in outfile:
                    outfile = os.path.expanduser(
                        outfile)  # expand tilde, see above
        if opt in ('-a', '--antfreq'):
            try:
                antfreq = round(abs(float(arg)), 1)
                fx.printmsg(
                    'user specified frequency value of %s MHz will be overwritten if DZT header has valid antenna information.'
                    % antfreq)
            except ValueError:
                fx.printmsg(
                    'ERROR: %s is not a valid decimal or integer frequency value.'
                    % arg)
                fx.printmsg(config.help_text)
                sys.exit(2)
        if opt in ('-f', '--format'):  # the format string
            # check whether the string is a supported format
            if arg:
                arg = arg.lower()
                if arg in ('csv', '.csv'):
                    frmt = 'csv'
                elif arg in ('sgy', 'segy', 'seg-y', '.sgy', '.segy',
                             '.seg-y'):
                    frmt = 'segy'
                elif arg in ('h5', 'hdf5', '.h5', '.hdf5'):
                    frmt = 'h5'
                elif arg in ('numpy', 'npy', '.npy', 'np'):
                    frmt = 'numpy'
                elif arg in ('gprpy'):
                    frmt = 'gprpy'
                elif arg in ('plot', 'png'):
                    plotting = True
                else:
                    # else the user has given an invalid format
                    fx.printmsg(config.help_text)
                    sys.exit(2)
            else:
                fx.printmsg(config.help_text)
                sys.exit(2)
        if opt in ('-s', '--stack'):
            if arg:
                if 'auto' in str(arg).lower():
                    stack = 'auto'
                else:
                    try:
                        stack = abs(int(arg))
                    except ValueError:
                        fx.printmsg(
                            'ERROR: stacking argument must be a positive integer or "auto".'
                        )
                        fx.printmsg(config.help_text)
                        sys.exit(2)
        if opt in ('-r', '--bgr'):
            bgr = True
            if arg:
                try:
                    win = abs(int(arg))
                except:
                    fx.printmsg(
                        'ERROR: background removal window must be a positive integer. defaulting to full width.'
                    )
        if opt in ('-w', '--dewow'):
            dewow = True
        if opt in ('-R', '--reverse'):
            reverse = True
        if opt in ('-N', '--normalize'):
            normalize = True
        if opt in ('-Z', '--zero'):
            if arg:
                try:
                    zero = list(map(int, arg.split(',')))
                except:
                    fx.printmsg(
                        'ERROR: zero correction must be an integer or list')
            else:
                fx.printmsg('WARNING: no zero correction argument supplied')
        if opt in ('-t', '--bandpass'):
            if arg:
                freqmin, freqmax = arg.split('-')
                try:
                    freqmin = int(freqmin)
                    freqmax = int(freqmax)
                except:
                    fx.printmsg(
                        'ERROR: filter frequency must be integers separated by a dash (-)'
                    )
                    freqmin, freqmax = None, None
            else:
                fx.printmsg('WARNING: no filter frequency argument supplied')
        if opt in ('-n', '--noshow'):
            noshow = True
        if opt in ('-p', '--plot'):
            plotting = True
            if arg:
                if 'auto' in arg.lower():
                    figsize = 8
                else:
                    try:
                        figsize = abs(int(arg))
                    except ValueError:
                        fx.printmsg(
                            'ERROR: plot size argument must be a positive integer or "auto".'
                        )
                        fx.printmsg(config.help_text)
                        sys.exit(2)
        if opt in ('-d', '--spm'):
            if arg:
                try:
                    spm = float(arg)
                    assert spm > 0
                except:
                    fx.printmsg(
                        'ERROR: samples per meter must be positive. defaulting to read from header'
                    )
                    spm = None
            else:
                fx.printmsg('WARNING: no samples per meter value given')
        if opt in ('-x', '--xscale'):
            if arg:
                if arg in ('temporal', 'time', 'seconds', 's'):
                    x = 'seconds'
                elif arg in ('spatial', 'distance', 'dist', 'length', 'meters',
                             'm'):
                    x = 'm'
                elif arg in ('centimeters', 'cm'):
                    x = 'cm'
                elif arg in ('kilometers', 'km'):
                    x = 'km'
                elif arg in ('traces', 'samples', 'pulses', 'columns'):
                    x = 'traces'
                else:
                    fx.printmsg(
                        'WARNING: invalid xscale type specified. defaulting to --xscale="seconds"'
                    )
                    x = 'seconds'
            else:
                fx.printmsg(
                    'WARNING: no xscale type specified. defaulting to --xscale="seconds"'
                )
                x = 'seconds'
        if opt in ('-z', '--zscale'):
            if arg:
                if arg in ('temporal', 'time', 'nanoseconds', 'ns'):
                    z = 'nanoseconds'
                elif arg in ('spatial', 'distance', 'depth', 'length',
                             'meters', 'm'):
                    z = 'm'
                elif arg in ('centimeters', 'cm'):
                    z = 'cm'
                elif arg in ('millimeters', 'mm'):
                    z = 'mm'
                elif arg in ('samples', 'rows'):
                    z = 'samples'
                else:
                    fx.printmsg(
                        'WARNING: invalid zscale type specified. defaulting to --zscale="nanoseconds"'
                    )
                    z = 'nanoseconds'
            else:
                fx.printmsg(
                    'WARNING: no zscale type specified. defaulting to --zscale="nanoseconds"'
                )
                z = 'nanoseconds'
        if opt in ('-E', '--epsr'):
            try:
                epsr = float(arg)
                if epsr <= 1:
                    raise Exception
            except:
                print(
                    'ERROR: invalid value for epsr (epsilon sub r "dielectric permittivity"). using DZT value instead.'
                )
                epsr = None
        if opt in ('-m', '--histogram'):
            histogram = True
        if opt in ('-c', '--colormap'):
            if arg:
                colormap = arg
        if opt in ('-b', '--colorbar'):
            colorbar = True
        if opt in ('-g', '--gain'):
            if arg:
                try:
                    gain = abs(float(arg))
                except:
                    fx.printmsg(
                        'ERROR: gain must be positive. defaulting to gain=1.')
                    gain = 1
        if opt in ('-T', '--titleoff'):
            title = False
        if opt in ('-e', '--zoom'):
            if arg:
                if True:  #try:
                    zoom = list(map(int, arg.split(',')))
                    if len(zoom) != 4:
                        fx.printmsg(
                            'ERROR: zoom must be a list of four numbers (zeros are accepted).'
                        )
                        fx.printmsg('       defaulting to full extents.')
                        zoom = [0, 0, 0, 0]
                # except Exception as e:
                #     fx.printmsg('ERROR setting zoom values. zoom must be a list of four numbers (zeros are accepted).')
                #     fx.printmsg('       defaulting to full extents.')
                #     fx.printmsg('details: %s' % e)
        if opt in ('-D', '--dpi'):
            if arg:
                try:
                    dpi = abs(int(arg))
                    assert dpi > 0
                except:
                    fx.printmsg(
                        'WARNING: DPI could not be set. did you supply a positive integer?'
                    )

    # call the function with the values we just got
    if infile:
        if verbose:
            fx.printmsg(config.dist)
        readgssi(infile=infile,
                 outfile=outfile,
                 antfreq=antfreq,
                 frmt=frmt,
                 plotting=plotting,
                 dpi=dpi,
                 figsize=figsize,
                 stack=stack,
                 verbose=verbose,
                 histogram=histogram,
                 x=x,
                 z=z,
                 colormap=colormap,
                 colorbar=colorbar,
                 reverse=reverse,
                 gain=gain,
                 bgr=bgr,
                 win=win,
                 zero=zero,
                 normalize=normalize,
                 dewow=dewow,
                 noshow=noshow,
                 freqmin=freqmin,
                 freqmax=freqmax,
                 spm=spm,
                 epsr=epsr,
                 title=title,
                 zoom=zoom)
        if verbose:
            fx.printmsg('done with %s' % infile)
        print('')
    else:
        fx.printmsg('ERROR: no input file was specified')
        fx.printmsg(config.help_text)
        sys.exit(2)
Пример #28
0
def readgssi(infile,
             outfile=None,
             verbose=False,
             antfreq=None,
             frmt='python',
             plotting=False,
             figsize=7,
             dpi=150,
             stack=1,
             x='seconds',
             z='nanoseconds',
             histogram=False,
             colormap='gray',
             colorbar=False,
             zero=[None, None, None, None],
             gain=1,
             freqmin=None,
             freqmax=None,
             reverse=False,
             bgr=False,
             win=0,
             dewow=False,
             normalize=False,
             specgram=False,
             noshow=False,
             spm=None,
             start_scan=0,
             num_scans=-1,
             epsr=None,
             title=True,
             zoom=[0, 0, 0, 0]):
    """
    This is the primary directive function. It coordinates calls to reading, filtering, translation, and plotting functions, and should be used as the overarching processing function in most cases.

    :param str infile: Input DZT data file
    :param str outfile: Base output file name for plots, CSVs, and other products. Defaults to :py:data:`None`, which will cause the output filename to take a form similar to the input. The default will let the file be named via the descriptive naming function :py:data:`readgssi.functions.naming()`.
    :param bool verbose: Whether or not to display (a lot of) information about the workings of the program. Defaults to :py:data:`False`. Can be helpful for debugging but also to see various header values and processes taking place.
    :param int antfreq: User setting for antenna frequency. Defaults to :py:data:`None`, which will cause the program to try to determine the frequency from the antenna name in the header of the input file. If the antenna name is not in the dictionary :py:data:`readgssi.constants.ANT`, the function will try to determine the frequency by decoding integers in the antenna name string.
    :param str frmt: The output format to be passed to :py:mod:`readgssi.translate`. Defaults to :py:data:`None`. Presently, this can be set to :py:data:`frmt='csv'`, :py:data:`'numpy'`, :py:data:`'gprpy'`, or :py:data:`'object'` (which will return the header dictionary, the image arrays, and the gps coordinates as objects). Plotting will not interfere with output (i.e. you can output to CSV and plot a PNG in the same command).
    :param bool plotting: Whether to plot the radargram using :py:func:`readgssi.plot.radargram`. Defaults to :py:data:`False`.
    :param int figsize: Plot size in inches to be passed to :py:func:`readgssi.plot.radargram`.
    :param int dpi: Dots per inch (DPI) for figure creation.
    :param int stack: Number of consecutive traces to stack (horizontally) using :py:func:`readgssi.arrayops.stack`. Defaults to 1 (no stacking). Especially good for handling long radar lines. Algorithm combines consecutive traces together using addition, which reduces noise and enhances signal. The more stacking is done, generally the clearer signal will become. The tradeoff is that you will reduce the length of the X-axis. Sometimes this is desirable (i.e. for long survey lines).
    :param str x: The units to display on the x-axis during plotting. Defaults to :py:data:`x='seconds'`. Acceptable values are :py:data:`x='distance'` (which sets to meters), :py:data:`'km'`, :py:data:`'m'`, :py:data:`'cm'`, :py:data:`'mm'`, :py:data:`'kilometers'`, :py:data:`'meters'`, etc., for distance; :py:data:`'seconds'`, :py:data:`'s'`, :py:data:`'temporal'` or :py:data:`'time'` for seconds, and :py:data:`'traces'`, :py:data:`'samples'`, :py:data:`'pulses'`, or :py:data:`'columns'` for traces.
    :param str z: The units to display on the z-axis during plotting. Defaults to :py:data:`z='nanoseconds'`. Acceptable values are :py:data:`z='depth'` (which sets to meters), :py:data:`'m'`, :py:data:`'cm'`, :py:data:`'mm'`, :py:data:`'meters'`, etc., for depth; :py:data:`'nanoseconds'`, :py:data:`'ns'`, :py:data:`'temporal'` or :py:data:`'time'` for seconds, and :py:data:`'samples'` or :py:data:`'rows'` for samples.
    :param bool histogram: Whether to plot a histogram of array values at plot time.
    :type colormap: :py:class:`str` or :class:`matplotlib.colors.Colormap`
    :param colormap: Plot using a Matplotlib colormap. Defaults to :py:data:`gray` which is colorblind-friendly and behaves similarly to the RADAN default, but :py:data:`seismic` is a favorite of many due to its diverging nature.
    :param bool colorbar: Whether to display a graded color bar at plot time.
    :param list[int,int,int,int] zero: A list of values representing the amount of samples to slice off each channel. Defaults to :py:data:`None` for all channels, which will end up being set as :py:data:`[2,2,2,2]` for a four-channel file (2 is the number of rows down that GSSI stores mark information in).
    :param int gain: The amount of gain applied to plots. Defaults to 1. Gain is applied as a ratio of the standard deviation of radargram values to the value set here.
    :param int freqmin: Minimum frequency value to feed to the vertical triangular FIR bandpass filter :py:func:`readgssi.filtering.triangular`. Defaults to :py:data:`None` (no filter).
    :param int freqmax: Maximum frequency value to feed to the vertical triangular FIR bandpass filter :py:func:`readgssi.filtering.triangular`. Defaults to :py:data:`None` (no filter).
    :param bool reverse: Whether to read the array backwards (i.e. flip horizontally; :py:func:`readgssi.arrayops.flip`). Defaults to :py:data:`False`. Useful for lining up travel directions of files run opposite each other.
    :param int bgr: Background removal filter applied after stacking (:py:func:`readgssi.filtering.bgr`). Defaults to :py:data:`False` (off). :py:data:`bgr=True` must be accompanied by a valid value for :py:data:`win`.
    :param int win: Window size for background removal filter (:py:func:`readgssi.filtering.bgr`). If :py:data:`bgr=True` and :py:data:`win=0`, the full-width row average will be subtracted from each row. If :py:data:`bgr=True` and :py:data:`win=50`, a moving window will calculate the average of 25 cells on either side of the current cell, and subtract that average from the cell value, using :py:func:`scipy.ndimage.uniform_filter1d` with :py:data:`mode='constant'` and :py:data:`cval=0`. This is useful for removing non-uniform horizontal average, but the tradeoff is that it creates ghost data half the window size away from vertical figures, and that a window size set too low will obscure any horizontal layering longer than the window size.
    :param bool dewow: Whether to apply a vertical dewow filter (experimental). See :py:func:`readgssi.filtering.dewow`.
    :param bool normalize: Distance normalization (:py:func:`readgssi.arrayops.distance_normalize`). Defaults to :py:data:`False`.
    :param bool specgram: Produce a spectrogram of a trace in the array using :py:func:`readgssi.plot.spectrogram`. Defaults to :py:data:`False` (if :py:data:`True`, defaults to a trace roughly halfway across the profile). This is mostly for debugging and is not currently accessible from the command line.
    :param bool noshow: If :py:data:`True`, this will suppress the matplotlib interactive window and simply save a file. This is useful for processing many files in a folder without user input.
    :param float spm: User-set samples per meter. This overrides the value read from the header, and typically doesn't need to be set if the samples per meter value was set correctly at survey time. This value does not need to be set if GPS input (DZG file) is present and the user sets :py:data:`normalize=True`.
    :param int start_scan: zero based start scan to read data from. Defaults to zero.
    :param int num_scans: number of scans to read from the file, Defaults to -1, which reads from start_scan to end of file.
    :param float epsr: Epsilon_r, otherwise known as relative permittivity, or dielectric constant. This determines the speed at which waves travel through the first medium they encounter. It is used to calculate the profile depth if depth units are specified on the Z-axis of plots.
    :param bool title: Whether to display descriptive titles on plots. Defaults to :py:data:`True`.
    :param list[int,int,int,int] zoom: Zoom extents to set programmatically for matplotlib plots. Must pass a list of four integers: :py:data:`[left, right, up, down]`. Since the z-axis begins at the top, the "up" value is actually the one that displays lower on the page. All four values are axis units, so if you are working in nanoseconds, 10 will set a limit 10 nanoseconds down. If your x-axis is in seconds, 6 will set a limit 6 seconds from the start of the survey. It may be helpful to display the matplotlib interactive window at full extents first, to determine appropriate extents to set for this parameter. If extents are set outside the boundaries of the image, they will be set back to the boundaries. If two extents on the same axis are the same, the program will default to plotting full extents for that axis.
    :rtype: header (:py:class:`dict`), radar array (:py:class:`numpy.ndarray`), gps (False or :py:class:`pandas.DataFrame`)
    """

    if infile:
        # read the file
        try:
            if verbose:
                fx.printmsg('reading...')
                fx.printmsg('input file:         %s' % (infile))
            r = readdzt(infile,
                        gps=normalize,
                        spm=spm,
                        start_scan=start_scan,
                        num_scans=num_scans,
                        epsr=epsr,
                        verbose=verbose)
            # time zero per channel
            r[0]['timezero'] = [None, None, None, None]
            for i in range(r[0]['rh_nchan']):
                try:
                    r[0]['timezero'][i] = int(list(zero)[i])
                except (TypeError, IndexError):
                    fx.printmsg(
                        'WARNING: no time zero specified for channel %s, defaulting to 2'
                        % i)
                    r[0]['timezero'][i] = 2
            # print a bunch of header info
            if verbose:
                fx.printmsg('success. header values:')
                header_info(r[0], r[1])
        except IOError as e:  # the user has selected an inaccessible or nonexistent file
            fx.printmsg(
                "ERROR: DZT file is inaccessable or does not exist at %s" %
                (os.path.abspath(infile)))
            raise IOError(e)
        infile_ext = os.path.splitext(infile)[1]
        infile_basename = os.path.splitext(infile)[0]
    else:
        raise IOError('ERROR: no input file specified')

    rhf_sps = r[0]['rhf_sps']
    rhf_spm = r[0]['rhf_spm']
    line_dur = r[0]['sec']
    for chan in list(range(r[0]['rh_nchan'])):
        try:
            ANT[r[0]['rh_antname'][chan]]
        except KeyError as e:
            print(
                '--------------------WARNING - PLEASE READ---------------------'
            )
            fx.printmsg(
                'WARNING: could not read frequency for antenna name %s' % e)
            if antfreq:
                fx.printmsg('using user-specified antenna frequency.')
                r[0]['antfreq'] = antfreq
            else:
                fx.printmsg(
                    'WARNING: trying to use frequency of %s MHz (estimated)...'
                    % (r[0]['antfreq'][chan]))
            fx.printmsg('more info: rh_ant=%s' % (r[0]['rh_ant']))
            fx.printmsg('           known_ant=%s' % (r[0]['known_ant']))
            fx.printmsg(
                "please submit a bug report with this warning, the antenna name and frequency"
            )
            fx.printmsg('at https://github.com/iannesbitt/readgssi/issues/new')
            fx.printmsg(
                'or send via email to ian (dot) nesbitt (at) gmail (dot) com.')
            fx.printmsg(
                'if possible, please attach a ZIP file with the offending DZT inside.'
            )
            print(
                '--------------------------------------------------------------'
            )

    # create a list of n arrays, where n is the number of channels
    arr = r[1].astype(np.int32)
    chans = list(range(r[0]['rh_nchan']))

    # set up list of arrays
    img_arr = arr[:r[0]['rh_nchan'] * r[0][
        'rh_nsamp']]  # test if we understand data structure. arrays should be stacked nchan*nsamp high
    new_arr = {}
    for ar in chans:
        a = []
        a = img_arr[(ar) * r[0]['rh_nsamp']:(ar + 1) *
                    r[0]['rh_nsamp']]  # break apart
        new_arr[ar] = a[r[0]['timezero'][ar]:, :int(
            img_arr.shape[1])]  # put into dict form

    img_arr = new_arr  # overwrite
    del arr, new_arr

    for ar in img_arr:
        """
        filter and construct an output file or plot from the current channel's array
        """
        if verbose:
            fx.printmsg('beginning processing for channel %s (antenna %s)' %
                        (ar, r[0]['rh_antname'][ar]))
        # execute filtering functions if necessary
        if normalize:
            r[0], img_arr[ar], r[2] = arrayops.distance_normalize(
                header=r[0], ar=img_arr[ar], gps=r[2], verbose=verbose)
        if dewow:
            # dewow
            img_arr[ar] = filtering.dewow(ar=img_arr[ar], verbose=verbose)
        if freqmin and freqmax:
            # vertical triangular bandpass
            img_arr[ar] = filtering.triangular(ar=img_arr[ar],
                                               header=r[0],
                                               freqmin=freqmin,
                                               freqmax=freqmax,
                                               zerophase=True,
                                               verbose=verbose)
        if stack != 1:
            # horizontal stacking
            img_arr[ar], stack = arrayops.stack(ar=img_arr[ar],
                                                stack=stack,
                                                verbose=verbose)
        else:
            stack = 1
        if bgr:
            # background removal
            img_arr[ar] = filtering.bgr(ar=img_arr[ar],
                                        header=r[0],
                                        win=win,
                                        verbose=verbose)
        else:
            win = None
        if reverse:
            # read array backwards
            img_arr[ar] = arrayops.flip(img_arr[ar], verbose=verbose)

        ## file naming
        # name the output file
        if ar == 0:  # first channel?
            orig_outfile = outfile  # preserve the original
        else:
            outfile = orig_outfile  # recover the original

        if outfile:
            outfile_ext = os.path.splitext(outfile)[1]
            outfile = '%s' % (os.path.join(os.path.splitext(outfile)[0]))
            if len(chans) > 1:
                outfile = '%sc%s' % (outfile, ar)  # avoid naming conflicts
        else:
            outfile = fx.naming(infile_basename=infile_basename,
                                chans=chans,
                                chan=ar,
                                normalize=normalize,
                                zero=r[0]['timezero'][ar],
                                stack=stack,
                                reverse=reverse,
                                bgr=bgr,
                                win=win,
                                dewow=dewow,
                                freqmin=freqmin,
                                freqmax=freqmax,
                                plotting=plotting,
                                gain=gain)

        if plotting:
            plot.radargram(ar=img_arr[ar],
                           header=r[0],
                           freq=r[0]['antfreq'][ar],
                           verbose=verbose,
                           figsize=figsize,
                           dpi=dpi,
                           stack=stack,
                           x=x,
                           z=z,
                           gain=gain,
                           colormap=colormap,
                           colorbar=colorbar,
                           noshow=noshow,
                           outfile=outfile,
                           win=win,
                           title=title,
                           zero=r[0]['timezero'][ar],
                           zoom=zoom)

        if histogram:
            plot.histogram(ar=img_arr[ar], verbose=verbose)

        if specgram:
            plot.spectrogram(ar=img_arr[ar],
                             header=header,
                             freq=r[0]['antfreq'][ar],
                             verbose=verbose)

    if frmt != None:
        if verbose:
            fx.printmsg('outputting to %s...' % frmt)
        for ar in img_arr:
            # is there an output filepath given?
            outfile_abspath = os.path.abspath(
                outfile)  # set output to given location

            # what is the output format
            if frmt in 'csv':
                translate.csv(ar=img_arr[ar],
                              outfile_abspath=outfile_abspath,
                              header=r[0],
                              verbose=verbose)
            elif frmt in 'h5':
                translate.h5(ar=img_arr[ar],
                             infile_basename=infile_basename,
                             outfile_abspath=outfile_abspath,
                             verbose=verbose)
            elif frmt in 'segy':
                translate.segy(ar=img_arr[ar],
                               outfile_abspath=outfile_abspath,
                               verbose=verbose)
            elif frmt in 'numpy':
                translate.numpy(ar=img_arr[ar],
                                outfile_abspath=outfile_abspath,
                                verbose=verbose)
            elif frmt in 'gprpy':
                translate.gprpy(ar=img_arr[ar],
                                outfile_abspath=outfile_abspath,
                                header=r[0],
                                verbose=verbose)
        if frmt in ('object', 'python'):
            return r[0], img_arr, r[2]
Пример #29
0
def radargram(ar, ant, header, freq, figsize='auto', gain=1, stack=1, x='seconds', z='nanoseconds', title=True,
              colormap='gray', colorbar=False, absval=False, noshow=False, win=None, outfile='readgssi_plot', zero=2,
              zoom=[0,0,0,0], dpi=150, showmarks=False, verbose=False):
    """
    Function that creates, modifies, and saves matplotlib plots of radargram images. For usage information, see :doc:`plotting`.

    :param numpy.ndarray ar: The radar array
    :param int ant: Antenna channel number
    :param dict header: Radar file header dictionary
    :param int freq: Antenna frequency
    :param float plotsize: The height of the output plot in inches
    :param float gain: The gain applied to the image. Must be positive but can be between 0 and 1 to reduce gain.
    :param int stack: Number of times the file was stacked horizontally. Used to calculate traces on the X axis.
    :param str x: The units to display on the x-axis during plotting. Defaults to :py:data:`x='seconds'`. Acceptable values are :py:data:`x='distance'` (which sets to meters), :py:data:`'km'`, :py:data:`'m'`, :py:data:`'cm'`, :py:data:`'mm'`, :py:data:`'kilometers'`, :py:data:`'meters'`, etc., for distance; :py:data:`'seconds'`, :py:data:`'s'`, :py:data:`'temporal'` or :py:data:`'time'` for seconds, and :py:data:`'traces'`, :py:data:`'samples'`, :py:data:`'pulses'`, or :py:data:`'columns'` for traces.
    :param str z: The units to display on the z-axis during plotting. Defaults to :py:data:`z='nanoseconds'`. Acceptable values are :py:data:`z='depth'` (which sets to meters), :py:data:`'m'`, :py:data:`'cm'`, :py:data:`'mm'`, :py:data:`'meters'`, etc., for depth; :py:data:`'nanoseconds'`, :py:data:`'ns'`, :py:data:`'temporal'` or :py:data:`'time'` for seconds, and :py:data:`'samples'` or :py:data:`'rows'` for samples.
    :param bool title: Whether to add a title to the figure. Defaults to True.
    :param matplotlib.colors.Colormap colormap: The matplotlib colormap to use, defaults to 'gray' which is to say: the same as the default RADAN colormap
    :param bool colorbar: Whether to draw the colorbar. Defaults to False.
    :param bool absval: Whether to draw the array with an absolute value scale. Defaults to False.
    :param bool noshow: Whether to suppress the matplotlib figure GUI window. Defaults to False, meaning the dialog will be displayed.
    :param int win: Window size for background removal filter :py:func:`readgssi.filtering.bgr` to display in plot title.
    :param str outfile: The name of the output file. Defaults to 'readgssi_plot.png' in the current directory.
    :param int zero: The zero point. This represents the number of samples sliced off the top of the profile by the timezero option in :py:func:`readgssi.readgssi.readgssi`.
    :param list[int,int,int,int] zoom: Zoom extents for matplotlib plots. Must pass a list of four integers: :py:data:`[left, right, up, down]`. Since the z-axis begins at the top, the "up" value is actually the one that displays lower on the page. All four values are axis units, so if you are working in nanoseconds, 10 will set a limit 10 nanoseconds down. If your x-axis is in seconds, 6 will set a limit 6 seconds from the start of the survey. It may be helpful to display the matplotlib interactive window at full extents first, to determine appropriate extents to set for this parameter. If extents are set outside the boundaries of the image, they will be set back to the boundaries. If two extents on the same axis are the same, the program will default to plotting full extents for that axis.
    :param int dpi: The dots per inch value to use when creating images. Defaults to 150.
    :param bool showmarks: Whether to plot user marks as vertical lines. Defaults to False.
    :param bool verbose: Verbose, defaults to False
    """

    # having lots of trouble with this line not being friendly with figsize tuple (integer coercion-related errors)
    # so we will force everything to be integers explicitly
    if figsize != 'auto':
        figx, figy = int(int(figsize)*int(int(ar.shape[1])/int(ar.shape[0]))), int(figsize)-1 # force to integer instead of coerce
        if figy <= 1:
            figy += 1 # avoid zero height error in y dimension
        if figx <= 1:
            figx += 1 # avoid zero height error in x dimension
        if verbose:
            fx.printmsg('plotting %sx%sin image with gain=%s...' % (figx, figy, gain))
        fig, ax = plt.subplots(figsize=(figx, figy), dpi=dpi)
    else:
        if verbose:
            fx.printmsg('plotting with gain=%s...' % gain)
        fig, ax = plt.subplots()

    mean = np.mean(ar)
    if verbose:
        fx.printmsg('image stats')
        fx.printmsg('size:               %sx%s' % (ar.shape[0], ar.shape[1]))
        fx.printmsg('mean:               %.3f' % mean)

    if absval:
        fx.printmsg('plotting absolute value of array gradient')
        ar = np.abs(np.gradient(ar, axis=1))
        flip = 1
        ll = np.min(ar)
        ul = np.max(ar)
        std = np.std(ar)
    else:
        if mean > 1000:
            fx.printmsg('WARNING: mean pixel value is very high. consider filtering with -t')
        flip = 1
        std = np.std(ar)
        ll = mean - (std * 3) # lower color limit
        ul = mean + (std * 3) # upper color limit
        fx.printmsg('stdev:              %.3f' % std)
        fx.printmsg('lower color limit:  %.2f [mean - (3 * stdev)]' % (ll))
        fx.printmsg('upper color limit:  %.2f [mean + (3 * stdev)]' % (ul))

    # X scaling routine
    if (x == None) or (x in 'seconds'): # plot x as time by default
        xmax = header['sec']
        xlabel = 'Time (s)'
    else:
        if (x in ('cm', 'm', 'km')) and (header['rhf_spm'] > 0): # plot as distance based on unit
            xmax = ar.shape[1] / header['rhf_spm']
            if 'cm' in x:
                xmax = xmax * 100.
            if 'km' in x:
                xmax = xmax / 1000.
            xlabel = 'Distance (%s)' % (x)
        else: # else we plot in units of stacked traces
            if header['rhf_spm'] == 0:
                fx.printmsg('samples per meter value is zero. plotting trace numbers instead.')
            xmax = ar.shape[1] # * float(stack)
            xlabel = 'Trace number'
            if stack > 1:
                xlabel = 'Trace number (after %sx stacking)' % (stack)
    # finally, relate max scale value back to array shape in order to set matplotlib axis scaling
    try:
        xscale = ar.shape[1]/xmax
    except ZeroDivisionError:
        fx.printmsg('ERROR: cannot plot x-axis in "%s" mode; header value is zero. using time instead.' % (x))
        xmax = header['sec']
        xlabel = 'Time (s)'
        xscale = ar.shape[1]/xmax

    zmin = 0
    # Z scaling routine
    if (z == None) or (z in 'nanoseconds'): # plot z as time by default
        zmax = header['rhf_range'] #could also do: header['ns_per_zsample'] * ar.shape[0] * 10**10 / 2
        zlabel = 'Two-way time (ns)'
    else:
        if z in ('mm', 'cm', 'm'): # plot z as TWTT based on unit and cr/rhf_epsr value
            zmax = header['rhf_depth'] - header['rhf_top']
            if 'cm' in z:
                zmax = zmax * 100.
            if 'mm' in z:
                zmax = zmax * 1000.
            zlabel = r'Depth at $\epsilon_r$=%.2f (%s)' % (header['rhf_epsr'], z)
        else: # else we plot in units of samples
            zmin = zero
            zmax = ar.shape[0] + zero
            zlabel = 'Sample'
    # finally, relate max scale value back to array shape in order to set matplotlib axis scaling
    try:
        zscale = ar.shape[0]/zmax
    except ZeroDivisionError: # apparently this can happen even in genuine GSSI files
        fx.printmsg('ERROR: cannot plot z-axis in "%s" mode; header max value is zero. using samples instead.' % (z))
        zmax = ar.shape[0]
        zlabel = 'Sample'
        zscale = ar.shape[0]/zmax

    if verbose:
        fx.printmsg('xmax: %.4f %s, zmax: %.4f %s' % (xmax, xlabel, zmax, zlabel))

    extent = [0, xmax, zmax, zmin]

    try:
        if verbose:
            fx.printmsg('attempting to plot with colormap %s' % (colormap))
        img = ax.imshow(ar, cmap=colormap, clim=(ll, ul), interpolation='bicubic', aspect=float(zscale)/float(xscale),
                     norm=colors.SymLogNorm(linthresh=float(std)/float(gain), linscale=flip,
                                            vmin=ll, vmax=ul), extent=extent)
    except:
        fx.printmsg('ERROR: matplotlib did not accept colormap "%s", using gray instead' % colormap)
        fx.printmsg('see examples here: https://matplotlib.org/users/colormaps.html#grayscale-conversion')
        img = ax.imshow(ar, cmap='gray', clim=(ll, ul), interpolation='bicubic', aspect=float(zscale)/float(xscale),
                     norm=colors.SymLogNorm(linthresh=float(std)/float(gain), linscale=flip,
                                            vmin=ll, vmax=ul), extent=extent)

    # user marks
    if showmarks:
        if verbose:
            fx.printmsg('plotting marks at traces: %s' % header['marks'])
        for mark in header['marks']:
            plt.axvline(x=mark/xscale, color='r', linestyle=(0, (14,14)), linewidth=1, alpha=0.7)

    # zooming
    if zoom != [0,0,0,0]: # if zoom is set
        zoom = fx.zoom(zoom=zoom, extent=extent, x=x, z=z, verbose=verbose) # figure out if the user set extents properly
    else:
        zoom = extent # otherwise, zoom is full extents
    if zoom != extent: # if zoom is set correctly, then set new axis limits
        if verbose:
            fx.printmsg('zooming in to %s [xmin, xmax, ymax, ymin]' % zoom)
        ax.set_xlim(zoom[0], zoom[1])
        ax.set_ylim(zoom[2], zoom[3])
        # add zoom extents to file name via the Seth W. Campbell honorary naming scheme
        outfile = fx.naming(outfile=outfile, zoom=[int(i) for i in zoom])

    ax.set_xlabel(xlabel)
    ax.set_ylabel(zlabel)

    if colorbar:
        fig.colorbar(img)
    if title:
        try:
            antfreq = freq[ant]
        except TypeError:
            antfreq = freq
        title = '%s - %s MHz - stacking: %s - gain: %s' % (
                    os.path.basename(header['infile']), antfreq, stack, gain)
        if win:
            if win == 0:
                win = 'full'
            title = '%s - bgr: %s' % (title, win)
        plt.title(title)
    if figx / figy >=1: # if x is longer than y (avoids plotting error where data disappears for some reason)
        plt.tight_layout()#pad=fig.get_size_inches()[1]/4.) # then it's ok to call tight_layout()
    else:
        try:
            # the old way of handling
            #plt.tight_layout(w_pad=2, h_pad=1)

            # the new way of handling
            fx.printmsg('WARNING: axis lengths are funky. using alternative sizing method. please adjust manually in matplotlib gui.')
            figManager = plt.get_current_fig_manager()
            try:
                figManager.window.showMaximized()
            except:
                figManager.resize(*figManager.window.maxsize())
            for item in ([ax.xaxis.label, ax.yaxis.label] +
                        ax.get_xticklabels() + ax.get_yticklabels()):
                item.set_fontsize(5)
            ax.title.set_fontsize(7)
            plt.draw()
            fig.canvas.start_event_loop(0.1)
            plt.tight_layout()
        except:
            fx.printmsg('WARNING: tight_layout() raised an error because axis lengths are funky. please adjust manually in matplotlib gui.')
    if outfile != 'readgssi_plot':
        # if outfile doesn't match this then save fig with the outfile name
        if verbose:
            fx.printmsg('saving figure as %s.png' % (outfile))
        plt.savefig('%s.png' % (outfile), dpi=dpi, bbox_inches='tight')
    else:
        # else someone has called this function from outside and forgotten the outfile field
        if verbose:
            fx.printmsg('saving figure as %s_%sMHz.png with dpi=%s' % (os.path.splitext(header['infile'])[0], freq, dpi))
        plt.savefig('%s_%sMHz.png' % (os.path.splitext(header['infile'])[0], freq), bbox_inches='tight')
    if noshow:
        if verbose:
            fx.printmsg('not showing matplotlib')
        plt.close()
    else:
        if verbose:
            fx.printmsg('showing matplotlib figure...')
        plt.show()
Пример #30
0
def radargram(ar,
              header,
              freq,
              verbose=True,
              figsize='auto',
              gain=1,
              stack=1,
              x='seconds',
              z='nanoseconds',
              colormap='Greys',
              colorbar=False,
              noshow=False,
              outfile='readgssi_plot',
              aspect='auto'):
    """
    let's do some matplotlib

    requirements:
    ar          - a radar array
    verbose     - boolean, whether to print progress. defaults to True
    plotsize    - the size of the plot in inches
    stack       - number of times to stack horizontally
    colormap    - the matplotlib colormap to use, defaults to 'Greys' which is to say: the same as the default RADAN colormap
    colorbar    - boolean, whether to draw the colorbar. defaults to False
    noshow      - boolean, whether to bring up the matplotlib figure dialog when drawing. defaults to False, meaning the dialog will be displayed.
    outfile     - name of the output file. defaults to 'readgssi_plot.png' in the current directory.
    """

    # having lots of trouble with this line not being friendly with figsize tuple (integer coercion-related errors)
    # so we will force everything to be integers explicitly
    if figsize != 'auto':
        figx, figy = int(
            int(figsize) * int(int(ar.shape[1]) / int(ar.shape[0]))), int(
                figsize)  # force to integer instead of coerce
        if figy <= 1:
            figy += 1  # avoid zero height error in y dimension
        if figx <= 1:
            figx += 1  # avoid zero height error in x dimension
        if verbose:
            fx.printmsg('plotting %sx%sin image with gain=%s...' %
                        (figx, figy, gain))
        fig, ax = plt.subplots(figsize=(figx, figy - 1), dpi=150)
    else:
        if verbose:
            fx.printmsg('plotting with gain=%s...' % gain)
        fig, ax = plt.subplots()

    mean = np.mean(ar)
    std = np.std(ar)
    ll = mean - (std * 3)  # lower color limit
    ul = mean + (std * 3)  # upper color limit
    if verbose:
        fx.printmsg('image stats')
        fx.printmsg('mean:               %s' % mean)
        fx.printmsg('stdev:              %s' % std)
        fx.printmsg('lower color limit:  %s [mean - (3 * stdev)]' % ll)
        fx.printmsg('upper color limit:  %s [mean + (3 * stdev)]' % ul)

    # X scaling routine
    if (x == None) or (x in 'seconds'):  # plot x as time by default
        xmax = header['sec']
        xlabel = 'Time (s)'
    else:
        if x in ('cm', 'm', 'km'):  # plot as distance based on unit
            xmax = (ar.shape[1] * float(stack)) / header['rhf_spm']
            if 'cm' in x:
                xmax = xmax * 100.
            if 'km' in x:
                xmax = xmax / 1000.
            xlabel = 'Distance (%s)' % (x)
        else:  # else we plot in units of stacked traces
            xmax = ar.shape[1]  # * float(stack)
            xlabel = 'Trace (after stacking)'
    # finally, relate max scale value back to array shape in order to set matplotlib axis scaling
    try:
        xscale = ar.shape[1] / xmax
    except ZeroDivisionError:
        fx.printmsg(
            'ERROR: cannot plot x-axis in "%s" mode; header value is zero. using time instead.'
            % (x))
        xmax = header['sec']
        xlabel = 'Time (s)'
        xscale = ar.shape[1] / xmax

    # Z scaling routine
    if (z == None) or (z in 'nanoseconds'):  # plot z as time by default
        zmax = header['ns_per_zsample'] * ar.shape[0] * 10**9
        zlabel = 'Two-way travel time (ns)'
    else:
        if z in ('mm', 'cm',
                 'm'):  # plot z as TWTT based on unit and cr/rhf_epsr value
            zmax = header['rhf_depth']
            if 'cm' in z:
                zmax = zmax * 100.
            if 'mm' in z:
                zmax = zmax * 1000.
            zlabel = r'Depth at $\epsilon_r$=%s (%s)' % (header['rhf_epsr'], z)
        else:  # else we plot in units of samples
            zmax = ar.shape[0]
            zlabel = 'Sample'
    # finally, relate max scale value back to array shape in order to set matplotlib axis scaling
    try:
        zscale = ar.shape[0] / zmax
    except ZeroDivisionError:  # apparently this can happen even in genuine GSSI files
        fx.printmsg(
            'ERROR: cannot plot z-axis in "%s" mode; header max value is zero. using samples instead.'
            % (z))
        zmax = ar.shape[0]
        zlabel = 'Sample'
        zscale = ar.shape[0] / zmax

    if verbose:
        fx.printmsg('xmax: %s %s, zmax: %s %s' % (xmax, xlabel, zmax, zlabel))

    try:
        if verbose:
            fx.printmsg('attempting to plot with colormap %s' % (colormap))
        img = ax.imshow(ar,
                        cmap=colormap,
                        clim=(ll, ul),
                        interpolation='bicubic',
                        aspect=float(zscale) / float(xscale),
                        norm=colors.SymLogNorm(linthresh=float(std) /
                                               float(gain),
                                               linscale=1,
                                               vmin=ll,
                                               vmax=ul),
                        extent=[0, xmax, zmax, 0])
    except:
        fx.printmsg(
            'ERROR: matplotlib did not accept colormap "%s", using viridis instead'
            % colormap)
        fx.printmsg(
            'see examples here: https://matplotlib.org/users/colormaps.html#grayscale-conversion'
        )
        img = ax.imshow(ar,
                        cmap='Greys',
                        clim=(ll, ul),
                        interpolation='bicubic',
                        aspect=float(zscale) / float(xscale),
                        norm=colors.SymLogNorm(linthresh=float(std) /
                                               float(gain),
                                               linscale=1,
                                               vmin=ll,
                                               vmax=ul),
                        extent=[0, xmax, zmax, 0])

    ax.set_xlabel(xlabel)
    ax.set_ylabel(zlabel)

    if colorbar:
        fig.colorbar(img)
    if verbose:
        plt.title('%s - %s MHz - stacking: %s - gain: %s' %
                  (os.path.basename(header['infile']), freq, stack, gain))
    if figx / figy >= 1:  # if x is longer than y (avoids plotting error where data disappears for some reason)
        plt.tight_layout(pad=fig.get_size_inches()
                         [1])  # then it's ok to call tight_layout()
    else:
        fx.printmsg(
            'WARNING: not calling tight_layout() because axis lengths are funky. please adjust manually in matplotlib gui.'
        )
    if outfile != 'readgssi_plot':
        # if outfile doesn't match this then save fig with the outfile name
        if verbose:
            fx.printmsg('saving figure as %s.png' % (outfile))
        plt.savefig('%s.png' % (outfile))
    else:
        # else someone has called this function from outside and forgotten the outfile field
        if verbose:
            fx.printmsg('saving figure as %s_%sMHz.png' %
                        (os.path.splitext(header['infile'])[0], freq))
        plt.savefig('%s_%sMHz.png' %
                    (os.path.splitext(header['infile'])[0], freq))
    if noshow:
        if verbose:
            fx.printmsg('not showing matplotlib')
        plt.close()
    else:
        if verbose:
            fx.printmsg('showing matplotlib figure...')
        plt.show()