pyadps 0.2.1b0__py3-none-any.whl → 0.3.0b0__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.
- pyadps/pages/01_Read_File.py +92 -17
- pyadps/pages/02_View_Raw_Data.py +69 -33
- pyadps/pages/04_Sensor_Health.py +892 -0
- pyadps/pages/05_QC_Test.py +478 -0
- pyadps/pages/06_Profile_Test.py +959 -0
- pyadps/pages/07_Velocity_Test.py +599 -0
- pyadps/pages/{07_Write_File.py → 08_Write_File.py} +127 -52
- pyadps/pages/09_Auto_process.py +62 -0
- pyadps/utils/__init__.py +2 -3
- pyadps/utils/autoprocess.py +129 -46
- pyadps/utils/metadata/config.ini +22 -4
- pyadps/utils/metadata/demo.000 +0 -0
- pyadps/utils/plotgen.py +499 -0
- pyadps/utils/profile_test.py +491 -126
- pyadps/utils/pyreadrdi.py +13 -6
- pyadps/utils/readrdi.py +78 -6
- pyadps/utils/script.py +21 -23
- pyadps/utils/sensor_health.py +120 -0
- pyadps/utils/signal_quality.py +343 -23
- pyadps/utils/velocity_test.py +75 -27
- pyadps/utils/writenc.py +8 -1
- {pyadps-0.2.1b0.dist-info → pyadps-0.3.0b0.dist-info}/METADATA +3 -3
- pyadps-0.3.0b0.dist-info/RECORD +33 -0
- {pyadps-0.2.1b0.dist-info → pyadps-0.3.0b0.dist-info}/WHEEL +1 -1
- pyadps/pages/04_QC_Test.py +0 -334
- pyadps/pages/05_Profile_Test.py +0 -575
- pyadps/pages/06_Velocity_Test.py +0 -341
- pyadps/utils/cutbin.py +0 -413
- pyadps/utils/regrid.py +0 -279
- pyadps-0.2.1b0.dist-info/RECORD +0 -31
- {pyadps-0.2.1b0.dist-info → pyadps-0.3.0b0.dist-info}/LICENSE +0 -0
- {pyadps-0.2.1b0.dist-info → pyadps-0.3.0b0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,892 @@
|
|
1
|
+
import numpy as np
|
2
|
+
import pandas as pd
|
3
|
+
import tempfile
|
4
|
+
import os
|
5
|
+
import plotly.graph_objects as go
|
6
|
+
import streamlit as st
|
7
|
+
from plotly_resampler import FigureResampler
|
8
|
+
from pyadps.utils import sensor_health
|
9
|
+
from utils.sensor_health import sound_speed_correction, tilt_sensor_check
|
10
|
+
|
11
|
+
if "flead" not in st.session_state:
|
12
|
+
st.write(":red[Please Select Data!]")
|
13
|
+
st.stop()
|
14
|
+
|
15
|
+
ds = st.session_state.ds
|
16
|
+
|
17
|
+
|
18
|
+
# ----------------- Functions ---------------
|
19
|
+
|
20
|
+
|
21
|
+
# File Access Function
|
22
|
+
@st.cache_data()
|
23
|
+
def file_access(uploaded_file):
|
24
|
+
"""
|
25
|
+
Function creates temporary directory to store the uploaded file.
|
26
|
+
The path of the file is returned
|
27
|
+
|
28
|
+
Args:
|
29
|
+
uploaded_file (string): Name of the uploaded file
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
path (string): Path of the uploaded file
|
33
|
+
"""
|
34
|
+
temp_dir = tempfile.mkdtemp()
|
35
|
+
path = os.path.join(temp_dir, uploaded_file.name)
|
36
|
+
with open(path, "wb") as f:
|
37
|
+
f.write(uploaded_file.getvalue())
|
38
|
+
return path
|
39
|
+
|
40
|
+
|
41
|
+
def status_color_map(value):
|
42
|
+
# Define a mapping function for styling
|
43
|
+
if value == "True":
|
44
|
+
return "background-color: green; color: white"
|
45
|
+
elif value == "False":
|
46
|
+
return "background-color: red; color: white"
|
47
|
+
|
48
|
+
|
49
|
+
# -------------- Widget Functions -------------
|
50
|
+
|
51
|
+
|
52
|
+
# Depth Tab
|
53
|
+
def set_button_upload_depth():
|
54
|
+
if st.session_state.uploaded_file_depth is not None:
|
55
|
+
st.session_state.pspath = file_access(st.session_state.uploaded_file_depth)
|
56
|
+
df_depth = pd.read_csv(st.session_state.pspath, header=None)
|
57
|
+
numpy_depth = df_depth.to_numpy()
|
58
|
+
st.session_state.df_numpy_depth = np.squeeze(numpy_depth)
|
59
|
+
if len(st.session_state.df_numpy_depth) != st.session_state.head.ensembles:
|
60
|
+
st.session_state.isDepthModified = False
|
61
|
+
else:
|
62
|
+
st.session_state.depth = st.session_state.df_numpy_depth
|
63
|
+
st.session_state.isDepthModified = True
|
64
|
+
|
65
|
+
|
66
|
+
def set_button_depth():
|
67
|
+
# st.session_state.depth = st.session_state.depth * 0 + int(
|
68
|
+
# st.session_state.sensor_depthinput * 10
|
69
|
+
# )
|
70
|
+
st.session_state.depth = np.full(
|
71
|
+
st.session_state.head.ensembles, st.session_state.sensor_depthinput
|
72
|
+
)
|
73
|
+
st.session_state.depth *= 10
|
74
|
+
st.session_state.isDepthModified = True
|
75
|
+
|
76
|
+
|
77
|
+
def reset_button_depth():
|
78
|
+
st.session_state.depth = st.session_state.vlead.depth_of_transducer.data
|
79
|
+
st.session_state.isDepthModified = False
|
80
|
+
|
81
|
+
|
82
|
+
# Salinity Tab
|
83
|
+
def set_button_upload_salinity():
|
84
|
+
if st.session_state.uploaded_file_salinity is not None:
|
85
|
+
st.session_state.pspath = file_access(st.session_state.uploaded_file_salinity)
|
86
|
+
df_salinity = pd.read_csv(st.session_state.pspath, header=None)
|
87
|
+
numpy_salinity = df_salinity.to_numpy()
|
88
|
+
st.session_state.df_numpy_salinity = np.squeeze(numpy_salinity)
|
89
|
+
if len(st.session_state.df_numpy_salinity) != st.session_state.head.ensembles:
|
90
|
+
st.session_state.isSalinityModified = False
|
91
|
+
else:
|
92
|
+
st.session_state.salinity = st.session_state.df_numpy_salinity
|
93
|
+
st.session_state.isSalinityModified = True
|
94
|
+
|
95
|
+
|
96
|
+
def set_button_salinity():
|
97
|
+
st.session_state.salinity = np.full(
|
98
|
+
st.session_state.head.ensembles, st.session_state.sensor_salinityinput
|
99
|
+
)
|
100
|
+
st.session_state.isSalinityModified = True
|
101
|
+
|
102
|
+
|
103
|
+
def reset_button_salinity():
|
104
|
+
st.session_state.salinity = st.session_state.vlead.salinity.data
|
105
|
+
st.session_state.isSalinityModified = False
|
106
|
+
|
107
|
+
|
108
|
+
# Temperature Tab
|
109
|
+
def set_button_upload_temperature():
|
110
|
+
if st.session_state.uploaded_file_temperature is not None:
|
111
|
+
st.session_state.pspath = file_access(
|
112
|
+
st.session_state.uploaded_file_temperature
|
113
|
+
)
|
114
|
+
df_temperature = pd.read_csv(st.session_state.pspath, header=None)
|
115
|
+
numpy_temperature = df_temperature.to_numpy()
|
116
|
+
st.session_state.df_numpy_temperature = np.squeeze(numpy_temperature)
|
117
|
+
if (
|
118
|
+
len(st.session_state.df_numpy_temperature)
|
119
|
+
!= st.session_state.head.ensembles
|
120
|
+
):
|
121
|
+
st.session_state.isTemperatureModified = False
|
122
|
+
else:
|
123
|
+
st.session_state.temperature = st.session_state.df_numpy_temperature
|
124
|
+
st.session_state.isTemperatureModified = True
|
125
|
+
|
126
|
+
|
127
|
+
def set_button_temperature():
|
128
|
+
st.session_state.temperature = np.full(
|
129
|
+
st.session_state.head.ensembles, st.session_state.sensor_tempinput
|
130
|
+
)
|
131
|
+
st.session_state.isTemperatureModified = True
|
132
|
+
|
133
|
+
|
134
|
+
def reset_button_temperature():
|
135
|
+
st.session_state.temperature = st.session_state.vlead.temperature.data
|
136
|
+
st.session_state.isTemperatureModified = False
|
137
|
+
|
138
|
+
|
139
|
+
# Corrections/Threshold Tab
|
140
|
+
def set_threshold_button():
|
141
|
+
if st.session_state.sensor_roll_checkbox:
|
142
|
+
rollmask = np.copy(st.session_state.sensor_mask_temp)
|
143
|
+
roll = ds.variableleader.roll.data
|
144
|
+
updated_rollmask = tilt_sensor_check(
|
145
|
+
roll, rollmask, cutoff=st.session_state.sensor_roll_cutoff
|
146
|
+
)
|
147
|
+
st.session_state.sensor_mask_temp = updated_rollmask
|
148
|
+
st.session_state.isRollCheck = True
|
149
|
+
|
150
|
+
if st.session_state.sensor_pitch_checkbox:
|
151
|
+
pitchmask = np.copy(st.session_state.sensor_mask_temp)
|
152
|
+
pitch = ds.variableleader.pitch.data
|
153
|
+
updated_pitchmask = tilt_sensor_check(
|
154
|
+
pitch, pitchmask, cutoff=st.session_state.sensor_pitch_cutoff
|
155
|
+
)
|
156
|
+
st.session_state.sensor_mask_temp = updated_pitchmask
|
157
|
+
st.session_state.isPitchCheck = True
|
158
|
+
|
159
|
+
if (
|
160
|
+
st.session_state.sensor_fix_velocity_checkbox
|
161
|
+
and not st.session_state.sensor_ischeckbox_disabled
|
162
|
+
):
|
163
|
+
sound = st.session_state.sound_speed
|
164
|
+
t = st.session_state.temperature
|
165
|
+
s = st.session_state.salinity
|
166
|
+
d = st.session_state.depth
|
167
|
+
velocity = sound_speed_correction(
|
168
|
+
st.session_state.velocity_sensor, sound, t, s, d
|
169
|
+
)
|
170
|
+
st.session_state.velocity_temp = velocity
|
171
|
+
st.session_state.isVelocityModifiedSound = True
|
172
|
+
|
173
|
+
|
174
|
+
# Save Tab
|
175
|
+
def reset_threshold_button():
|
176
|
+
st.session_state.isRollCheck = False
|
177
|
+
st.session_state.isPitchCheck = False
|
178
|
+
st.session_state.isVelocityModifiedSound = False
|
179
|
+
st.session_state.sensor_mask_temp = np.copy(st.session_state.orig_mask)
|
180
|
+
st.session_state.velocity_temp = np.copy(st.session_state.velocity)
|
181
|
+
|
182
|
+
|
183
|
+
def reset_sensor():
|
184
|
+
# Deactivate Global Test
|
185
|
+
st.session_state.isSensorTest = False
|
186
|
+
# Deactivate Local Tests
|
187
|
+
st.session_state.isRollCheck = False
|
188
|
+
st.session_state.isPitchCheck = False
|
189
|
+
# Deactivate Data Modification Tests
|
190
|
+
st.session_state.isDepthModified = False
|
191
|
+
st.session_state.isSalinityModified = False
|
192
|
+
st.session_state.isTemperatureModified = False
|
193
|
+
st.session_state.isVelocityModifiedSound = False
|
194
|
+
|
195
|
+
# Reset Mask Data
|
196
|
+
# `sensor_mask_temp` holds and transfers the mask changes between each section
|
197
|
+
st.session_state.sensor_mask_temp = np.copy(st.session_state.orig_mask)
|
198
|
+
# `sensor_mask` holds the final changes in the page after applying save button
|
199
|
+
st.session_state.sensor_mask = np.copy(st.session_state.orig_mask)
|
200
|
+
|
201
|
+
# Reset General Data
|
202
|
+
#
|
203
|
+
# The sensor test includes changes in ADCP data due to sound speed correction
|
204
|
+
st.session_state.depth = st.session_state.vlead.depth_of_transducer.data
|
205
|
+
st.session_state.salinity = st.session_state.vlead.salinity.data
|
206
|
+
st.session_state.temperature = st.session_state.vlead.temperature.data
|
207
|
+
# The `velocity_sensor` holds velocity data for correction
|
208
|
+
st.session_state.velocity_temp = np.copy(st.session_state.velocity)
|
209
|
+
st.session_state.velocity_sensor = np.copy(st.session_state.velocity)
|
210
|
+
|
211
|
+
|
212
|
+
def save_sensor():
|
213
|
+
st.session_state.velocity_sensor = np.copy(st.session_state.velocity_temp)
|
214
|
+
st.session_state.sensor_mask = np.copy(st.session_state.sensor_mask_temp)
|
215
|
+
st.session_state.isSensorTest = True
|
216
|
+
# Deactivate Checks for other pages
|
217
|
+
st.session_state.isQCTest = False
|
218
|
+
st.session_state.isProfileMask = False
|
219
|
+
st.session_state.isGridSave = False
|
220
|
+
st.session_state.isVelocityMask = False
|
221
|
+
|
222
|
+
|
223
|
+
# Plot Function
|
224
|
+
@st.cache_data
|
225
|
+
def lineplot(data, title, slope=None, xaxis="time"):
|
226
|
+
if xaxis == "time":
|
227
|
+
xdata = st.session_state.date
|
228
|
+
else:
|
229
|
+
xdata = st.session_state.ensemble_axis
|
230
|
+
scatter_trace = FigureResampler(go.Figure())
|
231
|
+
scatter_trace = go.Scatter(
|
232
|
+
x=xdata, y=data, mode="lines", name=title, marker=dict(color="blue", size=10)
|
233
|
+
)
|
234
|
+
# Create the slope line trace
|
235
|
+
if slope is not None:
|
236
|
+
line_trace = go.Scatter(
|
237
|
+
x=xdata,
|
238
|
+
y=slope,
|
239
|
+
mode="lines",
|
240
|
+
name="Slope Line",
|
241
|
+
line=dict(color="red", width=2, dash="dash"),
|
242
|
+
)
|
243
|
+
fig = go.Figure(data=[scatter_trace, line_trace])
|
244
|
+
else:
|
245
|
+
fig = go.Figure(data=[scatter_trace])
|
246
|
+
|
247
|
+
st.plotly_chart(fig)
|
248
|
+
|
249
|
+
|
250
|
+
# Session States
|
251
|
+
if not st.session_state.isSensorPageReturn:
|
252
|
+
st.write(":grey[Creating a new mask file ...]")
|
253
|
+
# Check if any test is carried out using isAnyQCTest().
|
254
|
+
# If the page is accessed first time, set all sensor session states
|
255
|
+
# to default.
|
256
|
+
if st.session_state.isFirstSensorVisit:
|
257
|
+
reset_sensor()
|
258
|
+
st.session_state.isFirstSensorVisit = False
|
259
|
+
else:
|
260
|
+
# If the page is revisited, warn the user not to change the settings
|
261
|
+
# without resetting the mask file.
|
262
|
+
# if st.session_state.isSensorPageReturn:
|
263
|
+
st.write(":grey[Working on a saved mask file ...]")
|
264
|
+
st.write(
|
265
|
+
":orange[WARNING! Sensor test already completed. Reset to change settings.]"
|
266
|
+
)
|
267
|
+
reset_button_saved_mask = st.button("Reset Mask Data", on_click=reset_sensor)
|
268
|
+
|
269
|
+
if reset_button_saved_mask:
|
270
|
+
st.write(":green[Mask data is reset to default]")
|
271
|
+
|
272
|
+
# ------------------------------------
|
273
|
+
# -------------WEB PAGES -------------
|
274
|
+
# ------------------------------------
|
275
|
+
|
276
|
+
|
277
|
+
# ----------- SENSOR HEALTH ----------
|
278
|
+
st.header("Sensor Health", divider="blue")
|
279
|
+
st.write(
|
280
|
+
"""
|
281
|
+
The following details can be used to determine whether the
|
282
|
+
additional sensors are functioning properly.
|
283
|
+
"""
|
284
|
+
)
|
285
|
+
|
286
|
+
tab1, tab2, tab3, tab4, tab5, tab6, tab7, tab8 = st.tabs(
|
287
|
+
[
|
288
|
+
"Pressure",
|
289
|
+
"Salinity",
|
290
|
+
"Temperature",
|
291
|
+
"Heading",
|
292
|
+
"Roll",
|
293
|
+
"Pitch",
|
294
|
+
"Corrections",
|
295
|
+
"Save/Reset",
|
296
|
+
]
|
297
|
+
)
|
298
|
+
|
299
|
+
# ################## Pressure Sensor Check ###################
|
300
|
+
with tab1:
|
301
|
+
st.subheader("1. Pressure Sensor Check", divider="orange")
|
302
|
+
st.write("""
|
303
|
+
Verify whether the pressure sensor is functioning correctly
|
304
|
+
or exhibiting drift. The actual deployment depth can be
|
305
|
+
cross-checked using the mooring diagram for confirmation.
|
306
|
+
To remove outliers, apply the standard deviation method.
|
307
|
+
""")
|
308
|
+
depth = ds.variableleader.depth_of_transducer
|
309
|
+
depth_data = depth.data * depth.scale * 1.0
|
310
|
+
|
311
|
+
leftd, rightd = st.columns([1, 1])
|
312
|
+
# Clean up the deployment and recovery data
|
313
|
+
# Compute mean and standard deviation
|
314
|
+
depth_median = np.median(depth_data)
|
315
|
+
depth_std = np.nanstd(depth_data)
|
316
|
+
# Get the number of standard deviation
|
317
|
+
with rightd:
|
318
|
+
depth_no_std = st.number_input(
|
319
|
+
"Standard Deviation Cutoff", 0.01, 10.0, 3.0, 0.1
|
320
|
+
)
|
321
|
+
depth_xbutton = st.radio(
|
322
|
+
"Select an x-axis to plot", ["time", "ensemble"], horizontal=True
|
323
|
+
)
|
324
|
+
# Local Reset
|
325
|
+
depth_reset = st.button("Reset Depth to Default", on_click=reset_button_depth)
|
326
|
+
if depth_reset:
|
327
|
+
st.success("Depth reset to default")
|
328
|
+
|
329
|
+
# Mark data above 3 standard deviation as bad
|
330
|
+
depth_bad = np.abs(depth_data - depth_median) > depth_no_std * depth_std
|
331
|
+
depth_data[depth_bad] = np.nan
|
332
|
+
depth_nan = ~np.isnan(depth_data)
|
333
|
+
# Remove data that are bad
|
334
|
+
depth_x = ds.variableleader.rdi_ensemble.data[depth_nan]
|
335
|
+
depth_y = depth_data[depth_nan]
|
336
|
+
|
337
|
+
# Compute the slope
|
338
|
+
depth_slope, depth_intercept = np.polyfit(depth_x, depth_y, 1)
|
339
|
+
depth_fitted_line = depth_slope * st.session_state.ensemble_axis + depth_intercept
|
340
|
+
depth_change = depth_fitted_line[-1] - depth_fitted_line[0]
|
341
|
+
st.session_state.sensor_depth_data = depth_data
|
342
|
+
|
343
|
+
# Display median and slope
|
344
|
+
with leftd:
|
345
|
+
st.write(":blue-background[Additional Information:]")
|
346
|
+
st.write(
|
347
|
+
"**Depth Sensor**: ", st.session_state.flead.ez_sensor()["Depth Sensor"]
|
348
|
+
)
|
349
|
+
st.write(f"Total ensembles: `{st.session_state.head.ensembles}`")
|
350
|
+
st.write(f"**Median depth**: `{depth_median/10} (m)`")
|
351
|
+
st.write(f"**Change in depth**: `{np.round(depth_change, 3)} (m)`")
|
352
|
+
st.write("**Depth Modified**: ", st.session_state.isDepthModified)
|
353
|
+
|
354
|
+
# Plot the data
|
355
|
+
# label= depth.long_name + ' (' + depth.unit + ')'
|
356
|
+
label = depth.long_name + " (m)"
|
357
|
+
lineplot(depth_data / 10, label, slope=depth_fitted_line / 10, xaxis=depth_xbutton)
|
358
|
+
|
359
|
+
st.info(
|
360
|
+
"""
|
361
|
+
If the pressure sensor is not working, upload corrected *CSV*
|
362
|
+
file containing the transducer depth. The number of ensembles
|
363
|
+
should match the original file. The *CSV* file should contain
|
364
|
+
only single column without header.
|
365
|
+
""",
|
366
|
+
icon="ℹ️",
|
367
|
+
)
|
368
|
+
|
369
|
+
st.session_state.sensor_depthoption = st.radio(
|
370
|
+
"Select method for depth correction:",
|
371
|
+
["File Upload", "Fixed Value"],
|
372
|
+
horizontal=True,
|
373
|
+
)
|
374
|
+
|
375
|
+
if st.session_state.sensor_depthoption == "Fixed Value":
|
376
|
+
st.session_state.sensor_depthinput = st.number_input(
|
377
|
+
"Enter corrected depth (m): ",
|
378
|
+
value=None,
|
379
|
+
min_value=0,
|
380
|
+
placeholder="Type a number ...",
|
381
|
+
)
|
382
|
+
depthbutton = st.button("Change Depth", on_click=set_button_depth)
|
383
|
+
if depthbutton:
|
384
|
+
st.success(f"Depth changed to {st.session_state.sensor_depthinput}")
|
385
|
+
else:
|
386
|
+
st.session_state.uploaded_file_depth = st.file_uploader(
|
387
|
+
"Upload Corrected Depth File",
|
388
|
+
type="csv",
|
389
|
+
)
|
390
|
+
if st.session_state.uploaded_file_depth is not None:
|
391
|
+
# Check if the number of ensembles match and call button function
|
392
|
+
upload_file_depth_button = st.button(
|
393
|
+
"Check & Save Depth", on_click=set_button_upload_depth
|
394
|
+
)
|
395
|
+
if upload_file_depth_button:
|
396
|
+
if (
|
397
|
+
len(st.session_state.df_numpy_depth)
|
398
|
+
!= st.session_state.head.ensembles
|
399
|
+
):
|
400
|
+
st.error(
|
401
|
+
f"""
|
402
|
+
**ERROR: Ensembles not matching.** \\
|
403
|
+
\\
|
404
|
+
Uploaded file ensemble size is {len(st.session_state.df_numpy_depth)}.
|
405
|
+
Actual ensemble size: {st.session_state.head.ensembles}.
|
406
|
+
""",
|
407
|
+
icon="🚨",
|
408
|
+
)
|
409
|
+
else:
|
410
|
+
lineplot(
|
411
|
+
np.squeeze(st.session_state.depth.T),
|
412
|
+
title="Modified Depth",
|
413
|
+
)
|
414
|
+
st.success(" Depth of the transducer modified.", icon="✅")
|
415
|
+
|
416
|
+
|
417
|
+
# ################## Conductivity Sensor Check ###################
|
418
|
+
with tab2:
|
419
|
+
st.subheader("2. Conductivity Sensor Check", divider="orange")
|
420
|
+
st.write("""
|
421
|
+
Verify whether the salinity sensor is functioning properly
|
422
|
+
or showing signs of drift. If a salinity sensor is unavailable,
|
423
|
+
use a constant value. To eliminate outliers, apply the standard
|
424
|
+
deviation method.
|
425
|
+
""")
|
426
|
+
salinity = ds.variableleader.salinity
|
427
|
+
salinity_data = salinity.data * salinity.scale * 1.0
|
428
|
+
|
429
|
+
lefts, rights = st.columns([1, 1])
|
430
|
+
|
431
|
+
# Clean up the deployment and recovery data
|
432
|
+
# Compute mean and standard deviation
|
433
|
+
salinity_median = np.nanmedian(salinity_data)
|
434
|
+
salinity_std = np.nanstd(salinity_data)
|
435
|
+
|
436
|
+
with rights:
|
437
|
+
salinity_no_std = st.number_input(
|
438
|
+
"Standard Deviation Cutoff for salinity", 0.01, 10.0, 3.0, 0.1
|
439
|
+
)
|
440
|
+
salinity_xbutton = st.radio(
|
441
|
+
"Select an x-axis to plot for salinity",
|
442
|
+
["time", "ensemble"],
|
443
|
+
horizontal=True,
|
444
|
+
)
|
445
|
+
salinity_reset = st.button(
|
446
|
+
"Reset Salinity to Default", on_click=reset_button_salinity
|
447
|
+
)
|
448
|
+
if salinity_reset:
|
449
|
+
st.success("Salinity reset to default")
|
450
|
+
|
451
|
+
salinity_bad = (
|
452
|
+
np.abs(salinity_data - salinity_median) > salinity_no_std * salinity_std
|
453
|
+
)
|
454
|
+
salinity_data[salinity_bad] = np.nan
|
455
|
+
salinity_nan = ~np.isnan(salinity_data)
|
456
|
+
|
457
|
+
# Remove data that are bad
|
458
|
+
salinity_x = st.session_state.ensemble_axis[salinity_nan]
|
459
|
+
salinity_y = salinity_data[salinity_nan]
|
460
|
+
|
461
|
+
## Compute the slope
|
462
|
+
salinity_slope, salinity_intercept = np.polyfit(salinity_x, salinity_y, 1)
|
463
|
+
salinity_fitted_line = (
|
464
|
+
salinity_slope * st.session_state.ensemble_axis + salinity_intercept
|
465
|
+
)
|
466
|
+
salinity_change = salinity_fitted_line[-1] - salinity_fitted_line[0]
|
467
|
+
|
468
|
+
st.session_state.sensor_salinity_data = salinity_data
|
469
|
+
|
470
|
+
with lefts:
|
471
|
+
st.write(":blue-background[Additional Information:]")
|
472
|
+
st.write(
|
473
|
+
"Conductivity Sensor: ",
|
474
|
+
st.session_state.flead.ez_sensor()["Conductivity Sensor"],
|
475
|
+
)
|
476
|
+
st.write(f"Total ensembles: `{st.session_state.head.ensembles}`")
|
477
|
+
st.write(f"Median salinity: {salinity_median} $^o$C")
|
478
|
+
st.write(f"Change in salinity: {salinity_change} $^o$C")
|
479
|
+
st.write("**Salinity Modified**: ", st.session_state.isSalinityModified)
|
480
|
+
|
481
|
+
# Plot the data
|
482
|
+
label = salinity.long_name
|
483
|
+
salinity_data = np.round(salinity_data)
|
484
|
+
salinity_fitted_line = np.round(salinity_fitted_line)
|
485
|
+
lineplot(
|
486
|
+
np.int32(salinity_data),
|
487
|
+
label,
|
488
|
+
slope=salinity_fitted_line,
|
489
|
+
xaxis=salinity_xbutton,
|
490
|
+
)
|
491
|
+
|
492
|
+
st.info(
|
493
|
+
"""
|
494
|
+
If the salinity values are not correct or the sensor is not
|
495
|
+
functioning, change the value or upload a
|
496
|
+
corrected *CSV* file containing only the salinity values.
|
497
|
+
The *CSV* file must have a single column without a header,
|
498
|
+
and the number of ensembles should match the original file.
|
499
|
+
These updated temperature values will be used to adjust the
|
500
|
+
velocity data and depth cell measurements.
|
501
|
+
""",
|
502
|
+
icon="ℹ️",
|
503
|
+
)
|
504
|
+
|
505
|
+
st.session_state.sensor_salinityoption = st.radio(
|
506
|
+
"Select method", ["Fixed Value", "File Upload"], horizontal=True
|
507
|
+
)
|
508
|
+
|
509
|
+
if st.session_state.sensor_salinityoption == "Fixed Value":
|
510
|
+
st.session_state.sensor_salinityinput = st.number_input(
|
511
|
+
"Enter corrected salinity: ",
|
512
|
+
value=None,
|
513
|
+
min_value=0.0,
|
514
|
+
placeholder="Type a number ...",
|
515
|
+
)
|
516
|
+
salinitybutton = st.button("Change Salinity", on_click=set_button_salinity)
|
517
|
+
if salinitybutton:
|
518
|
+
st.success(f"Salinity changed to {st.session_state.sensor_salinityinput}")
|
519
|
+
st.session_state.isSalinityModified = True
|
520
|
+
else:
|
521
|
+
st.write(f"Total ensembles: `{st.session_state.head.ensembles}`")
|
522
|
+
|
523
|
+
st.session_state.uploaded_file_salinity = st.file_uploader(
|
524
|
+
"Upload Corrected Salinity File",
|
525
|
+
type="csv",
|
526
|
+
)
|
527
|
+
if st.session_state.uploaded_file_salinity is not None:
|
528
|
+
upload_file_salinity_button = st.button(
|
529
|
+
"Check & Save Salinity", on_click=set_button_upload_salinity
|
530
|
+
)
|
531
|
+
if upload_file_salinity_button:
|
532
|
+
if (
|
533
|
+
len(st.session_state.df_numpy_salinity)
|
534
|
+
!= st.session_state.head.ensembles
|
535
|
+
):
|
536
|
+
st.session_state.isSalinityModified = False
|
537
|
+
st.error(
|
538
|
+
f"""
|
539
|
+
**ERROR: Ensembles not matching.** \\
|
540
|
+
\\
|
541
|
+
Uploaded file ensemble size is {len(st.session_state.df_numpy_salinity)}.
|
542
|
+
Actual ensemble size is {st.session_state.head.ensembles}.
|
543
|
+
""",
|
544
|
+
icon="🚨",
|
545
|
+
)
|
546
|
+
else:
|
547
|
+
st.success("Salinity changed.", icon="✅")
|
548
|
+
lineplot(
|
549
|
+
np.squeeze(st.session_state.df_numpy_salinity.T),
|
550
|
+
title="Modified Salinity",
|
551
|
+
)
|
552
|
+
|
553
|
+
# ################## Temperature Sensor Check ###################
|
554
|
+
with tab3:
|
555
|
+
# ################## Temperature Sensor Check ###################
|
556
|
+
st.subheader("3. Temperature Sensor Check", divider="orange")
|
557
|
+
st.write("""
|
558
|
+
Verify whether the temperature sensor is functioning correctly or exhibiting drift.
|
559
|
+
The actual deployment depth can be cross-checked using external data (like CTD cast)
|
560
|
+
for confirmation. To remove outliers, apply the standard deviation method.
|
561
|
+
""")
|
562
|
+
temp = ds.variableleader.temperature
|
563
|
+
temp_data = temp.data * temp.scale
|
564
|
+
|
565
|
+
leftt, rightt = st.columns([1, 1])
|
566
|
+
## Clean up the deployment and recovery data
|
567
|
+
# Compute mean and standard deviation
|
568
|
+
temp_median = np.nanmedian(temp_data)
|
569
|
+
temp_std = np.nanstd(temp_data)
|
570
|
+
# Get the number of standard deviation
|
571
|
+
with rightt:
|
572
|
+
temp_no_std = st.number_input(
|
573
|
+
"Standard Deviation Cutoff for Temperature", 0.01, 10.0, 3.0, 0.1
|
574
|
+
)
|
575
|
+
temp_xbutton = st.radio(
|
576
|
+
"Select an x-axis to plot for temperature",
|
577
|
+
["time", "ensemble"],
|
578
|
+
horizontal=True,
|
579
|
+
)
|
580
|
+
temp_reset = st.button(
|
581
|
+
"Reset Temperature to Default", on_click=reset_button_temperature
|
582
|
+
)
|
583
|
+
if temp_reset:
|
584
|
+
st.success("Temperature Reset to Default")
|
585
|
+
|
586
|
+
# Mark data above 3 standard deviation as bad
|
587
|
+
temp_bad = np.abs(temp_data - temp_median) > temp_no_std * temp_std
|
588
|
+
temp_data[temp_bad] = np.nan
|
589
|
+
temp_nan = ~np.isnan(temp_data)
|
590
|
+
# Remove data that are bad
|
591
|
+
temp_x = st.session_state.ensemble_axis[temp_nan]
|
592
|
+
temp_y = temp_data[temp_nan]
|
593
|
+
## Compute the slope
|
594
|
+
temp_slope, temp_intercept = np.polyfit(temp_x, temp_y, 1)
|
595
|
+
temp_fitted_line = temp_slope * st.session_state.ensemble_axis + temp_intercept
|
596
|
+
temp_change = temp_fitted_line[-1] - temp_fitted_line[0]
|
597
|
+
|
598
|
+
st.session_state.sensor_temp_data = temp_data
|
599
|
+
|
600
|
+
with leftt:
|
601
|
+
st.write(":blue-background[Additional Information:]")
|
602
|
+
st.write(
|
603
|
+
"Temperature Sensor: ",
|
604
|
+
st.session_state.flead.ez_sensor()["Temperature Sensor"],
|
605
|
+
)
|
606
|
+
st.write(f"Total ensembles: `{st.session_state.head.ensembles}`")
|
607
|
+
st.write(f"Median temperature: {temp_median} $^o$C")
|
608
|
+
st.write(f"Change in temperature: {np.round(temp_change, 3)} $^o$C")
|
609
|
+
st.write("**Temperature Modified**: ", st.session_state.isTemperatureModified)
|
610
|
+
|
611
|
+
# Plot the data
|
612
|
+
label = temp.long_name + " (oC)"
|
613
|
+
lineplot(temp_data, label, slope=temp_fitted_line, xaxis=temp_xbutton)
|
614
|
+
|
615
|
+
#
|
616
|
+
st.info(
|
617
|
+
"""
|
618
|
+
If the temperature sensor is not functioning, upload a
|
619
|
+
corrected *CSV* file containing only the temperature values.
|
620
|
+
The *CSV* file must have a single column without a header,
|
621
|
+
and the number of ensembles should match the original file.
|
622
|
+
These updated temperature values will be used to adjust the
|
623
|
+
velocity data and depth cell measurements.
|
624
|
+
""",
|
625
|
+
icon="ℹ️",
|
626
|
+
)
|
627
|
+
|
628
|
+
st.session_state.sensor_tempoption = st.radio(
|
629
|
+
"Select method for temperature correction:",
|
630
|
+
["File Upload", "Fixed Value"],
|
631
|
+
horizontal=True,
|
632
|
+
)
|
633
|
+
|
634
|
+
if st.session_state.sensor_tempoption == "Fixed Value":
|
635
|
+
st.session_state.sensor_tempinput = st.number_input(
|
636
|
+
"Enter corrected temperature: ",
|
637
|
+
value=None,
|
638
|
+
min_value=0.0,
|
639
|
+
placeholder="Type a number ...",
|
640
|
+
)
|
641
|
+
tempbutton = st.button("Change Temperature", on_click=set_button_temperature)
|
642
|
+
if tempbutton:
|
643
|
+
st.success(f"Temperature changed to {st.session_state.sensor_tempinput}")
|
644
|
+
st.session_state.isTemperatureModified = True
|
645
|
+
elif st.session_state.sensor_tempoption == "File Upload":
|
646
|
+
st.write(f"Total ensembles: `{st.session_state.head.ensembles}`")
|
647
|
+
st.session_state.uploaded_file_temperature = st.file_uploader(
|
648
|
+
"Upload Corrected Temperature File",
|
649
|
+
type="csv",
|
650
|
+
)
|
651
|
+
if st.session_state.uploaded_file_temperature is not None:
|
652
|
+
upload_file_temperature_button = st.button(
|
653
|
+
"Check & Save Temperature", on_click=set_button_upload_temperature
|
654
|
+
)
|
655
|
+
|
656
|
+
if upload_file_temperature_button:
|
657
|
+
if (
|
658
|
+
len(st.session_state.df_numpy_temperature)
|
659
|
+
!= st.session_state.head.ensembles
|
660
|
+
):
|
661
|
+
st.session_state.isTemperatureModified = False
|
662
|
+
st.error(
|
663
|
+
f"""
|
664
|
+
**ERROR: Ensembles not matching.** \\
|
665
|
+
\\
|
666
|
+
Uploaded file ensemble size is {len(st.session_state.df_numpy_temperature)}.
|
667
|
+
Actual ensemble size is {st.session_state.head.ensembles}.
|
668
|
+
""",
|
669
|
+
icon="🚨",
|
670
|
+
)
|
671
|
+
else:
|
672
|
+
st.success(" The temperature of transducer modified.", icon="✅")
|
673
|
+
st.session_state.temperature = st.session_state.df_numpy_temperature
|
674
|
+
st.session_state.isTemperatureModified = True
|
675
|
+
lineplot(
|
676
|
+
np.squeeze(st.session_state.df_numpy_temperature.T),
|
677
|
+
title="Modified Temperature",
|
678
|
+
)
|
679
|
+
|
680
|
+
|
681
|
+
# ################## Heading Sensor Check ###################
|
682
|
+
with tab4:
|
683
|
+
st.warning(
|
684
|
+
"""
|
685
|
+
WARNING: Heading sensor corrections are currently unavailable.
|
686
|
+
This feature will be included in a future release.
|
687
|
+
""",
|
688
|
+
icon="⚠️",
|
689
|
+
)
|
690
|
+
st.subheader("3. Heading Sensor Check", divider="orange")
|
691
|
+
head = ds.variableleader.heading
|
692
|
+
head_data = head.data * head.scale
|
693
|
+
|
694
|
+
# Compute mean
|
695
|
+
head_rad = np.radians(head_data)
|
696
|
+
head_mean_x = np.mean(np.cos(head_rad))
|
697
|
+
head_mean_y = np.mean(np.sin(head_rad))
|
698
|
+
head_mean_rad = np.arctan2(head_mean_y, head_mean_x)
|
699
|
+
head_mean_deg = np.degrees(head_mean_rad)
|
700
|
+
|
701
|
+
head_xbutton = st.radio(
|
702
|
+
"Select an x-axis to plot for headerature",
|
703
|
+
["time", "ensemble"],
|
704
|
+
horizontal=True,
|
705
|
+
)
|
706
|
+
|
707
|
+
st.write(f"Mean heading: {np.round(head_mean_deg, 2)} $^o$")
|
708
|
+
|
709
|
+
# Plot the data
|
710
|
+
label = head.long_name
|
711
|
+
lineplot(head_data, label, xaxis=head_xbutton)
|
712
|
+
|
713
|
+
################### Tilt Sensor Check: Pitch ###################
|
714
|
+
with tab5:
|
715
|
+
st.subheader("4. Tilt Sensor Check: Pitch", divider="orange")
|
716
|
+
st.warning(
|
717
|
+
"""
|
718
|
+
WARNING: Tilt sensor corrections are currently unavailable.
|
719
|
+
This feature will be included in a future release.
|
720
|
+
""",
|
721
|
+
icon="⚠️",
|
722
|
+
)
|
723
|
+
|
724
|
+
st.write("The tilt sensor should not show much variation.")
|
725
|
+
|
726
|
+
pitch = ds.variableleader.pitch
|
727
|
+
pitch_data = pitch.data * pitch.scale
|
728
|
+
# Compute mean
|
729
|
+
pitch_rad = np.radians(pitch_data)
|
730
|
+
pitch_mean_x = np.mean(np.cos(pitch_rad))
|
731
|
+
pitch_mean_y = np.mean(np.sin(pitch_rad))
|
732
|
+
pitch_mean_rad = np.arctan2(pitch_mean_y, pitch_mean_x)
|
733
|
+
pitch_mean_deg = np.degrees(pitch_mean_rad)
|
734
|
+
|
735
|
+
pitch_xbutton = st.radio(
|
736
|
+
"Select an x-axis to plot for pitcherature",
|
737
|
+
["time", "ensemble"],
|
738
|
+
horizontal=True,
|
739
|
+
)
|
740
|
+
st.write(f"Mean pitch: {np.round(pitch_mean_deg, 2)} $^o$")
|
741
|
+
|
742
|
+
# Plot the data
|
743
|
+
label = pitch.long_name
|
744
|
+
lineplot(pitch_data, label, xaxis=pitch_xbutton)
|
745
|
+
|
746
|
+
################### Tilt Sensor Check: Roll ###################
|
747
|
+
with tab6:
|
748
|
+
st.subheader("5. Tilt Sensor Check: Roll", divider="orange")
|
749
|
+
st.warning(
|
750
|
+
"""
|
751
|
+
WARNING: Tilt sensor corrections are currently unavailable.
|
752
|
+
This feature will be included in a future release.
|
753
|
+
""",
|
754
|
+
icon="⚠️",
|
755
|
+
)
|
756
|
+
roll = ds.variableleader.roll
|
757
|
+
roll_data = roll.data * roll.scale
|
758
|
+
# Compute mean
|
759
|
+
roll_rad = np.radians(roll_data)
|
760
|
+
roll_mean_x = np.mean(np.cos(roll_rad))
|
761
|
+
roll_mean_y = np.mean(np.sin(roll_rad))
|
762
|
+
roll_mean_rad = np.arctan2(roll_mean_y, roll_mean_x)
|
763
|
+
roll_mean_deg = np.degrees(roll_mean_rad)
|
764
|
+
|
765
|
+
roll_xbutton = st.radio(
|
766
|
+
"Select an x-axis to plot for rollerature",
|
767
|
+
["time", "ensemble"],
|
768
|
+
horizontal=True,
|
769
|
+
)
|
770
|
+
st.write(f"Mean roll: {np.round(roll_mean_deg, 2)} $^o$")
|
771
|
+
|
772
|
+
# Plot the data
|
773
|
+
label = roll.long_name
|
774
|
+
lineplot(roll_data, label, xaxis=roll_xbutton)
|
775
|
+
|
776
|
+
|
777
|
+
with tab7:
|
778
|
+
st.subheader("Apply Sensor Thresholds/Corrections", divider="orange")
|
779
|
+
col1, col2 = st.columns([0.4, 0.6], gap="large")
|
780
|
+
with col1:
|
781
|
+
st.session_state.sensor_roll_cutoff = st.number_input(
|
782
|
+
"Enter roll threshold (deg):", min_value=0, max_value=359, value=15
|
783
|
+
)
|
784
|
+
st.session_state.sensor_pitch_cutoff = st.number_input(
|
785
|
+
"Enter pitch threshold (deg):", min_value=0, max_value=359, value=15
|
786
|
+
)
|
787
|
+
|
788
|
+
with col2:
|
789
|
+
st.write("Select Options:")
|
790
|
+
|
791
|
+
with st.form("Select options"):
|
792
|
+
if (
|
793
|
+
st.session_state.isTemperatureModified
|
794
|
+
or st.session_state.isSalinityModified
|
795
|
+
):
|
796
|
+
st.session_state.sensor_ischeckbox_disabled = False
|
797
|
+
else:
|
798
|
+
st.session_state.sensor_ischeckbox_disabled = True
|
799
|
+
st.info("No velocity corrections required.")
|
800
|
+
|
801
|
+
st.session_state.sensor_roll_checkbox = st.checkbox("Roll Threshold")
|
802
|
+
st.session_state.sensor_pitch_checkbox = st.checkbox("Pitch Threshold")
|
803
|
+
st.session_state.sensor_fix_velocity_checkbox = st.checkbox(
|
804
|
+
"Fix Velocity", disabled=st.session_state.sensor_ischeckbox_disabled
|
805
|
+
)
|
806
|
+
# fix_depth_button = st.checkbox(
|
807
|
+
# "Fix Depth Cell Size", disabled=is_checkbox_disabled
|
808
|
+
# )
|
809
|
+
|
810
|
+
submitted = st.form_submit_button("Submit", on_click=set_threshold_button)
|
811
|
+
|
812
|
+
if submitted:
|
813
|
+
set_threshold_button()
|
814
|
+
# Display Threshold Checks
|
815
|
+
if st.session_state.sensor_roll_checkbox:
|
816
|
+
st.success("Roll Test Applied")
|
817
|
+
if st.session_state.sensor_pitch_checkbox:
|
818
|
+
st.success("Pitch Test Applied")
|
819
|
+
if (
|
820
|
+
st.session_state.sensor_fix_velocity_checkbox
|
821
|
+
and not st.session_state.sensor_ischeckbox_disabled
|
822
|
+
):
|
823
|
+
st.success("Velocity Correction Applied")
|
824
|
+
|
825
|
+
reset_button_threshold = st.button(
|
826
|
+
"Reset Corrections", on_click=reset_threshold_button
|
827
|
+
)
|
828
|
+
|
829
|
+
if reset_button_threshold:
|
830
|
+
st.info("Data reset to defaults")
|
831
|
+
|
832
|
+
with tab8:
|
833
|
+
################## Save Button #############
|
834
|
+
st.header("Save Data", divider="blue")
|
835
|
+
col1, col2 = st.columns([1, 1])
|
836
|
+
with col1:
|
837
|
+
save_mask_button = st.button(label="Save Mask Data", on_click=save_sensor)
|
838
|
+
if save_mask_button:
|
839
|
+
st.success("Mask file saved")
|
840
|
+
|
841
|
+
# Table summarizing changes
|
842
|
+
changes_summary = pd.DataFrame(
|
843
|
+
[
|
844
|
+
[
|
845
|
+
"Depth Modified",
|
846
|
+
"True" if st.session_state.isDepthModified else "False",
|
847
|
+
],
|
848
|
+
[
|
849
|
+
"Salinity Modified",
|
850
|
+
"True" if st.session_state.isSalinityModified else "False",
|
851
|
+
],
|
852
|
+
[
|
853
|
+
"Temperature Modified",
|
854
|
+
"True" if st.session_state.isTemperatureModified else "False",
|
855
|
+
],
|
856
|
+
[
|
857
|
+
"Pitch Test",
|
858
|
+
"True" if st.session_state.isPitchCheck else "False",
|
859
|
+
],
|
860
|
+
[
|
861
|
+
"Roll Test",
|
862
|
+
"True" if st.session_state.isRollCheck else "False",
|
863
|
+
],
|
864
|
+
[
|
865
|
+
"Velocity Correction (Sound)",
|
866
|
+
"True" if st.session_state.isVelocityModifiedSound else "False",
|
867
|
+
],
|
868
|
+
],
|
869
|
+
columns=["Test", "Status"],
|
870
|
+
)
|
871
|
+
# Apply styles using Styler.apply
|
872
|
+
styled_table = changes_summary.style.set_properties(
|
873
|
+
**{"text-align": "center"}
|
874
|
+
)
|
875
|
+
styled_table = styled_table.map(status_color_map, subset=["Status"])
|
876
|
+
|
877
|
+
# Display the styled table
|
878
|
+
st.write(styled_table.to_html(), unsafe_allow_html=True)
|
879
|
+
|
880
|
+
else:
|
881
|
+
st.warning(" WARNING: Mask data not saved", icon="⚠️")
|
882
|
+
with col2:
|
883
|
+
# Reset local variables
|
884
|
+
reset_mask_button = st.button("Reset mask Data", on_click=reset_sensor)
|
885
|
+
if reset_mask_button:
|
886
|
+
# Global variables reset
|
887
|
+
st.session_state.isSensorTest = False
|
888
|
+
st.session_state.isQCTest = False
|
889
|
+
st.session_state.isGrid = False
|
890
|
+
st.session_state.isProfileMask = False
|
891
|
+
st.session_state.isVelocityMask = False
|
892
|
+
st.success("Mask data is reset to default")
|