shipgrav 1.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shipgrav/__init__.py +147 -0
- shipgrav/database.toml +24 -0
- shipgrav/grav.py +1260 -0
- shipgrav/io.py +749 -0
- shipgrav/nav.py +91 -0
- shipgrav/tests/__init__.py +16 -0
- shipgrav/tests/__main__.py +3 -0
- shipgrav/tests/ex_files/AT05_01_bgm.RGS +4 -0
- shipgrav/tests/ex_files/DGStest_laptop.dat +1001 -0
- shipgrav/tests/ex_files/IXBlue.yaml +160 -0
- shipgrav/tests/ex_files/SR2312_dgs_raw.txt +2 -0
- shipgrav/tests/ex_files/SR2312_mru.txt +12 -0
- shipgrav/tests/ex_files/TN400_bgm.Raw +2 -0
- shipgrav/tests/ex_files/TN400_nav.Raw +2 -0
- shipgrav/tests/test_grav_data.py +75 -0
- shipgrav/tests/test_grav_nodata.py +47 -0
- shipgrav/tests/test_io.py +55 -0
- shipgrav/tests/test_nav.py +41 -0
- shipgrav/tests/test_utils.py +33 -0
- shipgrav/utils.py +275 -0
- shipgrav-1.0.0.dist-info/METADATA +44 -0
- shipgrav-1.0.0.dist-info/RECORD +24 -0
- shipgrav-1.0.0.dist-info/WHEEL +5 -0
- shipgrav-1.0.0.dist-info/licenses/LICENSE +675 -0
shipgrav/utils.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pandas import Series
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
# TODO gaussian filter doesn't *need* time array
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def gaussian_filter(x, fl):
|
|
10
|
+
""" Apply a gaussian filter to a vector.
|
|
11
|
+
|
|
12
|
+
The filtering is done via a ring buffer; results are not identical to
|
|
13
|
+
scipy.ndimage.gaussian_filter, which is why this function exists.
|
|
14
|
+
Note that the filter is not applied symmetrically and there is a shift by fl.
|
|
15
|
+
|
|
16
|
+
:param x: time series data to be filtered.
|
|
17
|
+
:type x: array_like
|
|
18
|
+
:param fl: length of the filter in # of samples.
|
|
19
|
+
:type fl: int
|
|
20
|
+
|
|
21
|
+
:return: **xfilt** (*ndarray*) - filtered time series x
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
assert len(x) > fl, 'raw data is shorter than gaussian filter'
|
|
25
|
+
|
|
26
|
+
coeffs = _gaussian_coeffs(fl)
|
|
27
|
+
|
|
28
|
+
# set up ring buffer
|
|
29
|
+
filt_len = len(coeffs)
|
|
30
|
+
ring_buffer = np.zeros(filt_len)
|
|
31
|
+
|
|
32
|
+
xfilt = np.zeros(len(x))
|
|
33
|
+
|
|
34
|
+
for i in range(len(x)):
|
|
35
|
+
ring_buffer = np.append(ring_buffer[1:], x[i])
|
|
36
|
+
filt_vec = ring_buffer*coeffs
|
|
37
|
+
|
|
38
|
+
xfilt[i] = sum(filt_vec)
|
|
39
|
+
|
|
40
|
+
return xfilt
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _gaussian_coeffs(fl):
|
|
44
|
+
""" Calculate a gaussian filter to convolve with things that need filtering.
|
|
45
|
+
|
|
46
|
+
:param fl: length of the filter in # of samples
|
|
47
|
+
:type fl: int
|
|
48
|
+
|
|
49
|
+
:return: **gauss_prms** (*ndarray*) - filter coefficients
|
|
50
|
+
"""
|
|
51
|
+
half_sample = np.floor(.5*fl)
|
|
52
|
+
coeff_len = int(half_sample + 1)
|
|
53
|
+
|
|
54
|
+
frac = 6./fl # same as NAVO and LDEO versions, and I think this is the secret sauce?
|
|
55
|
+
|
|
56
|
+
gauss_prms = np.zeros(coeff_len)
|
|
57
|
+
m = 0
|
|
58
|
+
for i in range(coeff_len, 0, -1):
|
|
59
|
+
x = (half_sample - i)*frac
|
|
60
|
+
x = -.5*x**2
|
|
61
|
+
gauss_prms[m] = np.exp(x)
|
|
62
|
+
m += 1
|
|
63
|
+
|
|
64
|
+
# reflect
|
|
65
|
+
gauss_prms = np.hstack(
|
|
66
|
+
(gauss_prms[1:][::-1], gauss_prms[0], gauss_prms[1:]))
|
|
67
|
+
|
|
68
|
+
# normalize
|
|
69
|
+
gauss_prms = gauss_prms/sum(gauss_prms)
|
|
70
|
+
|
|
71
|
+
return gauss_prms
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def decode_dgs_status_bits(stat, key='status'):
|
|
75
|
+
""" Decode status bits from integers in DGS gravimeter files.
|
|
76
|
+
|
|
77
|
+
Return flags as a dict (for one integer input) or a dataframe (status df input)
|
|
78
|
+
|
|
79
|
+
:param stat: status bit(s)
|
|
80
|
+
:type stat: int or pd.DataFrame
|
|
81
|
+
:param key: the DataFrame column for the status bits if input is a DataFrame
|
|
82
|
+
:type key: string, optional
|
|
83
|
+
|
|
84
|
+
:return: **stat** (*dict or pd.DataFrame*) - decoded bits
|
|
85
|
+
"""
|
|
86
|
+
# flags from Jasmine's code
|
|
87
|
+
# flags = ['clamp','unclamp','GPSsync','feedback','R1','R2','ADlock','Rcvd',\
|
|
88
|
+
# 'NavMod1','NavMod2','Free','SensCom','GPStime','ADsat','Datavalid','PlatCom']
|
|
89
|
+
# flags from DGS documentation?
|
|
90
|
+
flags = ['clamp status', 'GPSsync', 'reserved', 'feedback', 'R1', 'R2', 'ADlock', 'ack host',
|
|
91
|
+
'NavMod1', 'NavMod2', 'dgs1_trouble', 'dgs2_trouble', 'GPStime', 'ADsat', 'nemo1', 'nemo2']
|
|
92
|
+
|
|
93
|
+
assert type(stat) == int or type(
|
|
94
|
+
stat) == DataFrame, 'bad input type for status bits'
|
|
95
|
+
if type(stat) == int:
|
|
96
|
+
bt = format(stat, '016b')
|
|
97
|
+
out = {}
|
|
98
|
+
for i, f in enumerate(flags):
|
|
99
|
+
out[f] = bt[i]
|
|
100
|
+
return out
|
|
101
|
+
else: # dataframe, add columns
|
|
102
|
+
bts = np.array([format(s, '016b') for s in stat[key]])
|
|
103
|
+
for i, f in enumerate(flags):
|
|
104
|
+
stat.insert(i+1, f, [b[i] for b in bts])
|
|
105
|
+
return stat
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def clean_ini_to_toml(ini_file):
|
|
109
|
+
""" Read in a .ini file and try to rewrite as toml-compliant.
|
|
110
|
+
|
|
111
|
+
This uses simple, prescriptive regex stuff to (hopefully) clean out
|
|
112
|
+
ini conventions that don't work with toml.
|
|
113
|
+
It writes out a toml file with the same name as the input, with
|
|
114
|
+
the extension .ini replaced by .toml
|
|
115
|
+
|
|
116
|
+
:param ini_file: path to input ini file
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
with open(ini_file, 'r') as file:
|
|
120
|
+
text = file.read() # just read the whole thing
|
|
121
|
+
|
|
122
|
+
# fix comments (// to #)
|
|
123
|
+
text = re.sub('//', '#', text)
|
|
124
|
+
|
|
125
|
+
# clean out tabs
|
|
126
|
+
text = re.sub('\t', ' ', text)
|
|
127
|
+
|
|
128
|
+
# fix TRUE and FALSE
|
|
129
|
+
text = re.sub('TRUE', 'true', text)
|
|
130
|
+
text = re.sub('FALSE', 'false', text)
|
|
131
|
+
|
|
132
|
+
text = re.sub('$', '', text)
|
|
133
|
+
|
|
134
|
+
# fix unquoted strings and write
|
|
135
|
+
fo = open(ini_file.rstrip('ini')+'toml', 'w')
|
|
136
|
+
lines = text.split('\n')
|
|
137
|
+
for ln in lines:
|
|
138
|
+
if ln.startswith('$'):
|
|
139
|
+
ln = re.sub('\$', '', ln) # clean out $ for talkers
|
|
140
|
+
|
|
141
|
+
if ln.startswith('[') or ln.startswith('#') or ln == '':
|
|
142
|
+
fo.write(ln)
|
|
143
|
+
fo.write('\n')
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
val = ln.split('=')[1] # get value from key=value pair
|
|
147
|
+
extra_comm = ''
|
|
148
|
+
if '#' in val:
|
|
149
|
+
val = val.split('#')[0]
|
|
150
|
+
extra_comm = '='.join(val.split('#')[1:])
|
|
151
|
+
val = val.strip()
|
|
152
|
+
|
|
153
|
+
if val == 'true' or val == 'false': # skip bools
|
|
154
|
+
fo.write(ln)
|
|
155
|
+
fo.write('\n')
|
|
156
|
+
continue
|
|
157
|
+
if val.startswith("\""): # skip strings already quoted
|
|
158
|
+
fo.write(ln)
|
|
159
|
+
fo.write('\n')
|
|
160
|
+
continue
|
|
161
|
+
try:
|
|
162
|
+
float(val)
|
|
163
|
+
except:
|
|
164
|
+
val = "\"" + val + "\""
|
|
165
|
+
ln = '= '.join((ln.split('=')[0], val))
|
|
166
|
+
if extra_comm != '':
|
|
167
|
+
ln = ' #'.join((ln, extra_comm))
|
|
168
|
+
fo.write(ln)
|
|
169
|
+
fo.write('\n')
|
|
170
|
+
|
|
171
|
+
fo.close()
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _SnappingCursor:
|
|
176
|
+
"""A cross-hair cursor that snaps to the closest *x* point on a line
|
|
177
|
+
|
|
178
|
+
This is for a two-axis figure where the first axis (ax1)
|
|
179
|
+
has a cross-hair cursor, and the second axis (ax2) has a dot that's linked
|
|
180
|
+
to the cursor in ax1. Basically, we can plot two different values for one
|
|
181
|
+
time series, and use a cursor over one of them to move a dot around on the other one.
|
|
182
|
+
|
|
183
|
+
The time series with the cross hair cursor must have x values that are unique and sorted.
|
|
184
|
+
|
|
185
|
+
Since this class is designed for a specific use case, there are some hard-coded assumptions
|
|
186
|
+
about the names of columns in the DataFrame being used for plotting.
|
|
187
|
+
|
|
188
|
+
.. The specific use case here is a plot of FAA vs time (so, x unique and sorted) with
|
|
189
|
+
the cursor, and a dot moving around on a map view of a ship track (x not necessarily
|
|
190
|
+
unique or sorted).
|
|
191
|
+
|
|
192
|
+
.. The two are linked via the pandas DataFrame "data", which has columns *tsec*,
|
|
193
|
+
*lon_new*, and *lat_new*
|
|
194
|
+
|
|
195
|
+
The cursor can also register mouse clicks so the user can interactively select points
|
|
196
|
+
that are accessible later.
|
|
197
|
+
|
|
198
|
+
For usage, see the example script interactive_line_pick.py included with the
|
|
199
|
+
shipgrav package
|
|
200
|
+
|
|
201
|
+
Heavily adapted from the matplotlib cross-hair cursor demo at
|
|
202
|
+
https://matplotlib.org/stable/gallery/event_handling/cursor_demo.html
|
|
203
|
+
|
|
204
|
+
:param ax1: first axis, for cursor; x axis unique and sorted (tsec in data)
|
|
205
|
+
:param ax2: second axis, for points; lon_new vs lat_new in data
|
|
206
|
+
:param line: the line plotted in ax1, of type matplotlib.lines.Line2D
|
|
207
|
+
:param dot: single point plotted in ax2 that will move around, also Line2D
|
|
208
|
+
:param data: pd.DataFrame with tsec, lon_new, and lat_new columns that correspond
|
|
209
|
+
to what's plotted in both axes
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(self, ax1, ax2, line, dot, data):
|
|
213
|
+
self.ax1 = ax1
|
|
214
|
+
self.dot = dot
|
|
215
|
+
self.data = data
|
|
216
|
+
minx = ax1.get_xlim()[0]
|
|
217
|
+
maxy = ax1.get_ylim()[1]
|
|
218
|
+
self.horizontal_line = ax1.axhline(y=maxy, color='k', lw=0.8, ls='--')
|
|
219
|
+
self.vertical_line = ax1.axvline(x=minx, color='k', lw=0.8, ls='--')
|
|
220
|
+
self.x, self.y = line.get_data()
|
|
221
|
+
self._last_index = None
|
|
222
|
+
|
|
223
|
+
self.x_seg = []
|
|
224
|
+
self.y_seg = []
|
|
225
|
+
self.i_seg = []
|
|
226
|
+
self.scatters = ax2.plot(
|
|
227
|
+
self.x_seg, self.y_seg, marker='o', color='r', linestyle=None)
|
|
228
|
+
|
|
229
|
+
def set_cross_hair_visible(self, visible):
|
|
230
|
+
need_redraw = self.horizontal_line.get_visible() != visible
|
|
231
|
+
self.horizontal_line.set_visible(visible)
|
|
232
|
+
self.vertical_line.set_visible(visible)
|
|
233
|
+
return need_redraw
|
|
234
|
+
|
|
235
|
+
def on_mouse_move(self, event):
|
|
236
|
+
if not event.inaxes == self.ax1:
|
|
237
|
+
self._last_index = None
|
|
238
|
+
need_redraw = self.set_cross_hair_visible(False)
|
|
239
|
+
if need_redraw:
|
|
240
|
+
self.ax1.figure.canvas.draw()
|
|
241
|
+
else:
|
|
242
|
+
self.set_cross_hair_visible(True)
|
|
243
|
+
x, y = event.xdata, event.ydata
|
|
244
|
+
index = min(np.searchsorted(self.x, x), len(self.x) - 1)
|
|
245
|
+
if index == self._last_index:
|
|
246
|
+
return # still on the same data point. Nothing to do.
|
|
247
|
+
self._last_index = index
|
|
248
|
+
x = self.x[index]
|
|
249
|
+
y = self.y[index]
|
|
250
|
+
# update the line positions for the cursor
|
|
251
|
+
self.horizontal_line.set_ydata([y])
|
|
252
|
+
self.vertical_line.set_xdata([x])
|
|
253
|
+
|
|
254
|
+
# and also update ax2 dot
|
|
255
|
+
ind_x = np.argmin(abs(x - self.data['tsec']))
|
|
256
|
+
self.dot.update({'xdata': [self.data.iloc[ind_x]['lon_new'],], 'ydata': [
|
|
257
|
+
self.data.iloc[ind_x]['lat_new'],]})
|
|
258
|
+
self.ax1.figure.canvas.draw()
|
|
259
|
+
|
|
260
|
+
def on_mouse_click(self, event):
|
|
261
|
+
if event.button == 1:
|
|
262
|
+
x, y = event.xdata, event.ydata
|
|
263
|
+
ind_x = np.argmin(abs(x - self.data['tsec']))
|
|
264
|
+
self.x_seg.append(self.data.iloc[ind_x]['lon_new'])
|
|
265
|
+
self.y_seg.append(self.data.iloc[ind_x]['lat_new'])
|
|
266
|
+
self.i_seg.append(ind_x)
|
|
267
|
+
self.scatters[0].update({'xdata': self.x_seg, 'ydata': self.y_seg})
|
|
268
|
+
|
|
269
|
+
if event.button == 3: # pop the last element of the lists
|
|
270
|
+
self.x_seg.pop()
|
|
271
|
+
self.y_seg.pop()
|
|
272
|
+
self.i_seg.pop()
|
|
273
|
+
self.scatters[0].update({'xdata': self.x_seg, 'ydata': self.y_seg})
|
|
274
|
+
|
|
275
|
+
self.ax1.figure.canvas.draw()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: shipgrav
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Functions for marine gravity data processing and reduction
|
|
5
|
+
Author-email: "Hannah F. Mark" <hmark@whoi.edu>
|
|
6
|
+
Maintainer-email: "Hannah F. Mark" <hmark@whoi.edu>
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: UNOLS,gravimetry,marine gravity
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Dist: matplotlib
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: pandas
|
|
15
|
+
Requires-Dist: pyyaml
|
|
16
|
+
Requires-Dist: scipy
|
|
17
|
+
Requires-Dist: statsmodels
|
|
18
|
+
Requires-Dist: tomli
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# shipgrav
|
|
22
|
+
[](https://github.com/PFPE/shipgrav/actions)
|
|
23
|
+
|
|
24
|
+
shipgrav is a Python package designed for reading, processing, and reducing marine gravity data from UNOLS ships. It is created and maintained by PFPE for the marine gravimetry community. The shipgrav repository also contains scripts with example workflows for gravity data processing and reduction.
|
|
25
|
+
|
|
26
|
+
## Dependencies
|
|
27
|
+
python 3.9+\
|
|
28
|
+
numpy\
|
|
29
|
+
scipy\
|
|
30
|
+
pandas 2.0+\
|
|
31
|
+
statsmodels\
|
|
32
|
+
tomli\
|
|
33
|
+
pyyaml
|
|
34
|
+
|
|
35
|
+
To run the example scripts, you will also need matplotlib.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
shipgrav can be installed using `pip`. Detailed instructions are in the [documentation](https://shipgrav.readthedocs.io/).
|
|
39
|
+
|
|
40
|
+
## Documentation and usage
|
|
41
|
+
The shipgrav documentation is available online at [shipgrav.readthedocs.io](https://shipgrav.readthedocs.io/). It can also be accessed offline by building the contents of the `docs` folder using sphinx.
|
|
42
|
+
|
|
43
|
+
## Contributing to shipgrav
|
|
44
|
+
Please do! If you have ideas for how to make shipgrav better, you can raise an issue on github or contact PFPE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
shipgrav/__init__.py,sha256=oLzfXDKCuKsg3qHtE7Pg_8sxUkavmlfn1VkN5J36nWQ,11043
|
|
2
|
+
shipgrav/database.toml,sha256=qRSZBRsoMsbyhjkaYlXsWtVDz3JAhoW-cYfSoo8U3K4,532
|
|
3
|
+
shipgrav/grav.py,sha256=OQK2HNiZtLlycjVwidpD902K08NaWbZXnjZSbjyPF8I,43410
|
|
4
|
+
shipgrav/io.py,sha256=j9uy5z-CqhkUN4T-3iW5o5-cRLgjoGwfNc4o7cjw_pA,29273
|
|
5
|
+
shipgrav/nav.py,sha256=3E2Il-PhTEPVv8_8PrLVe7h6slHq-2HYX_Q8PdpB6X4,2895
|
|
6
|
+
shipgrav/utils.py,sha256=h5gd8cnWlO1pyAqLkJIVrbFRRLbZzHj-xJ9pWchaHtI,9521
|
|
7
|
+
shipgrav/tests/__init__.py,sha256=mejp00TQif0WLU45j40zfBQXN2RujaOP6ZSTgxUm4A8,367
|
|
8
|
+
shipgrav/tests/__main__.py,sha256=iJ8xSU8ucCmIDcOuv8uasZ9iV5dNR0DDuFRZrG0o2hE,38
|
|
9
|
+
shipgrav/tests/test_grav_data.py,sha256=OnPNW8XIG3DvWOr_trQY71XLzA0rff8VfGd4Az3B37Q,3403
|
|
10
|
+
shipgrav/tests/test_grav_nodata.py,sha256=0z5KUTzT9cJPko_bhyDG4EFm4i5xgOdVhJVKU2cPxX8,1493
|
|
11
|
+
shipgrav/tests/test_io.py,sha256=RBRB2GYtesuSv68lJl5yMOLH2DGFz2AAKADyPKq8Dhk,2259
|
|
12
|
+
shipgrav/tests/test_nav.py,sha256=6Ndn1mHN8kGruwk3R5yJFiK0n9Wt3XcK0GQ9PGbRIP8,1194
|
|
13
|
+
shipgrav/tests/test_utils.py,sha256=10brOKgB5ij-oXqQnfo3CkMmZxSQMxSr_KVLLN1g1u0,969
|
|
14
|
+
shipgrav/tests/ex_files/AT05_01_bgm.RGS,sha256=3MnlHJiIyzYe7KGr-bD8dfQ1nS_-Dt4A4g6TiVuNaBw,515
|
|
15
|
+
shipgrav/tests/ex_files/DGStest_laptop.dat,sha256=kUwAMltUXm32Dv9BnaDakA-KtNAuMxgSC504F8_0aws,353351
|
|
16
|
+
shipgrav/tests/ex_files/IXBlue.yaml,sha256=W-GGSMrSSvIXzX-b4rIU0u6Fv8bgNccZzNjTy-K1dNE,5394
|
|
17
|
+
shipgrav/tests/ex_files/SR2312_dgs_raw.txt,sha256=rTlIn75MBICBjnkjcn9SPCwpC5J3lMs4aq7-y6-dAmU,368
|
|
18
|
+
shipgrav/tests/ex_files/SR2312_mru.txt,sha256=YBM4qy4BS_D0U4ngEyoIFZr4QQbMWQgEO681fVEmgYM,1060
|
|
19
|
+
shipgrav/tests/ex_files/TN400_bgm.Raw,sha256=DL7T2aJednjGLMKJo6K8KcUYLMjQCiv6bsUiczlVwKw,74
|
|
20
|
+
shipgrav/tests/ex_files/TN400_nav.Raw,sha256=66dgRQTXjf0JItMWE1ZGUO6NptrGqV6JyN2wdYe5bW8,194
|
|
21
|
+
shipgrav-1.0.0.dist-info/METADATA,sha256=Ou01ttXgo18YQDfv2EngjSm-paE81HEqzkZN2Fd7H8A,1745
|
|
22
|
+
shipgrav-1.0.0.dist-info/WHEEL,sha256=fl6v0VwpzfGBVsGtkAkhILUlJxROXbA3HvRL6Fe3140,105
|
|
23
|
+
shipgrav-1.0.0.dist-info/licenses/LICENSE,sha256=5X8cMguM-HmKfS_4Om-eBqM6A1hfbgZf6pfx2G24QFI,35150
|
|
24
|
+
shipgrav-1.0.0.dist-info/RECORD,,
|