你已经派生过 gpr-sidl-inv
镜像自地址
https://gitee.com/sduem/gpr-sidl-inv.git
已同步 2025-08-03 18:56:51 +08:00
4
readgssi/__init__.py
普通文件
4
readgssi/__init__.py
普通文件
@@ -0,0 +1,4 @@
|
|||||||
|
from . import _version
|
||||||
|
|
||||||
|
name = 'readgssi'
|
||||||
|
__version__ = _version.version
|
1
readgssi/_version.py
普通文件
1
readgssi/_version.py
普通文件
@@ -0,0 +1 @@
|
|||||||
|
version = "0.0.22"
|
162
readgssi/arrayops.py
普通文件
162
readgssi/arrayops.py
普通文件
@@ -0,0 +1,162 @@
|
|||||||
|
import readgssi.functions as fx
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def reducex(ar, header, 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]
|
||||||
|
|
||||||
|
def stack(ar, header, 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, header=header, 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.')
|
||||||
|
|
||||||
|
if header['rh_nchan'] == 1:
|
||||||
|
# this is a hack to prevent the rhf_spx vars from being changed multiple times
|
||||||
|
# when stacking a multichannel file. this will go away when each array has its
|
||||||
|
# own header and thus each can be dealt with individually.
|
||||||
|
if header['rhf_sps'] != 0:
|
||||||
|
header['rhf_sps'] = header['rhf_sps'] / stack
|
||||||
|
if header['rhf_spm'] != 0:
|
||||||
|
header['rhf_spm'] = header['rhf_spm'] / stack
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if header['spx_updates']:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if header['rhf_sps'] != 0:
|
||||||
|
header['rhf_sps'] = header['rhf_sps'] / stack
|
||||||
|
if header['rhf_spm'] != 0:
|
||||||
|
header['rhf_spm'] = header['rhf_spm'] / stack
|
||||||
|
header['spx_updates'] = True
|
||||||
|
except KeyError:
|
||||||
|
header['spx_updates'] = False
|
||||||
|
|
||||||
|
return header, arr, stack
|
||||||
|
|
||||||
|
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 gps.empty:
|
||||||
|
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, errors='ignore')#, 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, errors='ignore').values[on:on+c.shape[1]], axis=1)
|
||||||
|
p = reducex(p, header=header, 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('total GPS distance: %.2f m' % gps['meters'].iloc[-1])
|
||||||
|
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]
|
||||||
|
header['rhf_sps'] = 0
|
||||||
|
return header, proc, gps
|
76
readgssi/config.py
普通文件
76
readgssi/config.py
普通文件
@@ -0,0 +1,76 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from readgssi.__init__ import __version__, name
|
||||||
|
import pkg_resources
|
||||||
|
import readgssi.plot as plot
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module contains some things readgssi needs to operate, both command line and python-related.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dist = pkg_resources.get_distribution(name)
|
||||||
|
year = datetime.now().year
|
||||||
|
author = 'Ian Nesbitt'
|
||||||
|
affil = 'School of Earth and Climate Sciences, University of Maine'
|
||||||
|
|
||||||
|
help_text = u'''Help text:
|
||||||
|
############################################################
|
||||||
|
readgssi version %s
|
||||||
|
|
||||||
|
Copyleft %s %s 2017-%s
|
||||||
|
%s
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
usage:
|
||||||
|
readgssi -i input.DZT [OPTIONS]
|
||||||
|
|
||||||
|
required flags:
|
||||||
|
OPTION | ARGUMENT | FUNCTIONALITY
|
||||||
|
-i, --input | file: /dir/f.DZT | input DZT file
|
||||||
|
|
||||||
|
optional flags:
|
||||||
|
OPTION | ARGUMENT | FUNCTIONALITY
|
||||||
|
-o, --output | file: /dir/f.ext | output file. if not set, will be named similar to input
|
||||||
|
-f, --format | string, eg. "csv" | output format (CSV and DZT are the only working formats currently available from the command line)
|
||||||
|
-p, --plot | +integer or "auto" | plot size. will be x inches high or "auto". default: 10. see also -D to set DPI
|
||||||
|
-D, --dpi | positive integer | set the plot DPI for figure making. defaults to 150
|
||||||
|
-T, --titleoff | | turn the plot title off (useful for figure making)
|
||||||
|
-x, --xscale | string, eg. "dist" | x units. will attempt to convert the x-axis to distance, time, or trace units based on header values
|
||||||
|
-z, --zscale | string, eg. "time" | z units. attempt to convert the x-axis to depth, time, or sample units based on header values
|
||||||
|
-e, --zoom | list of +int [LRUD] | set a zoom to automatically jump to. list order is [left,right,up,down] and units are the same as axis
|
||||||
|
-n, --noshow | | suppress matplotlib popup window and simply save a figure (useful for multi-file processing)
|
||||||
|
-c, --colormap | string, eg. "Greys" | specify the colormap (https://matplotlib.org/users/colormaps.html#grayscale-conversion)
|
||||||
|
-g, --gain | positive float | gain constant (higher=greater contrast, default: 1)
|
||||||
|
-A, --absval | | Displays the absolute value of the vertical gradient of the array when plotting. Good for displaying faint array features.
|
||||||
|
-r, --bgr | +integer or zero | horizontal background removal (useful to remove ringing). zero=full width; positive=window size (after stacking)
|
||||||
|
-R, --reverse | | reverse (flip array horizontally)
|
||||||
|
-w, --dewow | | trinomial dewow algorithm
|
||||||
|
-t, --bandpass | +int-+int (MHz) | triangular FIR bandpass filter applied vertically (positive integer range in megahertz; ex. 70-130)
|
||||||
|
-b, --colorbar | | add a colorbar to the radar figure
|
||||||
|
-a, --antfreq | positive integer | set antenna frequency. overrides header value
|
||||||
|
-s, --stack | +integer or "auto" | set trace stacking value or "auto" to autostack to ~2.5:1 x:y axis ratio
|
||||||
|
-N, --normalize | | distance normalize; reads .DZG NMEA data file if it exists; otherwise tries to read CSV with lat, lon, and time fields
|
||||||
|
-P, --pausecorr | | pause correction; fixes decoupling of DZG and DZT trace numbers during survey pauses using low velocity GPS marks
|
||||||
|
-d, --spm | positive float | specify the samples per meter (spm). overrides header value
|
||||||
|
-m, --histogram | | produce a histogram of data values
|
||||||
|
-E, --epsr | float > 1.0 | user-defined epsilon sub r (sometimes referred to as "dielectric") if set, ignores value in DZT header
|
||||||
|
-Z, --zero | +int or list of int | timezero: skip samples before direct wave. samples are removed from the top of the trace. use list for multi-channel
|
||||||
|
|
||||||
|
naming scheme for exports:
|
||||||
|
CHARACTERS | MEANING
|
||||||
|
Ch0 | Profile from channel 0 (can range from 0 - 3)
|
||||||
|
Dn | Distance normalization
|
||||||
|
Tz233 | Time zero at 233 samples
|
||||||
|
S8 | Stacked 8 times
|
||||||
|
Rv | Profile read in reverse (flipped horizontally)
|
||||||
|
Bgr75 | Background removal filter with window size of 75
|
||||||
|
Dw | Dewow filter
|
||||||
|
Bp70-130 | triangular FIR filter applied from 70 to 130 MHz
|
||||||
|
G30 | 30x contrast gain
|
||||||
|
Abs | Color scale represents absolute value of vertical gradient
|
||||||
|
Z10.20.7.5 | zoom from 10-20 axis units on the x-axis and 5-7 on the z-axis
|
||||||
|
|
||||||
|
matplotlib output formats supported by current backend:
|
||||||
|
EXTENSION | FILE FORMAT
|
||||||
|
%s''' % (__version__, u'\U0001F12F', author, year, affil, plot.fmtst)
|
||||||
|
|
||||||
|
version_text = '%s %s' % (name, __version__)
|
107
readgssi/constants.py
普通文件
107
readgssi/constants.py
普通文件
@@ -0,0 +1,107 @@
|
|||||||
|
import pytz
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module contains a number of variables that readgssi needs to perform physics calculations and interpret files from DZT files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MINHEADSIZE = 1024 # absolute minimum total header size
|
||||||
|
PAREASIZE = 128 # fixed info header size
|
||||||
|
RGPSSIZE = 9 # GPS record size
|
||||||
|
GPSAREASIZE = RGPSSIZE * 2
|
||||||
|
INFOAREASIZE = MINHEADSIZE - PAREASIZE - GPSAREASIZE
|
||||||
|
|
||||||
|
TZ = pytz.timezone('UTC')
|
||||||
|
|
||||||
|
# some physical constants for Maxwell's equation for speed of light in a dielectric medium
|
||||||
|
C = 299792458 # speed of light in a vacuum
|
||||||
|
Eps_0 = 8.8541878 * 10**(-12) # epsilon naught (vacuum permittivity)
|
||||||
|
Mu_0 = 1.257 * 10**(-6) # mu naught (vacuum permeability)
|
||||||
|
|
||||||
|
|
||||||
|
# the GSSI field unit used
|
||||||
|
UNIT = {
|
||||||
|
0: 'synthetic/gprMax',
|
||||||
|
1: 'unknown system type',
|
||||||
|
2: 'SIR 2000',
|
||||||
|
3: 'SIR 3000',
|
||||||
|
4: 'TerraVision',
|
||||||
|
6: 'SIR 20',
|
||||||
|
7: 'StructureScan Mini',
|
||||||
|
8: 'SIR 4000',
|
||||||
|
9: 'SIR 30',
|
||||||
|
10: 'unknown system type',
|
||||||
|
11: 'unknown system type',
|
||||||
|
12: 'UtilityScan DF',
|
||||||
|
13: 'HS',
|
||||||
|
14: 'StructureScan Mini XT',
|
||||||
|
}
|
||||||
|
|
||||||
|
# a dictionary of standard gssi antenna codes and frequencies
|
||||||
|
# unsure of what they all look like in code, however
|
||||||
|
ANT = {
|
||||||
|
'100MHz': 100,
|
||||||
|
'200MHz': 200,
|
||||||
|
'270MHz': 270,
|
||||||
|
'300/800D': 300, # technically two separate antennas and therefore, I think, a bug. users of this antenna will have to use `-a 300,800`
|
||||||
|
'350MHz': 350,
|
||||||
|
'400MHz': 400,
|
||||||
|
'500MHz': 500,
|
||||||
|
'800MHz': 800,
|
||||||
|
'900MHz': 900,
|
||||||
|
'1.5/1.6GHz': 1500,
|
||||||
|
'1600MHz': 1600,
|
||||||
|
'2000MHz': 2000,
|
||||||
|
'2300MHz': 2300,
|
||||||
|
'2600MHz': 2600,
|
||||||
|
'3200': 'adjustable',
|
||||||
|
'3200MLF': 'adjustable',
|
||||||
|
'gprMa': 'adjustable', # gprMax support
|
||||||
|
'GSSI': 'adjustable', # support for issue #11
|
||||||
|
'CUSTOM': 'adjustable',
|
||||||
|
'3207': 100,
|
||||||
|
'3207AP': 100,
|
||||||
|
'5106': 200,
|
||||||
|
'5106A': 200,
|
||||||
|
'50300': 300,
|
||||||
|
'350': 350,
|
||||||
|
'350HS': 350,
|
||||||
|
'D400HS': 350,
|
||||||
|
'50270': 270,
|
||||||
|
'50270S': 270,
|
||||||
|
'D50300': 300,
|
||||||
|
'5103': 400,
|
||||||
|
'5103A': 400,
|
||||||
|
'50400': 400,
|
||||||
|
'50400S': 400,
|
||||||
|
'3102 500MHZ': 500,
|
||||||
|
'3102': 500, # presumed to exist
|
||||||
|
'800': 800,
|
||||||
|
'D50800': 800,
|
||||||
|
'3101': 900,
|
||||||
|
'3101A': 900,
|
||||||
|
'51600': 1600,
|
||||||
|
'51600S': 1600,
|
||||||
|
'SS MINI': 1600,
|
||||||
|
'4105NR': 2000,
|
||||||
|
'42000S': 2000,
|
||||||
|
'62000': 2000,
|
||||||
|
'62000-003': 2000,
|
||||||
|
'62300': 2300,
|
||||||
|
'62300XT': 2300,
|
||||||
|
'52600': 2600,
|
||||||
|
'52600S': 2600,
|
||||||
|
'SSMINIXT': 2700,
|
||||||
|
}
|
||||||
|
|
||||||
|
# whether or not the file is GPS-enabled (does not guarantee presence of GPS data in file)
|
||||||
|
GPS = {
|
||||||
|
1: 'no',
|
||||||
|
2: 'yes',
|
||||||
|
}
|
||||||
|
|
||||||
|
# bits per data word in radar array
|
||||||
|
BPS = {
|
||||||
|
8: '8 unsigned',
|
||||||
|
16: '16 unsigned',
|
||||||
|
32: '32 signed'
|
||||||
|
}
|
419
readgssi/dzt.py
普通文件
419
readgssi/dzt.py
普通文件
@@ -0,0 +1,419 @@
|
|||||||
|
import struct
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
from pandas import DataFrame
|
||||||
|
from datetime import datetime
|
||||||
|
from itertools import takewhile
|
||||||
|
from readgssi.gps import readdzg
|
||||||
|
import readgssi.functions as fx
|
||||||
|
from readgssi.constants import *
|
||||||
|
from readgssi.dzx import get_user_marks, get_picks
|
||||||
|
|
||||||
|
"""
|
||||||
|
helper module for reading information from GSSI DZT files
|
||||||
|
|
||||||
|
the main function, readdzt(), returns header information in dictionary format
|
||||||
|
and the radar profile in a numpy array.
|
||||||
|
|
||||||
|
readdzt_gprpy() restructures output for use specifically with
|
||||||
|
Alain Plattner's GPRPy software (https://github.com/NSGeophysics/GPRPy).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def readtime(bytez):
|
||||||
|
"""
|
||||||
|
Function to read dates from :code:`rfDateByte` binary objects in DZT headers.
|
||||||
|
|
||||||
|
DZT :code:`rfDateByte` objects are 32 bits of binary (01001010111110011010011100101111),
|
||||||
|
structured as little endian u5u6u5u5u4u7 where all numbers are base 2 unsigned int (uX)
|
||||||
|
composed of X number of bits. Four bytes is an unnecessarily high level of compression
|
||||||
|
for a single date object in a filetype that often contains tens or hundreds of megabytes
|
||||||
|
of array information anyway.
|
||||||
|
|
||||||
|
So this function reads (seconds/2, min, hr, day, month, year-1980) then does
|
||||||
|
seconds*2 and year+1980 and returns a datetime object.
|
||||||
|
|
||||||
|
For more information on :code:`rfDateByte`, see page 55 of
|
||||||
|
`GSSI's SIR 3000 manual <https://support.geophysical.com/gssiSupport/Products/Documents/Control%20Unit%20Manuals/GSSI%20-%20SIR-3000%20Operation%20Manual.pdf>`_.
|
||||||
|
|
||||||
|
:param bytes bytes: The :code:`rfDateByte` to be decoded
|
||||||
|
:rtype: :py:class:`datetime.datetime`
|
||||||
|
"""
|
||||||
|
dtbits = ''
|
||||||
|
rfDateByte = (b for b in bytez)
|
||||||
|
for byte in rfDateByte: # assemble the binary string
|
||||||
|
for i in range(8):
|
||||||
|
dtbits += str((byte >> i) & 1)
|
||||||
|
dtbits = dtbits[::-1] # flip the string
|
||||||
|
sec2 = int(dtbits[27:32], 2) * 2 # seconds are stored as seconds/2 because there's only 5 bytes to work with
|
||||||
|
mins = int(dtbits[21:27], 2) # minutes
|
||||||
|
hr = int(dtbits[16:21], 2) # hours
|
||||||
|
day = int(dtbits[11:16], 2) # day
|
||||||
|
mo = int(dtbits[7:11], 2) # month
|
||||||
|
yr = int(dtbits[0:7], 2) + 1980 # year, stored as 1980+(0:127)
|
||||||
|
return datetime(yr, mo, day, hr, mins, sec2, 0, tzinfo=pytz.UTC)
|
||||||
|
|
||||||
|
|
||||||
|
def arraylist(header, data):
|
||||||
|
# create a list of n arrays, where n is the number of channels
|
||||||
|
data = data.astype(np.int32)
|
||||||
|
chans = list(range(header['rh_nchan']))
|
||||||
|
|
||||||
|
# set up list of arrays
|
||||||
|
img_arr = data[:header['rh_nchan']*header['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)*header['rh_nsamp']:(ar+1)*header['rh_nsamp']] # break apart
|
||||||
|
new_arr[ar] = a[header['timezero'][ar]:,:int(img_arr.shape[1])] # put into dict form
|
||||||
|
|
||||||
|
return new_arr
|
||||||
|
|
||||||
|
|
||||||
|
def readdzt(infile, gps=DataFrame(), spm=None, start_scan=0, num_scans=-1,
|
||||||
|
epsr=None, antfreq=[None,None,None,None], verbose=False,
|
||||||
|
zero=[None,None,None,None]):
|
||||||
|
"""
|
||||||
|
Function to unpack and return things the program needs from the file header, and the data itself.
|
||||||
|
|
||||||
|
:param str infile: The DZT file location
|
||||||
|
:param bool gps: Whether a GPS file exists. Defaults to False, but changed to :py:class:`pandas.DataFrame` if a DZG file with the same name as :code:`infile` exists.
|
||||||
|
:param float spm: User value of samples per meter, if specified. Defaults to None.
|
||||||
|
:param float epsr: User value of relative permittivity, if specified. Defaults to None.
|
||||||
|
:param list[int,int,int,int] zero: List of time-zero values per channel. Defaults to a list of :code:`None` values, which resolves to :code:`rh_zero`.
|
||||||
|
: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`)
|
||||||
|
"""
|
||||||
|
|
||||||
|
'''
|
||||||
|
currently unused but potentially useful lines:
|
||||||
|
# headerstruct = '<5h 5f h 4s 4s 7h 3I d I 3c x 3h d 2x 2c s s 14s s s 12s h 816s 76s' # the structure of the bytewise header and "gps data" as I understand it - 1024 bytes
|
||||||
|
# readsize = (2,2,2,2,2,4,4,4,4,4,2,4,4,4,2,2,2,2,2,4,4,4,8,4,3,1,2,2,2,8,1,1,14,1,1,12,2) # the variable size of bytes in the header (most of the time) - 128 bytes
|
||||||
|
# fx.printmsg('total header structure size: '+str(calcsize(headerstruct)))
|
||||||
|
# packed_size = 0
|
||||||
|
# for i in range(len(readsize)): packed_size = packed_size+readsize[i]
|
||||||
|
# fx.printmsg('fixed header size: '+str(packed_size)+'\\n')
|
||||||
|
'''
|
||||||
|
infile_gps = os.path.splitext(infile)[0] + ".DZG"
|
||||||
|
infile_dzx = os.path.splitext(infile)[0] + ".DZX"
|
||||||
|
infile = open(infile, 'rb')
|
||||||
|
header = {}
|
||||||
|
header['infile'] = infile.name
|
||||||
|
header['known_ant'] = [None, None, None, None]
|
||||||
|
header['dzt_ant'] = [None, None, None, None]
|
||||||
|
header['rh_ant'] = [None, None, None, None]
|
||||||
|
header['rh_antname'] = [None, None, None, None]
|
||||||
|
header['antfreq'] = [None, None, None, None]
|
||||||
|
header['timezero'] = [None, None, None, None]
|
||||||
|
|
||||||
|
# begin read
|
||||||
|
|
||||||
|
header['rh_tag'] = struct.unpack('<h', infile.read(2))[0] # 0x00ff if header, 0xfnff if old file format
|
||||||
|
header['rh_data'] = struct.unpack('<h', infile.read(2))[0] # offset to data from beginning of file
|
||||||
|
header['rh_nsamp'] = struct.unpack('<h', infile.read(2))[0] # samples per scan
|
||||||
|
header['rh_bits'] = struct.unpack('<h', infile.read(2))[0] # bits per data word
|
||||||
|
header['rh_zero'] = struct.unpack('<h', infile.read(2))[0] # if sir-30 or utilityscan df, then repeats per sample; otherwise 0x80 for 8bit and 0x8000 for 16bit
|
||||||
|
header['rhf_sps'] = struct.unpack('<f', infile.read(4))[0] # scans per second
|
||||||
|
header['dzt_sps'] = header['rhf_sps']
|
||||||
|
header['rhf_spm'] = struct.unpack('<f', infile.read(4))[0] # scans per meter
|
||||||
|
header['dzt_spm'] = header['rhf_spm']
|
||||||
|
if spm:
|
||||||
|
header['rhf_spm'] = spm
|
||||||
|
|
||||||
|
header['rhf_mpm'] = struct.unpack('<f', infile.read(4))[0] # meters per mark
|
||||||
|
header['rhf_position'] = struct.unpack('<f', infile.read(4))[0] # position (ns)
|
||||||
|
header['rhf_range'] = struct.unpack('<f', infile.read(4))[0] # range (ns)
|
||||||
|
header['rh_npass'] = struct.unpack('<h', infile.read(2))[0] # number of passes for 2-D files
|
||||||
|
# bytes 32-36 and 36-40: creation and modification date and time in bits
|
||||||
|
# structured as little endian u5u6u5u5u4u7
|
||||||
|
infile.seek(32)
|
||||||
|
try:
|
||||||
|
header['rhb_cdt'] = readtime(infile.read(4))
|
||||||
|
except:
|
||||||
|
header['rhb_cdt'] = datetime(1980, 1, 1)
|
||||||
|
try:
|
||||||
|
header['rhb_mdt'] = readtime(infile.read(4))
|
||||||
|
except:
|
||||||
|
header['rhb_mdt'] = datetime(1980, 1, 1)
|
||||||
|
header['rh_rgain'] = struct.unpack('<h', infile.read(2))[0] # offset to range gain function
|
||||||
|
header['rh_nrgain'] = struct.unpack('<h', infile.read(2))[0] # size of range gain function
|
||||||
|
infile.seek(header['rh_rgain'])
|
||||||
|
try:
|
||||||
|
header['rgain_bytes'] = infile.read(header['rh_nrgain'])
|
||||||
|
except:
|
||||||
|
fx.printmsg('WARNING: Could not read range gain function')
|
||||||
|
infile.seek(44)
|
||||||
|
header['rh_text'] = struct.unpack('<h', infile.read(2))[0] # offset to text
|
||||||
|
header['rh_ntext'] = struct.unpack('<h', infile.read(2))[0] # size of text
|
||||||
|
header['rh_proc'] = struct.unpack('<h', infile.read(2))[0] # offset to processing history
|
||||||
|
header['rh_nproc'] = struct.unpack('<h', infile.read(2))[0] # size of processing history
|
||||||
|
header['rh_nchan'] = struct.unpack('<h', infile.read(2))[0] # number of channels
|
||||||
|
if epsr != None: # in this case the user has specified an epsr value
|
||||||
|
header['dzt_epsr'] = struct.unpack('<f', infile.read(4))[0]
|
||||||
|
header['rhf_epsr'] = epsr
|
||||||
|
else:
|
||||||
|
header['rhf_epsr'] = struct.unpack('<f', infile.read(4))[0] # epsr (sometimes referred to as "dielectric permittivity")
|
||||||
|
header['dzt_epsr'] = header['rhf_epsr']
|
||||||
|
|
||||||
|
# calculate relative wave celerity given epsr value(s)
|
||||||
|
header['cr'] = 1 / math.sqrt(Mu_0 * Eps_0 * header['rhf_epsr']+1)
|
||||||
|
header['cr_true'] = 1 / math.sqrt(Mu_0 * Eps_0 * header['dzt_epsr']+1)
|
||||||
|
|
||||||
|
header['rhf_top'] = struct.unpack('<f', infile.read(4))[0] # from experimentation, it seems this is the data top position in meters
|
||||||
|
header['dzt_depth'] = struct.unpack('<f', infile.read(4))[0] # range in meters based on DZT rhf_epsr, before subtracting rhf_top
|
||||||
|
if (header['dzt_depth'] == 0):
|
||||||
|
# if dzt depth is 0, we need to calculate it using cr and rhf_range (converted to seconds)
|
||||||
|
header['dzt_depth'] = header['cr'] * (header['rhf_range'] * (10 ** (-10)))
|
||||||
|
|
||||||
|
header['rhf_depth'] = header['dzt_depth'] * (math.sqrt(header['dzt_epsr']) / math.sqrt(header['rhf_epsr']+1)) # range based on user epsr, before subtracting rhf_top
|
||||||
|
|
||||||
|
# getting into largely useless territory (under "normal" operation)
|
||||||
|
header['rh_xstart'] = struct.unpack('<f', infile.read(4))[0] # starting x grid coordinate? part of rh_coordx
|
||||||
|
header['rh_xend'] = struct.unpack('<f', infile.read(4))[0] # ending x grid coordinate? part of rh_coordx
|
||||||
|
header['rhf_servo_level'] = struct.unpack('<f', infile.read(4))[0] # gain servo level
|
||||||
|
# 3 "reserved" bytes
|
||||||
|
infile.seek(81)
|
||||||
|
header['rh_accomp'] = struct.unpack('B', infile.read(1))[0] # Ant Conf component
|
||||||
|
header['rh_sconfig'] = struct.unpack('<h', infile.read(2))[0] # setup config number
|
||||||
|
header['rh_spp'] = struct.unpack('<h', infile.read(2))[0] # scans per pass
|
||||||
|
header['rh_linenum'] = struct.unpack('<h', infile.read(2))[0] # line number
|
||||||
|
header['rh_ystart'] = struct.unpack('<f', infile.read(4))[0] # starting y grid coordinate? part of rh_coordx
|
||||||
|
header['rh_yend'] = struct.unpack('<f', infile.read(4))[0] # ending y grid coordinate? part of rh_coordx
|
||||||
|
|
||||||
|
header['rh_96'] = infile.read(1)
|
||||||
|
header['rh_lineorder'] = int('{0:08b}'.format(ord(header['rh_96']))[::-1][4:], 2)
|
||||||
|
header['rh_slicetype'] = int('{0:08b}'.format(ord(header['rh_96']))[::-1][:4], 2)
|
||||||
|
header['rh_dtype'] = infile.read(1) # no description of dtype
|
||||||
|
|
||||||
|
freq = [None, None, None, None]
|
||||||
|
for i in range(header['rh_nchan']):
|
||||||
|
if (antfreq != None) and (antfreq != [None, None, None, None]):
|
||||||
|
try:
|
||||||
|
freq[i] = antfreq[i]
|
||||||
|
except (TypeError, IndexError) as e:
|
||||||
|
freq[i] = 200
|
||||||
|
print('WARNING: due to an error, antenna %s frequency was set to 200 MHz' % (i))
|
||||||
|
print('Error detail: %s' % (e))
|
||||||
|
|
||||||
|
curpos = infile.tell()
|
||||||
|
# read frequencies for multiple antennae
|
||||||
|
for chan in list(range(header['rh_nchan'])):
|
||||||
|
if chan == 0:
|
||||||
|
infile.seek(98) # start of antenna section
|
||||||
|
else:
|
||||||
|
infile.seek(98 + (MINHEADSIZE*(chan))) # start of antenna bytes for channel n
|
||||||
|
header['dzt_ant'][chan] = infile.read(14)
|
||||||
|
header['rh_ant'][chan] = header['dzt_ant'][chan].decode('utf-8').split('\x00')[0]
|
||||||
|
header['rh_antname'][chan] = header['rh_ant'][chan].rsplit('x')[0]
|
||||||
|
try:
|
||||||
|
header['antfreq'][chan] = ANT[header['rh_antname'][chan]]
|
||||||
|
header['known_ant'][chan] = True
|
||||||
|
except KeyError:
|
||||||
|
header['known_ant'][chan] = False
|
||||||
|
try:
|
||||||
|
header['antfreq'][chan] = int("".join(takewhile(str.isdigit, header['rh_ant'][chan].replace('D5','').replace('D6','')))) # hoping this works
|
||||||
|
except ValueError:
|
||||||
|
header['antfreq'] = freq
|
||||||
|
#header['antfreq'][chan] = int(header['rh_antname'][chan].replace('D5','').replace('D6',''))
|
||||||
|
|
||||||
|
infile.seek(curpos+14)
|
||||||
|
header['rh_112'] = infile.read(1)
|
||||||
|
header['rh_lineorder'] = int('{0:08b}'.format(ord(header['rh_112']))[4:], 2)
|
||||||
|
header['rh_slicetype'] = int('{0:08b}'.format(ord(header['rh_112']))[:4], 2)
|
||||||
|
|
||||||
|
#infile.seek(113) # byte 113
|
||||||
|
header['vsbyte'] = infile.read(1) # byte containing versioning bits
|
||||||
|
header['rh_version'] = int('{0:08b}'.format(ord(header['vsbyte']))[5:], 2) # ord(vsbyte) >> 5 # whether or not the system is GPS-capable, 1=no 2=yes (does not mean GPS is in file)
|
||||||
|
header['rh_system'] = int('{0:08b}'.format(ord(header['vsbyte']))[:5], 2) # ord(vsbyte) >> 3 ## the system type (values in UNIT={...} dictionary in constants.py)
|
||||||
|
header['rh_name'] = infile.read(12)
|
||||||
|
header['rh_chksum'] = infile.read(2)
|
||||||
|
header['INFOAREA'] = infile.read(MINHEADSIZE-PAREASIZE-GPSAREASIZE)
|
||||||
|
header['rh_RGPS0'] = infile.read(RGPSSIZE)
|
||||||
|
header['rh_RGPS1'] = infile.read(RGPSSIZE)
|
||||||
|
|
||||||
|
if header['rh_system'] == 14: # hardcoded because this is so frustrating. assuming no other antennas can be paired with SS Mini XT
|
||||||
|
header['rh_antname'] = ['SSMINIXT', None, None, None]
|
||||||
|
header['antfreq'] = [2700, None, None, None]
|
||||||
|
header['known_ant'] = [True, False, False, False]
|
||||||
|
|
||||||
|
if header['rh_data'] < MINHEADSIZE: # whether or not the header is normal or big-->determines offset to data array
|
||||||
|
header['data_offset'] = MINHEADSIZE * header['rh_data']
|
||||||
|
else:
|
||||||
|
header['data_offset'] = MINHEADSIZE * header['rh_nchan']
|
||||||
|
|
||||||
|
infile.seek(MINHEADSIZE * header['rh_nchan'])
|
||||||
|
header['header_extra'] = infile.read(header['data_offset'] - (MINHEADSIZE * header['rh_nchan']))
|
||||||
|
|
||||||
|
if header['rh_bits'] == 8:
|
||||||
|
dtype = np.uint8 # 8-bit unsigned
|
||||||
|
elif header['rh_bits'] == 16:
|
||||||
|
dtype = np.uint16 # 16-bit unsigned
|
||||||
|
else:
|
||||||
|
dtype = np.int32 # 32-bit signed
|
||||||
|
header['dtype'] = dtype
|
||||||
|
|
||||||
|
if start_scan != 0:
|
||||||
|
try:
|
||||||
|
# calculate start offset in bytes:
|
||||||
|
start_offset = int(start_scan * header['rh_nchan'] * header['rh_nsamp'] * header['rh_bits']/8)
|
||||||
|
except ValueError:
|
||||||
|
# if this fails, then fall back to 0 offset.
|
||||||
|
start_offset = 0
|
||||||
|
fx.printmsg('WARNING: ValueError for scan offset: {start_scan} (reading from start of data)')
|
||||||
|
# consider returning No Data?
|
||||||
|
else:
|
||||||
|
start_offset = 0
|
||||||
|
|
||||||
|
if num_scans != -1:
|
||||||
|
try:
|
||||||
|
num_items = int(num_scans * header['rh_nsamp']*header['rh_nchan'])
|
||||||
|
except ValueError:
|
||||||
|
# if this fails then get all scans...
|
||||||
|
fx.printmsg('WARNING: ValueError for number of scans: {num_scans} (reading all items from {start_scan} scans)')
|
||||||
|
num_items = -1
|
||||||
|
else:
|
||||||
|
num_items = -1
|
||||||
|
|
||||||
|
# read in and transpose data
|
||||||
|
data = np.fromfile(infile, dtype, count=num_items)
|
||||||
|
data = data.reshape(-1,(header['rh_nsamp']*header['rh_nchan'])) # offset=start_offset,
|
||||||
|
data = data.T
|
||||||
|
header['shape'] = data.shape
|
||||||
|
|
||||||
|
header['ns_per_zsample'] = ((header['rhf_depth']-header['rhf_top']) * 2) / (header['rh_nsamp'] * header['cr'])
|
||||||
|
header['samp_freq'] = 1 / ((header['dzt_depth'] * 2) / (header['rh_nsamp'] * header['cr_true']))
|
||||||
|
|
||||||
|
try:
|
||||||
|
header['sec'] = data.shape[1]/float(header['rhf_sps'])
|
||||||
|
except ZeroDivisionError:
|
||||||
|
header['sec'] = 1.
|
||||||
|
|
||||||
|
infile.close()
|
||||||
|
|
||||||
|
|
||||||
|
for i in range(header['rh_nchan']):
|
||||||
|
try:
|
||||||
|
header['timezero'][i] = int(list(zero)[i])
|
||||||
|
except (TypeError, IndexError):
|
||||||
|
fx.printmsg('WARNING: no time zero specified for channel %s, defaulting to rh_zero value (%s)' % (i, header['rh_zero']))
|
||||||
|
header['timezero'][i] = header['rh_zero']
|
||||||
|
|
||||||
|
if os.path.isfile(infile_gps):
|
||||||
|
try:
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('reading GPS file...')
|
||||||
|
gps = readdzg(infile_gps, 'dzg', header, verbose=verbose)
|
||||||
|
except IOError as e0:
|
||||||
|
fx.printmsg('WARNING: cannot read DZG file')
|
||||||
|
try:
|
||||||
|
infile_gps = os.path.splitext(infile_gps)[0] + ".csv"
|
||||||
|
gps = readdzg(infile_gps, 'csv', header, verbose=verbose)
|
||||||
|
except Exception as e1:
|
||||||
|
try:
|
||||||
|
infile_gps = os.path.splitext(infile_gps)[0] + ".CSV"
|
||||||
|
gps = readdzg(infile_gps, 'csv', header, verbose=verbose)
|
||||||
|
except Exception as e2:
|
||||||
|
fx.printmsg('ERROR reading GPS. distance normalization will not be possible.')
|
||||||
|
fx.printmsg(' details: %s' % e0)
|
||||||
|
fx.printmsg(' %s' % e1)
|
||||||
|
fx.printmsg(' %s' % e2)
|
||||||
|
gps = DataFrame()
|
||||||
|
else:
|
||||||
|
fx.printmsg('WARNING: no DZG file found for GPS input')
|
||||||
|
gps = DataFrame()
|
||||||
|
|
||||||
|
header['marks'] = []
|
||||||
|
header['picks'] = {}
|
||||||
|
|
||||||
|
if os.path.isfile(infile_dzx):
|
||||||
|
header['marks'] = get_user_marks(infile_dzx, verbose=verbose)
|
||||||
|
header['picks'] = get_picks(infile_dzx, verbose=verbose)
|
||||||
|
else:
|
||||||
|
fx.printmsg('WARNING: could not find DZX file to read metadata. Trying to read array for marks...')
|
||||||
|
|
||||||
|
tnums = np.ndarray.tolist(data[0]) # the first row of the array is trace number
|
||||||
|
usr_marks = np.ndarray.tolist(data[1]) # when the system type is SIR3000, the second row should be user marks (otherwise these are in the DZX, see note below)
|
||||||
|
i = 0
|
||||||
|
for m in usr_marks:
|
||||||
|
if m > 0:
|
||||||
|
#print(m)
|
||||||
|
header['marks'].append(i)
|
||||||
|
i += 1
|
||||||
|
if len(header['marks']) == header['shape'][1]:
|
||||||
|
fx.printmsg('number of marks matches the number of traces (%s). this is probably wrong, so throwing out the mark list.' % (len(header['marks'])))
|
||||||
|
header['marks'] = []
|
||||||
|
else:
|
||||||
|
fx.printmsg('DZT marks read successfully. marks: %s' % len(header['marks']))
|
||||||
|
fx.printmsg(' traces: %s' % header['marks'])
|
||||||
|
|
||||||
|
# make a list of data by channel
|
||||||
|
data = arraylist(header, data)
|
||||||
|
|
||||||
|
return [header, data, gps]
|
||||||
|
|
||||||
|
def readdzt_gprpy(infile):
|
||||||
|
r = readdzt(infile)
|
||||||
|
data = r[1]
|
||||||
|
header = {
|
||||||
|
'sptrace': r[0]['rh_nsamp'],
|
||||||
|
'scpsec': r[0]['rhf_sps'],
|
||||||
|
'scpmeter': r[0]['rhf_spm'],
|
||||||
|
'startposition': r[0]['rhf_position'],
|
||||||
|
'nanosecptrace': r[0]['rhf_range'],
|
||||||
|
'scansppass': r[0]['rh_npass'],
|
||||||
|
}
|
||||||
|
return data, header
|
||||||
|
|
||||||
|
def header_info(header, data):
|
||||||
|
"""
|
||||||
|
Function to print relevant header data.
|
||||||
|
|
||||||
|
:param dict header: The header dictionary
|
||||||
|
:param numpy.ndarray data: The data array
|
||||||
|
"""
|
||||||
|
fx.printmsg('system: %s (system code %s)' % (UNIT[header['rh_system']], header['rh_system']))
|
||||||
|
fx.printmsg('antennas: %s' % header['rh_antname'])
|
||||||
|
fx.printmsg('ant freqs. (MHz): %s' % header['antfreq'])
|
||||||
|
fx.printmsg('ant time zeros: %s' % header['timezero'])
|
||||||
|
|
||||||
|
for i in range(header['rh_nchan']):
|
||||||
|
if header['known_ant'][i] == True:
|
||||||
|
fx.printmsg('ant %s center freq: %s MHz' % (i, ANT[header['rh_antname'][i]]))
|
||||||
|
else:
|
||||||
|
fx.printmsg('ant %s center freq: %s MHz (antenna name %s not in current dictionary)'
|
||||||
|
% (i, header['antfreq'][i], header['rh_antname'][i]))
|
||||||
|
|
||||||
|
fx.printmsg('date created: %s' % header['rhb_cdt'])
|
||||||
|
if header['rhb_mdt'] == datetime(1980, 1, 1):
|
||||||
|
fx.printmsg('date modified: (never modified)')
|
||||||
|
else:
|
||||||
|
fx.printmsg('date modified: %s' % header['rhb_mdt'])
|
||||||
|
try:
|
||||||
|
fx.printmsg('gps-enabled file: %s' % GPS[header['rh_version']])
|
||||||
|
except (TypeError, KeyError) as e:
|
||||||
|
fx.printmsg('gps-enabled file: unknown')
|
||||||
|
fx.printmsg('number of channels: %i' % header['rh_nchan'])
|
||||||
|
fx.printmsg('samples per trace: %i' % header['rh_nsamp'])
|
||||||
|
fx.printmsg('bits per sample: %s' % BPS[header['rh_bits']])
|
||||||
|
fx.printmsg('traces per second: %.1f' % header['rhf_sps'])
|
||||||
|
if header['dzt_spm'] != header['rhf_spm']:
|
||||||
|
fx.printmsg('traces per meter: %.2f (manually set - value from DZT: %.2f)' % (float(header['rhf_spm']), float(header['dzt_spm'])))
|
||||||
|
else:
|
||||||
|
fx.printmsg('traces per meter: %.1f' % (float(header['rhf_spm'])))
|
||||||
|
if header['dzt_epsr'] != header['rhf_epsr']:
|
||||||
|
fx.printmsg('user epsr: %.1f (manually set - value from DZT: %.1f)' % (header['rhf_epsr'], header['dzt_epsr']))
|
||||||
|
else:
|
||||||
|
fx.printmsg('epsr: %.1f' % header['rhf_epsr'])
|
||||||
|
fx.printmsg('speed of wave: %.2E m/sec (%.2f%% of vacuum)' % (header['cr'], header['cr'] / C * 100))
|
||||||
|
fx.printmsg('time range (TWTT): %.1f ns' % (header['rhf_range']))
|
||||||
|
if header['dzt_depth'] != header['rhf_depth']:
|
||||||
|
fx.printmsg('sampling depth: %.1f m (manually set - value from DZT: %.1f)' % (header['rhf_depth'], header['dzt_depth']))
|
||||||
|
else:
|
||||||
|
fx.printmsg('sampling depth: %.1f m' % (header['rhf_depth']-header['rhf_top']))
|
||||||
|
fx.printmsg('"rhf_top": %.1f m' % header['rhf_top'])
|
||||||
|
fx.printmsg('"rhf_depth": %.1f m' % header['rhf_depth'])
|
||||||
|
fx.printmsg('offset to data: %i bytes' % header['data_offset'])
|
||||||
|
fx.printmsg('traces: %i' % int(header['shape'][1]/header['rh_nchan']))
|
||||||
|
fx.printmsg('seconds: %.8f' % (header['sec']))
|
||||||
|
fx.printmsg('array dimensions: %i x %i' % (header['shape'][0], header['shape'][1]))
|
137
readgssi/dzx.py
普通文件
137
readgssi/dzx.py
普通文件
@@ -0,0 +1,137 @@
|
|||||||
|
import os, sys
|
||||||
|
import xml.etree.ElementTree as et
|
||||||
|
import readgssi.functions as fx
|
||||||
|
|
||||||
|
'''
|
||||||
|
Much of the logic here is taken from gpx2dzg.io.readdzx
|
||||||
|
https://github.com/iannesbitt/gpx2dzg/blob/master/gpx2dzg/io.py#L12
|
||||||
|
|
||||||
|
## user marks look like this in unproccesed DZX:
|
||||||
|
<DZX xmlns="www.geophysical.com/DZX/1.02">
|
||||||
|
<File>
|
||||||
|
<Profile>
|
||||||
|
<WayPt>
|
||||||
|
<scan>1351</scan>
|
||||||
|
<mark>User</mark>
|
||||||
|
<name>Mark1</name>
|
||||||
|
</WayPt>
|
||||||
|
</Profile>
|
||||||
|
</File>
|
||||||
|
</DZX>
|
||||||
|
|
||||||
|
## user marks look like this in processed DZX:
|
||||||
|
<DZX xmlns="www.geophysical.com/DZX/1.02">
|
||||||
|
<ProfileGroup>
|
||||||
|
<File>
|
||||||
|
<Profile>
|
||||||
|
<WayPt>
|
||||||
|
<scan>1351</scan>
|
||||||
|
<mark>User</mark>
|
||||||
|
<name>Mark1</name>
|
||||||
|
</WayPt>
|
||||||
|
</Profile>
|
||||||
|
</File>
|
||||||
|
</ProfileGroup>
|
||||||
|
</DZX>
|
||||||
|
'''
|
||||||
|
|
||||||
|
def get_user_marks(infile, verbose=False):
|
||||||
|
'''
|
||||||
|
Find and return a list of user marks from a dzx. Since there are multiple types of dzx without good documentation, this is prone to errors.
|
||||||
|
|
||||||
|
:param str infile: The full DZX file path
|
||||||
|
:param bool verbose: Verbosity, defaults to False
|
||||||
|
'''
|
||||||
|
dzxmarks = []
|
||||||
|
with open(infile, 'r') as f:
|
||||||
|
tree = et.parse(f)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# try the 'TargetGroup' type
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('testing DZX data type (currently supported types are "File", "TargetGroup", and "ProfileGroup")')
|
||||||
|
for item in root:
|
||||||
|
if 'TargetGroup' in item.tag:
|
||||||
|
for child in item:
|
||||||
|
if 'TargetWayPt' in child.tag:
|
||||||
|
for gchild in child:
|
||||||
|
if 'scanSampChanProp' in gchild.tag:
|
||||||
|
dzxmarks.append(int(gchild.text.split(',')[0]))
|
||||||
|
assert len(dzxmarks) > 0
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('INFO: DZX type is standard ("TargetGroup")')
|
||||||
|
except AssertionError as e:
|
||||||
|
try:
|
||||||
|
# the 'File' type
|
||||||
|
dzxmarks = []
|
||||||
|
for b in root:
|
||||||
|
if 'File' in b.tag:
|
||||||
|
for child in b:
|
||||||
|
if 'Profile' in child.tag:
|
||||||
|
for gchild in child:
|
||||||
|
if 'WayPt' in gchild.tag:
|
||||||
|
usermark = False
|
||||||
|
for ggchild in gchild:
|
||||||
|
if 'scan' in ggchild.tag:
|
||||||
|
mark = int(ggchild.text)
|
||||||
|
if 'mark' in ggchild.tag:
|
||||||
|
if ggchild.text == 'User':
|
||||||
|
usermark = True
|
||||||
|
if usermark:
|
||||||
|
dzxmarks.append(mark)
|
||||||
|
|
||||||
|
assert len(dzxmarks) > 1
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('INFO: DZX type is ("File")')
|
||||||
|
|
||||||
|
except AssertionError as e:
|
||||||
|
try:
|
||||||
|
# the 'ProfileGroup' type
|
||||||
|
dzxmarks = []
|
||||||
|
for item in root:
|
||||||
|
if 'ProfileGroup' in item.tag:
|
||||||
|
for child in item:
|
||||||
|
if 'File' in child.tag:
|
||||||
|
for gchild in child:
|
||||||
|
if 'Profile' in gchild.tag:
|
||||||
|
for ggchild in gchild:
|
||||||
|
usermark = False
|
||||||
|
if 'WayPt' in ggchild.tag:
|
||||||
|
for gggchild in ggchild:
|
||||||
|
if 'scan' in gggchild.tag:
|
||||||
|
mark = int(gggchild.text)
|
||||||
|
if 'mark' in gggchild.tag:
|
||||||
|
if gggchild.text == 'User':
|
||||||
|
usermark = True
|
||||||
|
if usermark:
|
||||||
|
dzxmarks.append(mark)
|
||||||
|
assert len(dzxmarks) > 0
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('INFO: DZX type is ("ProfileGroup")')
|
||||||
|
|
||||||
|
except AssertionError as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
if len(dzxmarks) > 0:
|
||||||
|
fx.printmsg('DZX read successfully. marks: %s' % len(dzxmarks))
|
||||||
|
fx.printmsg(' traces: %s' % dzxmarks)
|
||||||
|
else:
|
||||||
|
fx.printmsg('no user marks read from DZX. if you believe this is an error, please send the DZX to ian.nesbitt@gmail.com for testing. thank you.')
|
||||||
|
|
||||||
|
return dzxmarks
|
||||||
|
|
||||||
|
def get_picks(infile, verbose=False):
|
||||||
|
picks = {
|
||||||
|
'l1': [[], []],
|
||||||
|
'l2': [[], []],
|
||||||
|
'l3': [[], []],
|
||||||
|
'l4': [[], []],
|
||||||
|
'l5': [[], []],
|
||||||
|
'l6': [[], []],
|
||||||
|
'l7': [[], []],
|
||||||
|
'l8': [[], []],
|
||||||
|
}
|
||||||
|
|
||||||
|
return picks
|
141
readgssi/filtering.py
普通文件
141
readgssi/filtering.py
普通文件
@@ -0,0 +1,141 @@
|
|||||||
|
import numpy as np
|
||||||
|
from scipy.ndimage.filters import uniform_filter1d
|
||||||
|
from scipy.signal import firwin, lfilter
|
||||||
|
import readgssi.functions as fx
|
||||||
|
|
||||||
|
"""
|
||||||
|
Mathematical filtering routines for array manipulation
|
||||||
|
Written in part by François-Xavier Simon (@fxsimon)
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def bp(ar, header, freqmin, freqmax, zerophase=True, verbose=False):
|
||||||
|
"""
|
||||||
|
Vertical butterworth bandpass. This filter is not as effective as :py:func:`triangular` and thus is not available through the command line interface or through :py:func:`readgssi.readgssi.readgssi`.
|
||||||
|
|
||||||
|
Filter design and implementation are dictated by :py:func:`obspy.signal.filter.bandpass`.
|
||||||
|
|
||||||
|
: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`
|
||||||
|
"""
|
||||||
|
from obspy.signal.filter import bandpass
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('vertical butterworth bandpass filter')
|
||||||
|
fx.printmsg('NOTE: better results are achieved with readgssi.filtering.triangular()')
|
||||||
|
#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
|
||||||
|
|
||||||
|
corners = 1
|
||||||
|
|
||||||
|
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('corners: %s, zerophase: %s' % (corners, zerophase))
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for t in ar.T:
|
||||||
|
f = bandpass(data=t, freqmin=freqmin, freqmax=freqmax, df=samp_freq, corners=corners, zerophase=zerophase)
|
||||||
|
ar[:,i] = f
|
||||||
|
i += 1
|
||||||
|
return ar
|
||||||
|
|
||||||
|
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
|
170
readgssi/functions.py
普通文件
170
readgssi/functions.py
普通文件
@@ -0,0 +1,170 @@
|
|||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
from readgssi.constants import *
|
||||||
|
import os
|
||||||
|
|
||||||
|
def printmsg(msg):
|
||||||
|
"""
|
||||||
|
Prints with date/timestamp.
|
||||||
|
|
||||||
|
:param str msg: Message to print
|
||||||
|
"""
|
||||||
|
print('%s - %s' % (datetime.now().strftime('%Y-%m-%d %H:%M:%S'), msg))
|
||||||
|
|
||||||
|
def genericerror(filetype='file'):
|
||||||
|
"""Prints a standard message for a generic error using the `gpx2dzg.functions.printmsg()` function.
|
||||||
|
This is called from functions in `gpx2dzg.io`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filetype : str
|
||||||
|
The type of file this message is about. Used to format error string.
|
||||||
|
"""
|
||||||
|
printmsg('please attach this %s to a new github issue (https://github.com/iannesbitt/readgssi/issues/new)' % filetype)
|
||||||
|
printmsg(' or send it to ian.nesbitt@gmail.com in order to have the format assessed. please also')
|
||||||
|
printmsg(' include the output of the program (i.e. copy and paste this text and the text above in')
|
||||||
|
printmsg(' the message) as this will drastically speed up my ability to help you! thanks!')
|
||||||
|
printmsg(' ~!~> I am happy to help but please note that I am not responsible for the content of your')
|
||||||
|
printmsg(' ~!~> files, only the working-ness of this software. I appreciate your understanding!')
|
||||||
|
|
||||||
|
def dzxerror(e=''):
|
||||||
|
"""Prints an error message then calls `gpx2dzg.functions.genericerror()` and passes `filetype='DZX'`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
e : str
|
||||||
|
The error message to print.
|
||||||
|
"""
|
||||||
|
printmsg('ERROR TEXT: %s' % e)
|
||||||
|
genericerror('DZX')
|
||||||
|
|
||||||
|
def dzterror(e=''):
|
||||||
|
"""Prints an error message then calls `gpx2dzg.functions.genericerror()` and passes `filetype='DZT'`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
e : str
|
||||||
|
The error message to print.
|
||||||
|
"""
|
||||||
|
printmsg('ERROR TEXT: %s' % e)
|
||||||
|
genericerror('DZT')
|
||||||
|
|
||||||
|
def naming(outfile=None, infile_basename=None, chans=[1], chan=0, normalize=False, zero=None, stack=1, reverse=False,
|
||||||
|
bgr=False, win=None, gain=None, dewow=None, freqmin=None, freqmax=None, plotting=None, zoom=None,
|
||||||
|
absval=False):
|
||||||
|
"""
|
||||||
|
The Dr. Seth W. Campbell Honorary Naming Scheme
|
||||||
|
|
||||||
|
Descriptive naming, used to indicate the processing steps done on each file, if a specific output filename is not given. The theory behind this naming scheme is simple: it can be tough to remember how you made that plot!
|
||||||
|
|
||||||
|
Named for my graduate advisor, whom I love and cherish, who introduced me to this way of naming outputs.
|
||||||
|
|
||||||
|
.. literalinclude:: naming.txt
|
||||||
|
|
||||||
|
:param str outfile: The base output filename. If None, a new :code:`outfile` will be generated from the input file basename. If it already exists, subsequent arguments will be appended. Defaults to None.
|
||||||
|
:param str infile_basename: The input file basename (without file extension). Defaults to None.
|
||||||
|
:param list[int,int,int,int] chans: A list of channels, which is converted to the number of channels using :py:func:`len`. Defaults to [1].
|
||||||
|
:param int chan: The current channel number. Defaults to 0.
|
||||||
|
:param bool normalize: Whether or not the array is distance-normalized. Defaults to False.
|
||||||
|
:param int zero: The zero point for this particular channel. Defaults to None.
|
||||||
|
:param int stack: The number of times the array was stacked. Defaults to 1.
|
||||||
|
:param bool reverse: Whether or not the file was reversed. Defaults to False.
|
||||||
|
:param bool bgr: Whether or not BGR was applied. Defaults to False.
|
||||||
|
:param int win: The BGR window size if applicable. 0 is full-width BGR, greater than 0 is window size. Defaults to None.
|
||||||
|
:param float gain: The gain value applied to the plot. Defaults to None.
|
||||||
|
:param bool dewow: Whether or not dewow was applied. Defaults to None.
|
||||||
|
:param int freqmin: The lower corner of the bandpass filter if applicable. Defaults to None.
|
||||||
|
:param int freqmax: The upper corner of the bandpass filter if applicable. Defaults to None.
|
||||||
|
:param int plotting: Stand-in for whether or not a plot was generated. The integer represents the plot height. Defaults to None.
|
||||||
|
:param list[int,int,int,int] zoom: The zoom extents applied to the image. Defaults to None.
|
||||||
|
:param bool absval: Whether or not the plot is displayed with absolute value of gradient. Defaults to False.
|
||||||
|
"""
|
||||||
|
if outfile == None:
|
||||||
|
outfile = '%s' % (os.path.join(infile_basename))
|
||||||
|
else:
|
||||||
|
if len(chans) == 1:
|
||||||
|
return os.path.splitext(outfile)[0]
|
||||||
|
else:
|
||||||
|
printmsg('Warning: Input file has multiple channels but output filename was explicitly set. Avoiding clobbering by using incremental naming.')
|
||||||
|
return '%sCh%s' % (os.path.splitext(outfile)[0], chan)
|
||||||
|
|
||||||
|
if len(chans) > 1:
|
||||||
|
outfile = '%sCh%s' % (outfile, chan)
|
||||||
|
if zero and (zero > 0):
|
||||||
|
outfile = '%sTz%s' % (outfile, zero)
|
||||||
|
if normalize:
|
||||||
|
outfile = '%sDn' % (outfile)
|
||||||
|
if freqmin and freqmax:
|
||||||
|
outfile = '%sB%s-%s' % (outfile, freqmin, freqmax)
|
||||||
|
if dewow:
|
||||||
|
outfile = '%sDw' % (outfile)
|
||||||
|
if stack > 1:
|
||||||
|
outfile = '%sS%s' % (outfile, stack)
|
||||||
|
if bgr:
|
||||||
|
outfile = '%sBgr%s' % (outfile, win)
|
||||||
|
if reverse:
|
||||||
|
outfile = '%sRv' % (outfile)
|
||||||
|
if plotting:
|
||||||
|
outfile = '%sG%s' % (outfile, int(gain))
|
||||||
|
if absval:
|
||||||
|
outfile = '%sAbs' % (outfile)
|
||||||
|
if zoom:
|
||||||
|
outfile = '%sZ' % (outfile)
|
||||||
|
for ex in zoom:
|
||||||
|
outfile = '%s.%s' % (outfile, ex)
|
||||||
|
|
||||||
|
|
||||||
|
return outfile
|
||||||
|
|
||||||
|
|
||||||
|
def zoom(zoom, extent, x, z, verbose=False):
|
||||||
|
"""
|
||||||
|
Logic to figure out how to set zoom extents. If specified limits are out of bounds, they are set back to boundary extents. If limits of a specified axis are equal, they are expanded to the full extent of that axis.
|
||||||
|
|
||||||
|
: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.
|
||||||
|
:param list[int,int,int,int] extent: Full extent boundaries of the image, in the style :py:data:`[left, right, up, down]`.
|
||||||
|
:param str x: X axis units
|
||||||
|
:param str z: Z axis units
|
||||||
|
:param bool verbose: Verbose, defaults to False
|
||||||
|
"""
|
||||||
|
|
||||||
|
for i in range(4):
|
||||||
|
if zoom[i] < 0:
|
||||||
|
zoom[i] = 0
|
||||||
|
if verbose:
|
||||||
|
printmsg('WARNING: %s zoom limit was negative, now set to zero' % ['left','right','up','down'][i])
|
||||||
|
if (zoom[0] > 0) or (zoom[1] > 0): # if a LR value has been set
|
||||||
|
if zoom[0] > extent[1]: # if L zoom is beyond extents, set back to extent limit
|
||||||
|
zoom[0] = extent[1]
|
||||||
|
if verbose:
|
||||||
|
printmsg('WARNING: left zoom limit out of bounds (limit is %s %s)' % (extent[1], x))
|
||||||
|
if zoom[1] > extent[1]: # if R zoom is beyond extents, set back to extent limit
|
||||||
|
zoom[1] = extent[1]
|
||||||
|
if verbose:
|
||||||
|
printmsg('WARNING: right zoom limit out of bounds (limit is %s %s)' % (extent[1], x))
|
||||||
|
if zoom[0] == zoom[1]: # if LR extents are impossible,
|
||||||
|
zoom[0] = extent[0] # set both to full extents
|
||||||
|
zoom[1] = extent[1]
|
||||||
|
if verbose:
|
||||||
|
printmsg('WARNING: left and right zoom values were equal or both out of bounds, now set to full extent')
|
||||||
|
else:
|
||||||
|
zoom[0] = extent[0]
|
||||||
|
zoom[1] = extent[1]
|
||||||
|
if (zoom[2] > 0) or (zoom[3] > 0): # if a UD value has been set
|
||||||
|
if zoom[2] > extent[2]: # if upper zoom is beyond extents, set back to extent limit
|
||||||
|
zoom[2] = extent[2]
|
||||||
|
if verbose:
|
||||||
|
printmsg('WARNING: upper zoom limit out of bounds (limit is %s %s)' % (extent[3], z))
|
||||||
|
if zoom[3] > extent[2]: # if lower zoom is beyond extents, set back to extent limit
|
||||||
|
zoom[3] = extent[2]
|
||||||
|
if verbose:
|
||||||
|
printmsg('WARNING: lower zoom limit out of bounds (limit is %s %s)' % (extent[3], z))
|
||||||
|
if zoom[2] == zoom[3]: # if UD extents are impossible,
|
||||||
|
zoom[2] = extent[2] # set both to full extents
|
||||||
|
zoom[3] = extent[3]
|
||||||
|
if verbose:
|
||||||
|
printmsg('WARNING: top and bottom zoom values were equal or both out of bounds, now set to full extent')
|
||||||
|
else:
|
||||||
|
zoom[2] = extent[2]
|
||||||
|
zoom[3] = extent[3]
|
||||||
|
return zoom
|
351
readgssi/gps.py
普通文件
351
readgssi/gps.py
普通文件
@@ -0,0 +1,351 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pynmea2
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from geopy.distance import geodesic
|
||||||
|
import readgssi.functions as fx
|
||||||
|
from readgssi.constants import TZ
|
||||||
|
from math import sin, cos, sqrt, atan2, radians
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
"""
|
||||||
|
contains functions for reading gps data from various formats
|
||||||
|
"""
|
||||||
|
|
||||||
|
def msgparse(msg):
|
||||||
|
"""
|
||||||
|
.. deprecated:: 0.0.12
|
||||||
|
|
||||||
|
This function returns the NMEA message variables shared by both RMC and GGA.
|
||||||
|
|
||||||
|
:param pynmea2.nmea.NMEASentence msg: A pynmea2 sentence object.
|
||||||
|
:rtype: :py:class:`datetime.datetime`, :py:class:`float`, :py:class:`float`
|
||||||
|
"""
|
||||||
|
return msg.timestamp, msg.latitude, msg.longitude
|
||||||
|
|
||||||
|
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 = datetime.combine(msg.datestamp, msg.timestamp).replace(tzinfo=TZ) # row 0's timestamp (not ideal)
|
||||||
|
if rowrmc == 1:
|
||||||
|
msg = pynmea2.parse(ln.rstrip())
|
||||||
|
ts1 = datetime.combine(msg.datestamp, msg.timestamp).replace(tzinfo=TZ) # 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 = datetime.combine(datetime(1980, 1, 1), msg.timestamp).replace(tzinfo=TZ) # row 0's timestamp (not ideal)
|
||||||
|
if rowgga == 1:
|
||||||
|
msg = pynmea2.parse(ln.rstrip())
|
||||||
|
ts1 = datetime.combine(datetime(1980, 1, 1), msg.timestamp).replace(tzinfo=TZ) # 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):
|
||||||
|
if verbose:
|
||||||
|
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 = datetime.combine(msg.datestamp, msg.timestamp).replace(tzinfo=TZ) # 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())
|
||||||
|
if msg.timestamp:
|
||||||
|
timestamp = datetime.combine(header['rhb_cdt'], msg.timestamp).replace(tzinfo=TZ) # 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:
|
||||||
|
#If the difference in Altitude (z) between points is not 0, uses Pythagorean Theorem to better calculate the distance
|
||||||
|
if abs(z1 - z0) > 1e-6:
|
||||||
|
m_geodesic = geodesic((y1, x1), (y0, x0)).meters
|
||||||
|
m += math.sqrt(m_geodesic**2 + (z1 - z0)**2)
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def pause_correct(header, dzg_file, verbose=False, **kwargs):
|
||||||
|
'''
|
||||||
|
This is a streamlined way of removing pauses from DZG files and re-assigning trace values.
|
||||||
|
GSSI controllers have a bug in which GPS sentences are collected with increasing trace numbers even though radar trace collection is stopped.
|
||||||
|
This results in a misalignment between GPS and radar traces of the same number.
|
||||||
|
The function attempts to realign the trace numbering in the GPS file by identifying "pauses", i.e. when velocity drops below a certain value for longer than a certain number of epochs.
|
||||||
|
This assumes that each GPS pause period is correctly timed with the surveyor pressing the pause/unpause button on the radar control unit.
|
||||||
|
|
||||||
|
.. warning:: This function will identify and remove ALL pauses longer than 3 epochs and renumber the traces accordingly. Obviously this can have unintended consequences if the radar controller remains collecting data during these periods. Please be extremely cautious and only use this functionality on files you know have radar pauses that are accompanied by movement pauses. A backup of the original DZG file is made each time this function is run on a file, which means that if you make a mistake, you can simply copy the DZG backup (.DZG.bak) and overwrite the output (.DZG).
|
||||||
|
|
||||||
|
.. note:: Any time you are working with original files, it is always good to have a "working" and "raw" copy of your data. Experimental functionality in readgssi cannot be held responsible for its actions in modifying data. You are responsible for keeping a raw backup of your data just in case.
|
||||||
|
|
||||||
|
A detailed explanation of each step taken by this function is available in the code comments.
|
||||||
|
|
||||||
|
:param dict header: File header produced by :py:func:`readgssi.dzt.readdzt`
|
||||||
|
:param str dzg_file: DZG GPS file (the original .DZG, not the backup)
|
||||||
|
:param float threshold: Numerical velocities threshold, under which will be considered a "pause" (default: 0.25)
|
||||||
|
:param bool verbose: Verbose, defaults to False
|
||||||
|
:rtype: corrected, de-paused GPS data (pandas.DataFrame)
|
||||||
|
'''
|
||||||
|
output_file = dzg_file # we need RADAN and readgssi to find the modified DZG file
|
||||||
|
backup_file = dzg_file + '.bak' # so we back it up and rewrite the original
|
||||||
|
# ensuring a DZG backup
|
||||||
|
if os.path.isfile(dzg_file): # if there is a DZG
|
||||||
|
if not os.path.isfile(backup_file): # if there is not a backup, make one
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('backing up original DZG file to %s' % (backup_file))
|
||||||
|
copyfile(dzg_file, backup_file)
|
||||||
|
else: # if there already is a backup
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('found backed up DZG file %s' % (backup_file))
|
||||||
|
dzg_file = backup_file # we always want to draw from the backup and write to the main DZG file
|
||||||
|
|
||||||
|
# threshold default
|
||||||
|
threshold = 0.25
|
||||||
|
# get threshold value from kwargs if it exists
|
||||||
|
for arg in kwargs:
|
||||||
|
if arg == "threshold":
|
||||||
|
if (type(kwargs[arg]) == float) or (type(kwargs[arg]) == int):
|
||||||
|
threshold = float(kwargs[arg])
|
||||||
|
|
||||||
|
# pandas ninja maneuvers to get a list of pause boundaries
|
||||||
|
orig_gps = readdzg(fi=backup_file, frmt='dzg', header=header, verbose=False) # get original GPS values
|
||||||
|
orig_gps['groups'] = pd.cut(orig_gps.velocity,[-1,threshold,100000]) # segment file into groups based on velocity
|
||||||
|
orig_gps['cats'] = (orig_gps.groups != orig_gps.groups.shift()).cumsum() # give each group a number
|
||||||
|
orig_gps['threshold'] = 0 # create threshold column
|
||||||
|
orig_gps.loc[3:-3, 'threshold'] = threshold # ignoring start and end epochs, assign threshold
|
||||||
|
orig_gps['mask'] = orig_gps['velocity'] < orig_gps['threshold'] # where does velocity fall beneath threshold
|
||||||
|
exceeds = orig_gps[orig_gps['mask'] == True] # create a view based on non exceedence rows
|
||||||
|
bounds = []
|
||||||
|
if exceeds.cats.count() > 0: # test whether any exceedences were detected
|
||||||
|
for i in range(exceeds.cats.max() + 1): # for each group in the view
|
||||||
|
if exceeds[exceeds['cats'] == i].count().cats > 3: # are there more than 3 epochs in the group?
|
||||||
|
bounds.append([exceeds[exceeds['cats'] == i].iloc[0].name, # if so create a data point in the list
|
||||||
|
exceeds[exceeds['cats'] == i].iloc[-1].name, # with time and trace number of boundary
|
||||||
|
exceeds[exceeds['cats'] == i].iloc[0].trace,
|
||||||
|
exceeds[exceeds['cats'] == i].iloc[-1].trace])
|
||||||
|
|
||||||
|
def new_row_value(trace): # quick and dirty function to figure out new trace number
|
||||||
|
subtract = 0
|
||||||
|
for pause in bounds:
|
||||||
|
if (trace >= pause[2]) and (trace >= pause[3]):
|
||||||
|
subtract = subtract + pause[3] - pause[2]
|
||||||
|
return trace - subtract
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('found %s pause periods' % len(bounds))
|
||||||
|
if len(bounds) > 0:
|
||||||
|
i = 1
|
||||||
|
for pause in bounds:
|
||||||
|
fx.printmsg('pause %s' % i)
|
||||||
|
fx.printmsg(' start trace: %s (%s)' % (pause[2], pause[0]))
|
||||||
|
fx.printmsg(' end trace: %s (%s)' % (pause[3], pause[1]))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('transcribing DZG file with new trace values...')
|
||||||
|
with open(dzg_file, 'r') as gf: # gps file
|
||||||
|
with open(output_file, 'w') as tf: # transcription file
|
||||||
|
subtract_amount = 0
|
||||||
|
for ln in gf: # loop over backup file line by line
|
||||||
|
if '$GSSIS' in ln:
|
||||||
|
write = True # assume this line will get written
|
||||||
|
# if it's a GSSI sentence, grab the scan/trace number
|
||||||
|
trace = int(ln.split(',')[1])
|
||||||
|
for pause in bounds: # for each pause period
|
||||||
|
if (trace >= pause[2]) and (trace <= pause[3]): # if trace is in pause bounds
|
||||||
|
write = False # do not write this group of sentences to file
|
||||||
|
if write: # if it is outside of a pause period
|
||||||
|
tf.write(ln.replace(str(trace), str(new_row_value(trace)))) # replace trace value and write line to file
|
||||||
|
else: # if it's not a GSSI proprietary line
|
||||||
|
if write: # and it's outside a pause priod
|
||||||
|
tf.write(ln) # transcribe line to new file
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('done. reading new values into array...')
|
||||||
|
|
||||||
|
else:
|
||||||
|
fx.printmsg('no dzg file found at (%s)' % (dzg_file))
|
||||||
|
|
||||||
|
return readdzg(fi=output_file, frmt='dzg', header=header, verbose=verbose)
|
287
readgssi/plot.py
普通文件
287
readgssi/plot.py
普通文件
@@ -0,0 +1,287 @@
|
|||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib as mpl
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.colors as colors
|
||||||
|
import readgssi.functions as fx
|
||||||
|
from readgssi.constants import *
|
||||||
|
|
||||||
|
# figure out what formats are available
|
||||||
|
def get_supported_filetypes():
|
||||||
|
fig = plt.Figure()
|
||||||
|
fmts = fig.canvas.get_supported_filetypes()
|
||||||
|
plt.close('all')
|
||||||
|
fmtst = ''
|
||||||
|
for fmt in fmts:
|
||||||
|
fmtst += ' %-10s | %s\n' % (fmt, fmts[fmt])
|
||||||
|
return fmts, fmtst
|
||||||
|
|
||||||
|
fmts, fmtst = get_supported_filetypes()
|
||||||
|
|
||||||
|
"""
|
||||||
|
contains several plotting functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
def histogram(ar, verbose=True):
|
||||||
|
"""
|
||||||
|
Shows a y-log histogram of data value distribution.
|
||||||
|
|
||||||
|
:param numpy.ndarray ar: The radar array
|
||||||
|
:param bool verbose: Verbose, defaults to False
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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',
|
||||||
|
fmt='png', 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 :py:data:`fmt=readgssi_plot` (a filename :py:data:`readgssi_plot`, not including the extension which is determined by the :py:data:`fmt` variable, in the current directory).
|
||||||
|
:param str fmt: The format of the output file. Defaults to :py:data:`fmt='png'`. Acceptable values come from :py:func:`matplotlib.backend_bases.FigureCanvasBase.get_supported_filetypes`.
|
||||||
|
: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, base=np.e), 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, base=np.e), 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.%s' % (outfile, fmt))
|
||||||
|
plt.savefig('%s.%s' % (outfile, fmt), 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.%s with dpi=%s' % (os.path.splitext(header['infile'])[0], freq, fmt, dpi))
|
||||||
|
plt.savefig('%s_%sMHz.%s' % (os.path.splitext(header['infile'])[0], freq, fmt), bbox_inches='tight')
|
||||||
|
if noshow:
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('not showing matplotlib')
|
||||||
|
plt.close()
|
||||||
|
else:
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('showing matplotlib figure...')
|
||||||
|
plt.show()
|
500
readgssi/readgssi.py
普通文件
500
readgssi/readgssi.py
普通文件
@@ -0,0 +1,500 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
## readgssi.py
|
||||||
|
## intended to translate radar data from DZT to other more workable formats.
|
||||||
|
## DZT is a file format maintained by Geophysical Survey Systems Incorporated (GSSI).
|
||||||
|
## specifically, this script is intended for use with radar data recorded
|
||||||
|
## with GSSI SIR 3000 and 4000 field units. Other field unit models may record DZT slightly
|
||||||
|
## differently, in which case this script may need to be modified.
|
||||||
|
|
||||||
|
# readgssi was originally written as matlab code by
|
||||||
|
# Gabe Lewis, Dartmouth College Department of Earth Sciences.
|
||||||
|
# matlab code was adapted for python with permission by
|
||||||
|
# Ian Nesbitt, University of Maine School of Earth and Climate Sciences.
|
||||||
|
# Copyleft (c) 2017 Ian Nesbitt
|
||||||
|
|
||||||
|
# this code is freely available under the GNU Affero General Public License 3.0.
|
||||||
|
# if you did not receive a copy of the license upon obtaining this software, please visit
|
||||||
|
# (https://opensource.org/licenses/AGPL-3.0) to obtain a copy.
|
||||||
|
|
||||||
|
import sys, getopt, os
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import readgssi.functions as fx
|
||||||
|
import readgssi.plot as plot
|
||||||
|
from readgssi import translate
|
||||||
|
from readgssi import filtering
|
||||||
|
from readgssi import arrayops
|
||||||
|
from readgssi import config
|
||||||
|
from readgssi.constants import *
|
||||||
|
from readgssi.dzt import *
|
||||||
|
from readgssi.gps import pause_correct
|
||||||
|
|
||||||
|
|
||||||
|
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, absval=False,
|
||||||
|
normalize=False, specgram=False, noshow=False, spm=None,
|
||||||
|
start_scan=0, num_scans=-1, epsr=None, title=True, zoom=[0,0,0,0],
|
||||||
|
pausecorrect=False, showmarks=False):
|
||||||
|
"""
|
||||||
|
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='dzt'`, :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 by the :code:`rh_zero` variable in :py:func:`readgssi.dzt.readdzt`.
|
||||||
|
: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 absval: If :py:data:`True`, displays the absolute value of the vertical gradient of the array when plotting. Good for displaying faint array features.
|
||||||
|
: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 arrays by channel :py:class:`dict` {0: :py:class:`numpy.ndarray`, 1: :py:class:`numpy.ndarray`, etc.}, gps (False or :py:class:`pandas.DataFrame`)
|
||||||
|
:param bool pausecorrect: If :py:data:`True` or minimum speed given as :py:data:`+float`, search the DZG file for pauses, where GPS keeps recording but radar unit does not, and correct them if necessary. Defaults to :py:data:`False`. Minimum speed defaults to 0.25 m/s.
|
||||||
|
:param bool showmarks: If :py:data:`True`, display mark locations in plot. Defaults to :py:data:`False`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if infile:
|
||||||
|
# read the file
|
||||||
|
try:
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('reading...')
|
||||||
|
fx.printmsg('input file: %s' % (infile))
|
||||||
|
header, data, gps = readdzt(infile, gps=normalize, spm=spm,
|
||||||
|
start_scan=start_scan, num_scans=num_scans,
|
||||||
|
epsr=epsr, antfreq=antfreq, zero=zero,
|
||||||
|
verbose=verbose)
|
||||||
|
# print a bunch of header info
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('success. header values:')
|
||||||
|
header_info(header, data)
|
||||||
|
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')
|
||||||
|
|
||||||
|
for chan in list(range(header['rh_nchan'])):
|
||||||
|
try:
|
||||||
|
ANT[header['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 != None) and (antfreq != [None, None, None, None]):
|
||||||
|
fx.printmsg('using user-specified antenna frequency. Please ensure frequency value or list of values is correct.')
|
||||||
|
fx.printmsg('old values: %s' % (header['antfreq']))
|
||||||
|
fx.printmsg('new values: %s' % (antfreq))
|
||||||
|
header['antfreq'] = antfreq
|
||||||
|
else:
|
||||||
|
fx.printmsg('WARNING: trying to use frequencies of %s MHz (estimated)...' % (header['antfreq'][chan]))
|
||||||
|
fx.printmsg('more info: rh_ant=%s' % (header['rh_ant']))
|
||||||
|
fx.printmsg(' known_ant=%s' % (header['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('--------------------------------------------------------------')
|
||||||
|
|
||||||
|
chans = list(range(header['rh_nchan']))
|
||||||
|
outfiles = {}
|
||||||
|
|
||||||
|
if (pausecorrect) and (not gps.empty):
|
||||||
|
kwargs = {}
|
||||||
|
fx.printmsg('correcting GPS errors created by user-initiated recording pauses...')
|
||||||
|
if (type(pausecorrect) == float) or (type(pausecorrect) == int):
|
||||||
|
kwargs['threshold'] = pausecorrect
|
||||||
|
fx.printmsg('pause velocity threshold is %s m/s (user-specified)' % (kwargs['threshold']))
|
||||||
|
else:
|
||||||
|
fx.printmsg('pause velocity threshold is 0.25 m/s (default)')
|
||||||
|
gps = pause_correct(header=header, dzg_file=os.path.splitext(infile)[0] + ".DZG", verbose=verbose, **kwargs)
|
||||||
|
elif (pausecorrect) and (gps.empty):
|
||||||
|
fx.printmsg("can't correct pauses without a valid DZG file to look for. are you sure the DZG has the same name as the DZT file?")
|
||||||
|
|
||||||
|
|
||||||
|
for ar in data:
|
||||||
|
"""
|
||||||
|
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, header['rh_antname'][ar]))
|
||||||
|
# execute filtering functions if necessary
|
||||||
|
if normalize:
|
||||||
|
header, data[ar], gps = arrayops.distance_normalize(header=header, ar=data[ar], gps=gps,
|
||||||
|
verbose=verbose)
|
||||||
|
if dewow:
|
||||||
|
# dewow
|
||||||
|
data[ar] = filtering.dewow(ar=data[ar], verbose=verbose)
|
||||||
|
if freqmin and freqmax:
|
||||||
|
# vertical triangular bandpass
|
||||||
|
data[ar] = filtering.triangular(ar=data[ar], header=header, freqmin=freqmin, freqmax=freqmax,
|
||||||
|
zerophase=True, verbose=verbose)
|
||||||
|
if stack != 1:
|
||||||
|
# horizontal stacking
|
||||||
|
header, data[ar], stack = arrayops.stack(ar=data[ar],
|
||||||
|
header=header,
|
||||||
|
stack=stack,
|
||||||
|
verbose=verbose)
|
||||||
|
else:
|
||||||
|
stack = 1 # just in case it's not an integer
|
||||||
|
if bgr:
|
||||||
|
# background removal
|
||||||
|
data[ar] = filtering.bgr(ar=data[ar], header=header, win=win, verbose=verbose)
|
||||||
|
else:
|
||||||
|
win = None
|
||||||
|
if reverse:
|
||||||
|
# read array backwards
|
||||||
|
data[ar] = arrayops.flip(data[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
|
||||||
|
|
||||||
|
outfiles[ar] = fx.naming(outfile=outfile, infile_basename=infile_basename, chans=chans, chan=ar,
|
||||||
|
normalize=normalize, zero=header['timezero'][ar], stack=stack, reverse=reverse,
|
||||||
|
bgr=bgr, win=win, dewow=dewow, freqmin=freqmin, freqmax=freqmax, plotting=plotting,
|
||||||
|
gain=gain, absval=absval)
|
||||||
|
if plotting:
|
||||||
|
plot.radargram(ar=data[ar], ant=ar, header=header, freq=header['antfreq'][ar], verbose=verbose,
|
||||||
|
figsize=figsize, dpi=dpi, stack=stack, x=x, z=z, gain=gain, colormap=colormap,
|
||||||
|
colorbar=colorbar, noshow=noshow, outfile=outfiles[ar], fmt=frmt, win=win, title=title,
|
||||||
|
zero=header['timezero'][ar], zoom=zoom, absval=absval, showmarks=showmarks)
|
||||||
|
|
||||||
|
if histogram:
|
||||||
|
plot.histogram(ar=data[ar], verbose=verbose)
|
||||||
|
|
||||||
|
if specgram:
|
||||||
|
plot.spectrogram(ar=data[ar], header=header, freq=header['antfreq'][ar], verbose=verbose)
|
||||||
|
|
||||||
|
if frmt != None:
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('outputting to %s...' % frmt)
|
||||||
|
for ar in data:
|
||||||
|
# is there an output filepath given?
|
||||||
|
outfile_abspath = os.path.abspath(outfiles[ar]) # set output to given location
|
||||||
|
|
||||||
|
# what is the output format
|
||||||
|
if frmt in 'csv':
|
||||||
|
translate.csv(ar=data[ar], outfile_abspath=outfile_abspath,
|
||||||
|
header=header, verbose=verbose)
|
||||||
|
elif frmt in 'h5':
|
||||||
|
translate.h5(ar=data[ar], infile_basename=infile_basename,
|
||||||
|
outfile_abspath=outfile_abspath, verbose=verbose)
|
||||||
|
elif frmt in 'segy':
|
||||||
|
translate.segy(ar=data[ar], outfile_abspath=outfile_abspath,
|
||||||
|
verbose=verbose)
|
||||||
|
elif frmt in 'numpy':
|
||||||
|
translate.numpy(ar=data[ar], outfile_abspath=outfile_abspath,
|
||||||
|
verbose=verbose)
|
||||||
|
elif frmt in 'gprpy':
|
||||||
|
translate.gprpy(ar=data[ar], outfile_abspath=outfile_abspath,
|
||||||
|
header=header, verbose=verbose)
|
||||||
|
elif frmt in 'dzt':
|
||||||
|
if ar == 0:
|
||||||
|
translate.dzt(ar=data, outfile_abspath=outfile_abspath,
|
||||||
|
header=header, verbose=verbose)
|
||||||
|
if frmt in ('object', 'python'):
|
||||||
|
return header, data, gps
|
||||||
|
|
||||||
|
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, plotting, figsize, histogram, colorbar, dewow, bgr, noshow = None, None, None, None, None, None, None, None, None, None
|
||||||
|
reverse, freqmin, freqmax, specgram, normalize, spm, epsr, absval, pausecorrect, showmarks = None, None, None, None, None, None, None, None, None, None
|
||||||
|
colormap = 'gray'
|
||||||
|
x, z = 'seconds', 'nanoseconds'
|
||||||
|
frmt = 'png'
|
||||||
|
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:APM',
|
||||||
|
['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=', 'absval','pausecorrect',
|
||||||
|
'showmarks'])
|
||||||
|
# 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 = [None, None, None, None]
|
||||||
|
freq_arg = arg.split(',')
|
||||||
|
for i in range(len(freq_arg)):
|
||||||
|
antfreq[i] = round(abs(float(freq_arg[i])))
|
||||||
|
except ValueError:
|
||||||
|
fx.printmsg('ERROR: %s is not a valid number or list of frequency values.' % arg)
|
||||||
|
fx.printmsg(config.help_text)
|
||||||
|
sys.exit(2)
|
||||||
|
fx.printmsg('user specified frequency values of %s MHz will be overwritten if DZT header has valid antenna information.' % antfreq)
|
||||||
|
|
||||||
|
if opt in ('-f', '--format'): # the format string
|
||||||
|
# check whether the string is a supported format
|
||||||
|
if arg:
|
||||||
|
arg = arg.lower()
|
||||||
|
if arg in ('.dzt', 'dzt', 'gssi', 'radan'):
|
||||||
|
frmt = 'dzt'
|
||||||
|
elif 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'):
|
||||||
|
frmt = 'png'
|
||||||
|
plotting = True
|
||||||
|
elif arg in (plot.fmtst):
|
||||||
|
frmt = arg
|
||||||
|
plotting = True
|
||||||
|
else:
|
||||||
|
# else the user has given an invalid format
|
||||||
|
fx.printmsg('Invalid file format given.')
|
||||||
|
fx.printmsg(config.help_text)
|
||||||
|
sys.exit(2)
|
||||||
|
else:
|
||||||
|
fx.printmsg('No file format specified.')
|
||||||
|
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 ('-P', '--pausecorr'):
|
||||||
|
pausecorrect = 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:
|
||||||
|
fx.printmsg('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 ('-M', '--showmarks'):
|
||||||
|
showmarks = True
|
||||||
|
if opt in ('-c', '--colormap'):
|
||||||
|
if arg:
|
||||||
|
colormap = arg
|
||||||
|
if opt in ('-b', '--colorbar'):
|
||||||
|
colorbar = True
|
||||||
|
if opt in ('-A', '--absval'):
|
||||||
|
absval = 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, absval=absval, pausecorrect=pausecorrect,
|
||||||
|
showmarks=showmarks)
|
||||||
|
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)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
directs the command line call to the main argument parsing function.
|
||||||
|
"""
|
||||||
|
main()
|
501
readgssi/readgssi2.py
普通文件
501
readgssi/readgssi2.py
普通文件
@@ -0,0 +1,501 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
## readgssi.py
|
||||||
|
## intended to translate radar data from DZT to other more workable formats.
|
||||||
|
## DZT is a file format maintained by Geophysical Survey Systems Incorporated (GSSI).
|
||||||
|
## specifically, this script is intended for use with radar data recorded
|
||||||
|
## with GSSI SIR 3000 and 4000 field units. Other field unit models may record DZT slightly
|
||||||
|
## differently, in which case this script may need to be modified.
|
||||||
|
|
||||||
|
# readgssi was originally written as matlab code by
|
||||||
|
# Gabe Lewis, Dartmouth College Department of Earth Sciences.
|
||||||
|
# matlab code was adapted for python with permission by
|
||||||
|
# Ian Nesbitt, University of Maine School of Earth and Climate Sciences.
|
||||||
|
# Copyleft (c) 2017 Ian Nesbitt
|
||||||
|
|
||||||
|
# this code is freely available under the GNU Affero General Public License 3.0.
|
||||||
|
# if you did not receive a copy of the license upon obtaining this software, please visit
|
||||||
|
# (https://opensource.org/licenses/AGPL-3.0) to obtain a copy.
|
||||||
|
|
||||||
|
import sys, getopt, os
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import readgssi.functions as fx
|
||||||
|
import readgssi.plot as plot
|
||||||
|
from readgssi import translate
|
||||||
|
from readgssi import filtering
|
||||||
|
from readgssi import arrayops
|
||||||
|
from readgssi import config
|
||||||
|
from readgssi.constants import *
|
||||||
|
from readgssi.dzt import *
|
||||||
|
from readgssi.gps import pause_correct
|
||||||
|
|
||||||
|
|
||||||
|
def readgssi2(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, absval=False,
|
||||||
|
normalize=False, specgram=False, noshow=False, spm=None,
|
||||||
|
start_scan=0, num_scans=-1, epsr=None, title=True, zoom=[0,0,0,0],
|
||||||
|
pausecorrect=False, showmarks=False):
|
||||||
|
"""
|
||||||
|
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='dzt'`, :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 by the :code:`rh_zero` variable in :py:func:`readgssi.dzt.readdzt`.
|
||||||
|
: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 absval: If :py:data:`True`, displays the absolute value of the vertical gradient of the array when plotting. Good for displaying faint array features.
|
||||||
|
: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 arrays by channel :py:class:`dict` {0: :py:class:`numpy.ndarray`, 1: :py:class:`numpy.ndarray`, etc.}, gps (False or :py:class:`pandas.DataFrame`)
|
||||||
|
:param bool pausecorrect: If :py:data:`True` or minimum speed given as :py:data:`+float`, search the DZG file for pauses, where GPS keeps recording but radar unit does not, and correct them if necessary. Defaults to :py:data:`False`. Minimum speed defaults to 0.25 m/s.
|
||||||
|
:param bool showmarks: If :py:data:`True`, display mark locations in plot. Defaults to :py:data:`False`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if infile:
|
||||||
|
# read the file
|
||||||
|
try:
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('reading...')
|
||||||
|
fx.printmsg('input file: %s' % (infile))
|
||||||
|
header, data, gps = readdzt(infile, gps=normalize, spm=spm,
|
||||||
|
start_scan=start_scan, num_scans=num_scans,
|
||||||
|
epsr=epsr, antfreq=antfreq, zero=zero,
|
||||||
|
verbose=verbose)
|
||||||
|
# print a bunch of header info
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('success. header values:')
|
||||||
|
header_info(header, data)
|
||||||
|
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')
|
||||||
|
|
||||||
|
for chan in list(range(header['rh_nchan'])):
|
||||||
|
try:
|
||||||
|
ANT[header['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 != None) and (antfreq != [None, None, None, None]):
|
||||||
|
fx.printmsg('using user-specified antenna frequency. Please ensure frequency value or list of values is correct.')
|
||||||
|
fx.printmsg('old values: %s' % (header['antfreq']))
|
||||||
|
fx.printmsg('new values: %s' % (antfreq))
|
||||||
|
header['antfreq'] = antfreq
|
||||||
|
else:
|
||||||
|
fx.printmsg('WARNING: trying to use frequencies of %s MHz (estimated)...' % (header['antfreq'][chan]))
|
||||||
|
fx.printmsg('more info: rh_ant=%s' % (header['rh_ant']))
|
||||||
|
fx.printmsg(' known_ant=%s' % (header['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('--------------------------------------------------------------')
|
||||||
|
|
||||||
|
chans = list(range(header['rh_nchan']))
|
||||||
|
outfiles = {}
|
||||||
|
|
||||||
|
if (pausecorrect) and (not gps.empty):
|
||||||
|
kwargs = {}
|
||||||
|
fx.printmsg('correcting GPS errors created by user-initiated recording pauses...')
|
||||||
|
if (type(pausecorrect) == float) or (type(pausecorrect) == int):
|
||||||
|
kwargs['threshold'] = pausecorrect
|
||||||
|
fx.printmsg('pause velocity threshold is %s m/s (user-specified)' % (kwargs['threshold']))
|
||||||
|
else:
|
||||||
|
fx.printmsg('pause velocity threshold is 0.25 m/s (default)')
|
||||||
|
gps = pause_correct(header=header, dzg_file=os.path.splitext(infile)[0] + ".DZG", verbose=verbose, **kwargs)
|
||||||
|
elif (pausecorrect) and (gps.empty):
|
||||||
|
fx.printmsg("can't correct pauses without a valid DZG file to look for. are you sure the DZG has the same name as the DZT file?")
|
||||||
|
|
||||||
|
|
||||||
|
for ar in data:
|
||||||
|
"""
|
||||||
|
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, header['rh_antname'][ar]))
|
||||||
|
# execute filtering functions if necessary
|
||||||
|
if normalize:
|
||||||
|
header, data[ar], gps = arrayops.distance_normalize(header=header, ar=data[ar], gps=gps,
|
||||||
|
verbose=verbose)
|
||||||
|
if dewow:
|
||||||
|
# dewow
|
||||||
|
data[ar] = filtering.dewow(ar=data[ar], verbose=verbose)
|
||||||
|
if freqmin and freqmax:
|
||||||
|
# vertical triangular bandpass
|
||||||
|
data[ar] = filtering.triangular(ar=data[ar], header=header, freqmin=freqmin, freqmax=freqmax,
|
||||||
|
zerophase=True, verbose=verbose)
|
||||||
|
if stack != 1:
|
||||||
|
# horizontal stacking
|
||||||
|
header, data[ar], stack = arrayops.stack(ar=data[ar],
|
||||||
|
header=header,
|
||||||
|
stack=stack,
|
||||||
|
verbose=verbose)
|
||||||
|
else:
|
||||||
|
stack = 1 # just in case it's not an integer
|
||||||
|
if bgr:
|
||||||
|
# background removal
|
||||||
|
data[ar] = filtering.bgr(ar=data[ar], header=header, win=win, verbose=verbose)
|
||||||
|
else:
|
||||||
|
win = None
|
||||||
|
if reverse:
|
||||||
|
# read array backwards
|
||||||
|
data[ar] = arrayops.flip(data[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
|
||||||
|
|
||||||
|
outfiles[ar] = fx.naming(outfile=outfile, infile_basename=infile_basename, chans=chans, chan=ar,
|
||||||
|
normalize=normalize, zero=header['timezero'][ar], stack=stack, reverse=reverse,
|
||||||
|
bgr=bgr, win=win, dewow=dewow, freqmin=freqmin, freqmax=freqmax, plotting=plotting,
|
||||||
|
gain=gain, absval=absval)
|
||||||
|
if plotting:
|
||||||
|
plot.radargram(ar=data[ar], ant=ar, header=header, freq=header['antfreq'][ar], verbose=verbose,
|
||||||
|
figsize=figsize, dpi=dpi, stack=stack, x=x, z=z, gain=gain, colormap=colormap,
|
||||||
|
colorbar=colorbar, noshow=noshow, outfile=outfiles[ar], fmt=frmt, win=win, title=title,
|
||||||
|
zero=header['timezero'][ar], zoom=zoom, absval=absval, showmarks=showmarks)
|
||||||
|
|
||||||
|
if histogram:
|
||||||
|
plot.histogram(ar=data[ar], verbose=verbose)
|
||||||
|
|
||||||
|
if specgram:
|
||||||
|
plot.spectrogram(ar=data[ar], header=header, freq=header['antfreq'][ar], verbose=verbose)
|
||||||
|
|
||||||
|
if frmt != None:
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('outputting to %s...' % frmt)
|
||||||
|
for ar in data:
|
||||||
|
# is there an output filepath given?
|
||||||
|
outfile_abspath = os.path.abspath(outfiles[ar]) # set output to given location
|
||||||
|
|
||||||
|
# what is the output format
|
||||||
|
if frmt in 'csv':
|
||||||
|
translate.csv(ar=data[ar], outfile_abspath=outfile_abspath,
|
||||||
|
header=header, verbose=verbose)
|
||||||
|
elif frmt in 'h5':
|
||||||
|
translate.h5(ar=data[ar], infile_basename=infile_basename,
|
||||||
|
outfile_abspath=outfile_abspath, verbose=verbose)
|
||||||
|
elif frmt in 'segy':
|
||||||
|
translate.segy(ar=data[ar], outfile_abspath=outfile_abspath,
|
||||||
|
verbose=verbose)
|
||||||
|
elif frmt in 'numpy':
|
||||||
|
translate.numpy(ar=data[ar], outfile_abspath=outfile_abspath,
|
||||||
|
verbose=verbose)
|
||||||
|
elif frmt in 'gprpy':
|
||||||
|
translate.gprpy(ar=data[ar], outfile_abspath=outfile_abspath,
|
||||||
|
header=header, verbose=verbose)
|
||||||
|
elif frmt in 'dzt':
|
||||||
|
if ar == 0:
|
||||||
|
translate.dzt(ar=data, outfile_abspath=outfile_abspath,
|
||||||
|
header=header, verbose=verbose)
|
||||||
|
if frmt in ('object', 'python'):
|
||||||
|
return header, data, gps
|
||||||
|
'''
|
||||||
|
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, plotting, figsize, histogram, colorbar, dewow, bgr, noshow = None, None, None, None, None, None, None, None, None, None
|
||||||
|
reverse, freqmin, freqmax, specgram, normalize, spm, epsr, absval, pausecorrect, showmarks = None, None, None, None, None, None, None, None, None, None
|
||||||
|
colormap = 'gray'
|
||||||
|
x, z = 'seconds', 'nanoseconds'
|
||||||
|
frmt = 'png'
|
||||||
|
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:APM',
|
||||||
|
['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=', 'absval','pausecorrect',
|
||||||
|
'showmarks'])
|
||||||
|
# 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 = [None, None, None, None]
|
||||||
|
freq_arg = arg.split(',')
|
||||||
|
for i in range(len(freq_arg)):
|
||||||
|
antfreq[i] = round(abs(float(freq_arg[i])))
|
||||||
|
except ValueError:
|
||||||
|
fx.printmsg('ERROR: %s is not a valid number or list of frequency values.' % arg)
|
||||||
|
fx.printmsg(config.help_text)
|
||||||
|
sys.exit(2)
|
||||||
|
fx.printmsg('user specified frequency values of %s MHz will be overwritten if DZT header has valid antenna information.' % antfreq)
|
||||||
|
|
||||||
|
if opt in ('-f', '--format'): # the format string
|
||||||
|
# check whether the string is a supported format
|
||||||
|
if arg:
|
||||||
|
arg = arg.lower()
|
||||||
|
if arg in ('.dzt', 'dzt', 'gssi', 'radan'):
|
||||||
|
frmt = 'dzt'
|
||||||
|
elif 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'):
|
||||||
|
frmt = 'png'
|
||||||
|
plotting = True
|
||||||
|
elif arg in (plot.fmtst):
|
||||||
|
frmt = arg
|
||||||
|
plotting = True
|
||||||
|
else:
|
||||||
|
# else the user has given an invalid format
|
||||||
|
fx.printmsg('Invalid file format given.')
|
||||||
|
fx.printmsg(config.help_text)
|
||||||
|
sys.exit(2)
|
||||||
|
else:
|
||||||
|
fx.printmsg('No file format specified.')
|
||||||
|
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 ('-P', '--pausecorr'):
|
||||||
|
pausecorrect = 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:
|
||||||
|
fx.printmsg('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 ('-M', '--showmarks'):
|
||||||
|
showmarks = True
|
||||||
|
if opt in ('-c', '--colormap'):
|
||||||
|
if arg:
|
||||||
|
colormap = arg
|
||||||
|
if opt in ('-b', '--colorbar'):
|
||||||
|
colorbar = True
|
||||||
|
if opt in ('-A', '--absval'):
|
||||||
|
absval = 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)
|
||||||
|
readgssi2(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, absval=absval, pausecorrect=pausecorrect,
|
||||||
|
showmarks=showmarks)
|
||||||
|
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)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
directs the command line call to the main argument parsing function.
|
||||||
|
"""
|
||||||
|
main()
|
||||||
|
'''
|
338
readgssi/translate.py
普通文件
338
readgssi/translate.py
普通文件
@@ -0,0 +1,338 @@
|
|||||||
|
import h5py
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
from readgssi.gps import readdzg
|
||||||
|
import readgssi.functions as fx
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
contains translations to common formats
|
||||||
|
"""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def gprpy(ar, header, outfile_abspath, verbose=False):
|
||||||
|
"""
|
||||||
|
Save in a format `GPRPy <https://github.com/NSGeophysics/gprpy>`_ can open (numpy binary .npy and a .json formatted header file).
|
||||||
|
|
||||||
|
.. note:: GPRPy support for this feature is forthcoming (https://github.com/NSGeophysics/GPRPy/issues/3#issuecomment-460462612).
|
||||||
|
|
||||||
|
: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
|
||||||
|
"""
|
||||||
|
numpy(ar=ar, header=header, outfile_abspath=outfile_abspath, verbose=verbose)
|
||||||
|
|
||||||
|
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.')
|
||||||
|
|
||||||
|
def h5(ar, infile_basename, outfile_abspath, header, verbose=False):
|
||||||
|
"""
|
||||||
|
.. warning:: HDF5 output is not yet available.
|
||||||
|
|
||||||
|
In the future, this function will output to HDF5 format.
|
||||||
|
|
||||||
|
:param numpy.ndarray ar: Radar array
|
||||||
|
:param str infile_basename: Input file basename
|
||||||
|
: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
|
||||||
|
"""
|
||||||
|
|
||||||
|
'''
|
||||||
|
Assumptions:
|
||||||
|
- constant velocity between marks (may be possible to add a check)
|
||||||
|
- marks are made at same time on GPS and SIR
|
||||||
|
- gps and gpr are in same location when mark is made
|
||||||
|
- good quality horizontal solution
|
||||||
|
single-channel IceRadar h5 structure is
|
||||||
|
/line_x/location_n/datacapture_0/echogram_0 (/group/group/group/dataset)
|
||||||
|
each dataset has an 'attributes' item attached, formatted in 'collections.defaultdict' style:
|
||||||
|
[('PCSavetimestamp', str), ('GPS Cluster- MetaData_xml', str), ('Digitizer-MetaData_xml', str), ('GPS Cluster_UTM-MetaData_xml', str)]
|
||||||
|
'''
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('output format is IceRadar HDF5. writing file to: %s' % outfile_abspath)
|
||||||
|
|
||||||
|
# setup formattable strings
|
||||||
|
svts = 'PCSavetimestamp'
|
||||||
|
gpsx = 'GPS Cluster- MetaData_xml'
|
||||||
|
# main gps string. 8 formattable values: gps_sec, lat, lon, qual, num_sats, hdop, altitude, geoid_ht
|
||||||
|
gpsclstr = '<Cluster>\r\n<Name>GPS Cluster</Name>\r\n<NumElts>10</NumElts>\r\n<String>\r\n<Name>GPS_timestamp_UTC</Name>\r\n<Val>%.2f</Val>\r\n</String>\r\n<String>\r\n<Name>Lat_N</Name>\r\n<Val>%.4f</Val>\r\n</String>\r\n<String>\r\n<Name>Long_ W</Name>\r\n<Val>%.4f</Val>\r\n</String>\r\n<String>\r\n<Name>Fix_Quality</Name>\r\n<Val>%i</Val>\r\n</String>\r\n<String>\r\n<Name>Num _Sat</Name>\r\n<Val>%i</Val>\r\n</String>\r\n<String>\r\n<Name>Dilution</Name>\r\n<Val>%.2f</Val>\r\n</String>\r\n<String>\r\n<Name>Alt_asl_m</Name>\r\n<Val>%.2f</Val>\r\n</String>\r\n<String>\r\n<Name>Geoid_Heigh_m</Name>\r\n<Val>%.2f</Val>\r\n</String>\r\n<Boolean>\r\n<Name>GPS Fix valid</Name>\r\n<Val>1</Val>\r\n</Boolean>\r\n<Boolean>\r\n<Name>GPS Message ok</Name>\r\n<Val>1</Val>\r\n</Boolean>\r\n</Cluster>\r\n'
|
||||||
|
dimx = 'Digitizer-MetaData_xml'
|
||||||
|
# digitizer string. 3 formattable values: rhf_depth, rh_nsamp, stack
|
||||||
|
dimxstr = '<Cluster>\r\n<Name>Digitizer MetaData</Name>\r\n<NumElts>3</NumElts>\r\n<Cluster>\r\n<Name>Digitizer settings</Name>\r\n<NumElts>5</NumElts>\r\n<Cluster>\r\n<Name>Vertical</Name>\r\n<NumElts>3</NumElts>\r\n<DBL>\r\n<Name>vertical range</Name>\r\n<Val>%f</Val>\r\n</DBL>\r\n<DBL>\r\n<Name>Vertical Offset</Name>\r\n<Val>0.00000000000000</Val>\r\n</DBL>\r\n<I32>\r\n<Name>vertical coupling</Name>\r\n<Val>1</Val>\r\n</I32>\r\n</Cluster>\r\n<Cluster>\r\n<Name>Channel</Name>\r\n<NumElts>1</NumElts>\r\n<DBL>\r\n<Name>maximum input frequency</Name>\r\n<Val>%f</Val>\r\n</DBL>\r\n</Cluster>\r\n<Cluster>\r\n<Name>Horizontal</Name>\r\n<NumElts>2</NumElts>\r\n<DBL>\r\n<Name> Sample Rate</Name>\r\n<Val>250000000.00000000000000</Val>\r\n</DBL>\r\n<I32>\r\n<Name>Record Length</Name>\r\n<Val>%i</Val>\r\n</I32>\r\n</Cluster>\r\n<Cluster>\r\n<Name>Trigger</Name>\r\n<NumElts>12</NumElts>\r\n<U16>\r\n<Name>trigger type</Name>\r\n<Val>0</Val>\r\n</U16>\r\n<DBL>\r\n<Name>trigger delay</Name>\r\n<Val>0.00000000000000</Val>\r\n</DBL>\r\n<DBL>\r\n<Name>reference position</Name>\r\n<Val>10.00000000000000</Val>\r\n</DBL>\r\n<DBL>\r\n<Name>trigger level</Name>\r\n<Val>2.00000000000000E-2</Val>\r\n</DBL>\r\n<DBL>\r\n<Name>hysteresis</Name>\r\n<Val>0.00000000000000</Val>\r\n</DBL>\r\n<DBL>\r\n<Name>low level</Name>\r\n<Val>0.00000000000000</Val>\r\n</DBL>\r\n<DBL>\r\n<Name>high level</Name>\r\n<Val>0.00000000000000</Val>\r\n</DBL>\r\n<U16>\r\n<Name>trigger coupling</Name>\r\n<Val>1</Val>\r\n</U16>\r\n<I32>\r\n<Name>trigger window mode</Name>\r\n<Val>0</Val>\r\n</I32>\r\n<I32>\r\n<Name>trigger slope</Name>\r\n<Val>0</Val>\r\n</I32>\r\n<String>\r\n<Name>trigger source</Name>\r\n<Val>0</Val>\r\n</String>\r\n<I32>\r\n<Name>Trigger Modifier</Name>\r\n<Val>2</Val>\r\n</I32>\r\n</Cluster>\r\n<String>\r\n<Name>channel name</Name>\r\n<Val>0</Val>\r\n</String>\r\n</Cluster>\r\n<U16>\r\n<Name>Stacking</Name>\r\n<Val>%i</Val>\r\n</U16>\r\n<Cluster>\r\n<Name>Radargram extra info</Name>\r\n<NumElts>2</NumElts>\r\n<DBL>\r\n<Name>relativeInitialX</Name>\r\n<Val>-1.51999998365682E-7</Val>\r\n</DBL>\r\n<DBL>\r\n<Name>xIncrement</Name>\r\n<Val>3.99999988687227E-9</Val>\r\n</DBL>\r\n</Cluster>\r\n</Cluster>\r\n'
|
||||||
|
gutx = 'GPS Cluster_UTM-MetaData_xml'
|
||||||
|
# gps UTM string. 1 formattable value: num_sats
|
||||||
|
gpsutmstr = '<Cluster>\r\n<Name>GPS_UTM Cluster</Name>\r\n<NumElts>10</NumElts>\r\n<String>\r\n<Name>Datum</Name>\r\n<Val>NaN</Val>\r\n</String>\r\n<String>\r\n<Name>Easting_m</Name>\r\n<Val></Val>\r\n</String>\r\n<String>\r\n<Name>Northing_m</Name>\r\n<Val>NaN</Val>\r\n</String>\r\n<String>\r\n<Name>Elevation</Name>\r\n<Val>NaN</Val>\r\n</String>\r\n<String>\r\n<Name>Zone</Name>\r\n<Val>NaN</Val>\r\n</String>\r\n<String>\r\n<Name>Satellites (dup)</Name>\r\n<Val>%i</Val>\r\n</String>\r\n<Boolean>\r\n<Name>GPS Fix Valid (dup)</Name>\r\n<Val>1</Val>\r\n</Boolean>\r\n<Boolean>\r\n<Name>GPS Message ok (dup)</Name>\r\n<Val>1</Val>\r\n</Boolean>\r\n<Boolean>\r\n<Name>Flag_1</Name>\r\n<Val>0</Val>\r\n</Boolean>\r\n<Boolean>\r\n<Name>Flag_2</Name>\r\n<Val>0</Val>\r\n</Boolean>\r\n</Cluster>\r\n'
|
||||||
|
|
||||||
|
if os.path.exists(infile_basename + '.DZG'):
|
||||||
|
gps = readdzg(infile_basename + '.DZG', 'dzg', header['rhf_sps'], ar.shape[1], verbose)
|
||||||
|
else:
|
||||||
|
gps = '' # if there's no DZG file...need a way to parse another gps source if possible
|
||||||
|
|
||||||
|
# make data structure
|
||||||
|
n = 0 # line number, iteratively increased
|
||||||
|
f = h5py.File('%s.h5' % (outfile_abspath), 'w') # overwrite existing file
|
||||||
|
if verbose:
|
||||||
|
fx.printmsg('exporting to %s.h5' % outfile_abspath)
|
||||||
|
|
||||||
|
try:
|
||||||
|
li = f.create_group('line_0') # create line zero
|
||||||
|
except ValueError: # the line already exists in the file
|
||||||
|
li = f['line_0']
|
||||||
|
for sample in ar.T:
|
||||||
|
# create strings
|
||||||
|
|
||||||
|
# pcsavetimestamp
|
||||||
|
# formatting: m/d/yyyy_h:m:ss PM
|
||||||
|
svts_str = gps[n]['timestamp'].astype(datetime).strftime('%m/%d/%Y_%H:%M:%S %p')
|
||||||
|
|
||||||
|
# gpscluster
|
||||||
|
# order we need: (len(list), tracetime, y, x, q, sats, dil, z, gh, 1, 1)
|
||||||
|
# rows in gps: tracenum, lat, lon, altitude, geoid_ht, qual, num_sats, hdop, timestamp
|
||||||
|
gpsx_str = gpsclstr % (gps[n]['gps_sec'], gps[n]['lat'], gps[n]['lon'], gps[n]['qual'], gps[n]['num_sats'], gps[n]['hdop'], gps[n]['altitude'], gps[n]['geoid_ht'])
|
||||||
|
|
||||||
|
# digitizer
|
||||||
|
dimx_str = dimxstr % (r[0]['rhf_depth'], freq, r[0]['rh_nsamp'], r[0]['stack'])
|
||||||
|
|
||||||
|
# utm gpscluster
|
||||||
|
gutx_str = gpsutmstr % (gps[n]['num_sats'])
|
||||||
|
|
||||||
|
lo = li.create_group('location_' + str(n)) # create a location for each trace
|
||||||
|
dc = lo.create_group('datacapture_0')
|
||||||
|
eg = dc.create_dataset('echogram_0', (ar.shape[0],), data=sample)
|
||||||
|
eg.attrs.create(svts, svts_str) # store pcsavetimestamp attribute
|
||||||
|
eg.attrs.create(gpsx, gpsx_str) # store gpscluster attribute
|
||||||
|
eg.attrs.create(dimx, dimx_str) # store digitizer attribute
|
||||||
|
eg.attrs.create(gutx, gutx_str) # store utm gpscluster attribute
|
||||||
|
n += 1
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def writetime(d):
|
||||||
|
'''
|
||||||
|
Function to write dates to :code:`rfDateByte` binary objects in DZT headers.
|
||||||
|
An inverse of the :py:func:`readgssi.dzt.readtime` function.
|
||||||
|
|
||||||
|
DZT :code:`rfDateByte` objects are 32 bits of binary (01001010111110011010011100101111),
|
||||||
|
structured as little endian u5u6u5u5u4u7 where all numbers are base 2 unsigned int (uX)
|
||||||
|
composed of X number of bits. Four bytes is an unnecessarily high level of compression
|
||||||
|
for a single date object in a filetype that often contains tens or hundreds of megabytes
|
||||||
|
of array information anyway.
|
||||||
|
|
||||||
|
So this function reads a datetime object and outputs
|
||||||
|
(seconds/2, min, hr, day, month, year-1980).
|
||||||
|
|
||||||
|
For more information on :code:`rfDateByte`, see page 55 of
|
||||||
|
`GSSI's SIR 3000 manual <https://support.geophysical.com/gssiSupport/Products/Documents/Control%20Unit%20Manuals/GSSI%20-%20SIR-3000%20Operation%20Manual.pdf>`_.
|
||||||
|
|
||||||
|
:param datetime d: the :py:class:`datetime.datetime` to be encoded
|
||||||
|
:rtype: bytes
|
||||||
|
'''
|
||||||
|
# get binary values
|
||||||
|
sec2 = int(bin(int(d.second / 2))[2:])
|
||||||
|
mins = int(bin(d.minute)[2:])
|
||||||
|
hr = int(bin(d.hour)[2:])
|
||||||
|
day = int(bin(d.day)[2:])
|
||||||
|
mo = int(bin(d.month)[2:])
|
||||||
|
yr = int(bin(d.year - 1980)[2:])
|
||||||
|
# create binary string with proper padding
|
||||||
|
dtbits = '%07d%04d%05d%05d%06d%05d' % (yr, mo, day, hr, mins, sec2)
|
||||||
|
# create four bytes that make up rfDateByte
|
||||||
|
byt0 = int(dtbits[24:], 2)
|
||||||
|
byt1 = int(dtbits[16:24], 2)
|
||||||
|
byt2 = int(dtbits[8:16], 2)
|
||||||
|
byt3 = int(dtbits[0:8], 2)
|
||||||
|
# return a byte array
|
||||||
|
return bytes([byt0, byt1, byt2, byt3])
|
||||||
|
|
||||||
|
def dzt(ar, outfile_abspath, header, verbose=False):
|
||||||
|
"""
|
||||||
|
.. warning:: DZT output is only currently compatible with single-channel files.
|
||||||
|
|
||||||
|
This function will output a RADAN-compatible DZT file after processing.
|
||||||
|
This is useful to circumvent RADAN's distance-normalization bug
|
||||||
|
when the desired outcome is array migration.
|
||||||
|
|
||||||
|
Users can set DZT output via the command line by setting the
|
||||||
|
:code:`-f dzt` flag, or in Python by doing the following: ::
|
||||||
|
|
||||||
|
from readgssi.dzt import readdzt
|
||||||
|
from readgssi import translate
|
||||||
|
from readgssi.arrayops import stack, distance_normalize
|
||||||
|
|
||||||
|
# first, read a data file
|
||||||
|
header, data, gps = readdzt('FILE__001.DZT')
|
||||||
|
|
||||||
|
# do some stuff
|
||||||
|
# (distance normalization must be done before stacking)
|
||||||
|
for a in data:
|
||||||
|
header, data[a], gps = distance_normalize(header=header, ar=data[a], gps=gps)
|
||||||
|
header, data[a], stack = stack(header=header, ar=data[a], stack=10)
|
||||||
|
|
||||||
|
# output as modified DZT
|
||||||
|
translate.dzt(ar=data, outfile_abspath='FILE__001-DnS10.DZT', header=header)
|
||||||
|
|
||||||
|
This will output :code:`FILE__001-DnS10.DZT` as a distance-normalized DZT.
|
||||||
|
|
||||||
|
:param numpy.ndarray ar: Radar array
|
||||||
|
:param str infile_basename: Input file basename
|
||||||
|
: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
|
||||||
|
"""
|
||||||
|
|
||||||
|
'''
|
||||||
|
Assumptions:
|
||||||
|
- constant velocity or distance between marks (may be possible to add a check)
|
||||||
|
'''
|
||||||
|
if len(ar) > 1:
|
||||||
|
outfile_abspath = outfile_abspath.replace('c1', '')
|
||||||
|
if not outfile_abspath.endswith(('.DZT', '.dzt')):
|
||||||
|
outfile_abspath = outfile_abspath + '.DZT'
|
||||||
|
|
||||||
|
outfile = open(outfile_abspath, 'wb')
|
||||||
|
fx.printmsg('writing to: %s' % outfile.name)
|
||||||
|
|
||||||
|
for i in range(header['rh_nchan']):
|
||||||
|
fx.printmsg('writing DZT header for channel %s' % (i))
|
||||||
|
# header should read all values per-channel no matter what
|
||||||
|
outfile.write(struct.pack('<h', header['rh_tag']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_data']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_nsamp']))
|
||||||
|
outfile.write(struct.pack('<h', 32)) # rhf_bits - for simplicity, just hard-coding 32 bit
|
||||||
|
outfile.write(struct.pack('<h', header['rh_zero']))
|
||||||
|
# byte 10
|
||||||
|
outfile.write(struct.pack('<f', header['rhf_sps']))
|
||||||
|
outfile.write(struct.pack('<f', header['rhf_spm'])) # dzt.py ln 94-97
|
||||||
|
outfile.write(struct.pack('<f', header['rhf_mpm']))
|
||||||
|
outfile.write(struct.pack('<f', header['rhf_position']))
|
||||||
|
outfile.write(struct.pack('<f', header['rhf_range']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_npass']))
|
||||||
|
# byte 32
|
||||||
|
outfile.write(writetime(header['rhb_cdt']))
|
||||||
|
outfile.write(writetime(datetime.now())) # modification date/time
|
||||||
|
# byte 40
|
||||||
|
outfile.write(struct.pack('<h', header['rh_rgain']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_nrgain']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_text']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_ntext']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_proc']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_nproc']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_nchan']))
|
||||||
|
outfile.write(struct.pack('<f', header['rhf_epsr'])) # dzt.py ln 121-126
|
||||||
|
outfile.write(struct.pack('<f', header['rhf_top']))
|
||||||
|
outfile.write(struct.pack('<f', header['rhf_depth']))
|
||||||
|
# byte 66
|
||||||
|
outfile.write(struct.pack('<f', header['rh_xstart'])) # part of rh_coordx
|
||||||
|
outfile.write(struct.pack('<f', header['rh_xend'])) # part of rh_coordx
|
||||||
|
outfile.write(struct.pack('<f', header['rhf_servo_level']))
|
||||||
|
outfile.write(bytes(3)) # "reserved"
|
||||||
|
outfile.write(struct.pack('B', header['rh_accomp']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_sconfig']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_spp']))
|
||||||
|
outfile.write(struct.pack('<h', header['rh_linenum']))
|
||||||
|
# byte 88
|
||||||
|
outfile.write(struct.pack('<f', header['rh_ystart'])) # part of rh_coordy
|
||||||
|
outfile.write(struct.pack('<f', header['rh_yend'])) # part of rh_coordy
|
||||||
|
outfile.write(header['rh_96'])
|
||||||
|
outfile.write(struct.pack('c', header['rh_dtype']))
|
||||||
|
outfile.write(header['dzt_ant'][i])
|
||||||
|
outfile.write(header['rh_112'])
|
||||||
|
# byte 113
|
||||||
|
outfile.write(header['vsbyte'])
|
||||||
|
outfile.write(header['rh_name'])
|
||||||
|
outfile.write(header['rh_chksum'])
|
||||||
|
# byte 128
|
||||||
|
outfile.write(header['INFOAREA'])
|
||||||
|
outfile.write(header['rh_RGPS0'])
|
||||||
|
outfile.write(header['rh_RGPS1'])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
outfile.write(header['header_extra'])
|
||||||
|
|
||||||
|
stack = []
|
||||||
|
i = 0
|
||||||
|
for i in range(header['rh_nchan']):
|
||||||
|
# replace zeroed rows
|
||||||
|
stack.append(np.zeros((header['timezero'][i], ar[i].shape[1]),
|
||||||
|
dtype=np.int32))
|
||||||
|
stack.append(ar[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
writestack = np.vstack(tuple(stack))
|
||||||
|
sh = writestack.shape
|
||||||
|
writestack = writestack.T.reshape(-1)
|
||||||
|
fx.printmsg('writing %s data samples for %s channels (%s x %s)'
|
||||||
|
% (writestack.shape[0],
|
||||||
|
int(len(stack)/2),
|
||||||
|
sh[0], sh[1]))
|
||||||
|
|
||||||
|
# hard coded to write 32 bit signed ints to keep lossiness to a minimum
|
||||||
|
outfile.write(writestack.round().astype(np.int32, casting='unsafe').tobytes(order='C'))
|
||||||
|
|
||||||
|
outfile.close()
|
||||||
|
|
在新工单中引用
屏蔽一个用户