pyadps 0.2.0b0__py3-none-any.whl → 0.3.0__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.
Files changed (39) hide show
  1. pyadps/Home_Page.py +11 -5
  2. pyadps/pages/01_Read_File.py +623 -211
  3. pyadps/pages/02_View_Raw_Data.py +97 -41
  4. pyadps/pages/03_Download_Raw_File.py +200 -67
  5. pyadps/pages/04_Sensor_Health.py +905 -0
  6. pyadps/pages/05_QC_Test.py +493 -0
  7. pyadps/pages/06_Profile_Test.py +971 -0
  8. pyadps/pages/07_Velocity_Test.py +600 -0
  9. pyadps/pages/08_Write_File.py +623 -0
  10. pyadps/pages/09_Add-Ons.py +168 -0
  11. pyadps/utils/__init__.py +5 -3
  12. pyadps/utils/autoprocess.py +371 -80
  13. pyadps/utils/logging_utils.py +269 -0
  14. pyadps/utils/metadata/config.ini +22 -4
  15. pyadps/utils/metadata/demo.000 +0 -0
  16. pyadps/utils/metadata/flmeta.json +420 -420
  17. pyadps/utils/metadata/vlmeta.json +611 -565
  18. pyadps/utils/multifile.py +292 -0
  19. pyadps/utils/plotgen.py +505 -3
  20. pyadps/utils/profile_test.py +720 -125
  21. pyadps/utils/pyreadrdi.py +164 -92
  22. pyadps/utils/readrdi.py +436 -186
  23. pyadps/utils/script.py +197 -147
  24. pyadps/utils/sensor_health.py +120 -0
  25. pyadps/utils/signal_quality.py +472 -68
  26. pyadps/utils/velocity_test.py +79 -31
  27. pyadps/utils/writenc.py +222 -39
  28. {pyadps-0.2.0b0.dist-info → pyadps-0.3.0.dist-info}/METADATA +63 -33
  29. pyadps-0.3.0.dist-info/RECORD +35 -0
  30. {pyadps-0.2.0b0.dist-info → pyadps-0.3.0.dist-info}/WHEEL +1 -1
  31. {pyadps-0.2.0b0.dist-info → pyadps-0.3.0.dist-info}/entry_points.txt +1 -0
  32. pyadps/pages/04_QC_Test.py +0 -334
  33. pyadps/pages/05_Profile_Test.py +0 -575
  34. pyadps/pages/06_Velocity_Test.py +0 -341
  35. pyadps/pages/07_Write_File.py +0 -452
  36. pyadps/utils/cutbin.py +0 -413
  37. pyadps/utils/regrid.py +0 -279
  38. pyadps-0.2.0b0.dist-info/RECORD +0 -31
  39. {pyadps-0.2.0b0.dist-info → pyadps-0.3.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,493 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import plotly.graph_objects as go
5
+ import streamlit as st
6
+ from plotly.subplots import make_subplots
7
+ from plotly_resampler import FigureResampler
8
+ from streamlit.runtime.state import session_state
9
+ from utils.signal_quality import (
10
+ ev_check,
11
+ false_target,
12
+ pg_check,
13
+ echo_check,
14
+ correlation_check,
15
+ )
16
+
17
+ if "flead" not in st.session_state:
18
+ st.write(":red[Please Select Data!]")
19
+ st.stop()
20
+
21
+
22
+ def reset_qctest():
23
+ # Reset Global Test
24
+ st.session_state.isQCTest = False
25
+ # Reset Local Tests
26
+ st.session_state.isQCCheck_QCT = False
27
+
28
+ # Reset Data
29
+ st.session_state.isBeamModified_QCT = False
30
+ st.session_state.beam_direction_QCT = st.session_state.beam_direction
31
+
32
+ # As QC test is not saved the Sensor Page Returns is set to False
33
+ st.session_state.isSensorPageReturn = False
34
+
35
+ # Reset Mask Data
36
+ # Copy the sensor mask if sensor mask completed.
37
+ # Else copy the default mask.
38
+ if st.session_state.isSensorTest:
39
+ st.session_state.qc_mask = np.copy(st.session_state.sensor_mask)
40
+ st.session_state.qc_mask_temp = np.copy(st.session_state.sensor_mask)
41
+ else:
42
+ st.session_state.qc_mask = np.copy(st.session_state.orig_mask)
43
+ st.session_state.qc_mask_temp = np.copy(st.session_state.orig_mask)
44
+
45
+
46
+ def hard_reset(option):
47
+ # Reset Global Test
48
+ st.session_state.isQCTest = False
49
+ # Reset Local Tests
50
+ st.session_state.isQCCheck_QCT = False
51
+ st.session_state.isBeamModified_QCT = False
52
+ # Reset Data
53
+ st.session_state.beam_direction_QCT = st.session_state.beam_direction
54
+
55
+ st.session_state.isSensorPageReturn = False
56
+
57
+ # Reset Mask Data based on user options
58
+ if option == "Sensor Test":
59
+ st.session_state.qc_mask = np.copy(st.session_state.sensor_mask)
60
+ st.session_state.qc_mask_temp = np.copy(st.session_state.sensor_mask)
61
+
62
+ elif option == "Default":
63
+ st.session_state.qc_mask = np.copy(st.session_state.orig_mask)
64
+ st.session_state.qc_mask_temp = np.copy(st.session_state.orig_mask)
65
+
66
+
67
+ def save_qctest():
68
+ st.session_state.qc_mask = np.copy(st.session_state.qc_mask_temp)
69
+ st.session_state.isQCTest = True
70
+ st.session_state.isProfileMask = False
71
+ st.session_state.isGridSave = False
72
+ st.session_state.isVelocityMask = False
73
+ # Indicate previous pages that Test has been carried out
74
+ st.session_state.isSensorPageReturn = True
75
+
76
+
77
+ def qc_submit():
78
+ # st.write(st.session_state.newthresh)
79
+ st.session_state.isQCCheck_QCT = True
80
+
81
+ # First Quality check of the page
82
+ mask = np.copy(st.session_state.qc_mask_temp)
83
+ # if st.session_state.isSensorTest:
84
+ # mask = np.copy(st.session_state.sensor_mask)
85
+ # else:
86
+ # mask = np.copy(st.session_state.default_mask)
87
+
88
+ ds = st.session_state.ds
89
+ pgt = st.session_state.pgt_QCT
90
+ ct = st.session_state.ct_QCT
91
+ et = st.session_state.et_QCT
92
+ evt = st.session_state.evt_QCT
93
+ ft = st.session_state.ft_QCT
94
+ is3beam = st.session_state.is3beam_QCT
95
+ beam_ignore = st.session_state.beam_to_ignore
96
+ mask = pg_check(ds, mask, pgt, threebeam=is3beam)
97
+ mask = correlation_check(ds, mask, ct,is3beam,beam_ignore=beam_ignore)
98
+ mask = echo_check(ds, mask, et,is3beam,beam_ignore=beam_ignore)
99
+ mask = ev_check(ds, mask, evt)
100
+ mask = false_target(ds, mask, ft, threebeam=is3beam, beam_ignore=beam_ignore)
101
+ # Store the processed mask in a temporary mask
102
+ st.session_state.qc_mask_temp = mask
103
+
104
+
105
+ if st.session_state.isSensorTest:
106
+ st.write(":grey[Working on a saved mask file ...]")
107
+ if st.session_state.isQCPageReturn:
108
+ st.write(
109
+ ":orange[Warning: QC test already completed. Reset the mask file to change settings.]"
110
+ )
111
+ reset_selectbox = st.selectbox(
112
+ "Choose reset option",
113
+ ("Sensor Test", "Default"),
114
+ index=None,
115
+ placeholder="Reset mask to ...",
116
+ )
117
+ if reset_selectbox is not None:
118
+ hard_reset(reset_selectbox)
119
+ elif st.session_state.isFirstQCVisit:
120
+ reset_qctest()
121
+ st.session_state.isFirstQCVisit = False
122
+ else:
123
+ if st.session_state.isFirstQCVisit:
124
+ # This will rest to the default mask file
125
+ reset_qctest()
126
+ st.session_state.isFirstQCVisit = False
127
+ st.write(":orange[Creating a new mask file ...]")
128
+
129
+
130
+ # Load data
131
+ ds = st.session_state.ds
132
+ flobj = st.session_state.flead
133
+ vlobj = st.session_state.vlead
134
+ velocity = st.session_state.velocity
135
+ echo = st.session_state.echo
136
+ correlation = st.session_state.correlation
137
+ pgood = st.session_state.pgood
138
+ ensembles = st.session_state.head.ensembles
139
+ cells = flobj.field()["Cells"]
140
+ fdata = flobj.fleader
141
+ vdata = vlobj.vleader
142
+ x = np.arange(0, ensembles, 1)
143
+ y = np.arange(0, cells, 1)
144
+
145
+
146
+ @st.cache_data
147
+ def fillplot_plotly(data, colorscale="balance"):
148
+ fig = FigureResampler(go.Figure())
149
+ data1 = np.where(data == -32768, np.nan, data)
150
+ fig.add_trace(
151
+ go.Heatmap(z=data1[:, 0:-1], x=x, y=y, colorscale=colorscale, hoverongaps=False)
152
+ )
153
+ st.plotly_chart(fig)
154
+
155
+
156
+ @st.cache_data
157
+ def lineplot(data, title, slope=None, xaxis="time"):
158
+ if xaxis == "time":
159
+ xdata = st.session_state.date
160
+ else:
161
+ xdata = st.session_state.ensemble_axis
162
+ scatter_trace = go.Scatter(
163
+ x=xdata,
164
+ y=data,
165
+ mode="markers",
166
+ name=title,
167
+ marker=dict(color="blue", size=10), # Customize marker color and size
168
+ )
169
+ # Create the slope line trace
170
+ if slope is not None:
171
+ line_trace = go.Scatter(
172
+ x=xdata,
173
+ y=slope,
174
+ mode="lines",
175
+ name="Slope Line",
176
+ line=dict(color="red", width=2, dash="dash"), # Dashed red line
177
+ )
178
+ fig = go.Figure(data=[scatter_trace, line_trace])
179
+ else:
180
+ fig = go.Figure(data=[scatter_trace])
181
+
182
+ st.plotly_chart(fig)
183
+
184
+
185
+ @st.cache_data
186
+ def plot_noise(dep=0, rec=-1):
187
+ n = dep
188
+ m = rec
189
+ colorleft = [
190
+ "rgb(240, 255, 255)",
191
+ "rgb(115, 147, 179)",
192
+ "rgb(100, 149, 237)",
193
+ "rgb(15, 82, 186)",
194
+ ]
195
+ colorright = [
196
+ "rgb(250, 200, 152)",
197
+ "rgb(255, 165, 0)",
198
+ "rgb(255, 95, 31)",
199
+ "rgb(139, 64, 0)",
200
+ ]
201
+ fig = make_subplots(
202
+ rows=1,
203
+ cols=2,
204
+ subplot_titles=[
205
+ f"Deployment Ensemble ({x[n]+1})",
206
+ f"Recovery Ensemble ({x[m]+1})",
207
+ ],
208
+ )
209
+ for i in range(4):
210
+ fig.add_trace(
211
+ go.Scatter(
212
+ x=echo[i, :, n],
213
+ y=y,
214
+ name=f"Beam (D) {i+1}",
215
+ line=dict(color=colorleft[i]),
216
+ ),
217
+ row=1,
218
+ col=1,
219
+ )
220
+ for i in range(4):
221
+ fig.add_trace(
222
+ go.Scatter(
223
+ x=echo[i, :, m],
224
+ y=y,
225
+ name=f"Beam (R) {i+1}",
226
+ line=dict(color=colorright[i]),
227
+ ),
228
+ row=1,
229
+ col=2,
230
+ )
231
+
232
+ fig.update_layout(height=600, width=800, title_text="Echo Intensity")
233
+ fig.update_xaxes(title="Echo (count)")
234
+ fig.update_yaxes(title="Cells")
235
+ st.plotly_chart(fig)
236
+
237
+
238
+ tab1, tab2, tab3, tab4, tab5 = st.tabs(
239
+ [
240
+ "Noise Floor Identification",
241
+ "QC Tests",
242
+ "Display Mask",
243
+ "Fix Orientation",
244
+ "Save Data",
245
+ ]
246
+ )
247
+ ######### NOISE FLOOR IDENTIFICATION ##############
248
+ with tab1:
249
+ dn = rn = 1
250
+ st.header("Noise Floor Identification", divider="blue")
251
+ st.write(
252
+ """
253
+ If the ADCP has collected data from the air either
254
+ before deployment or after recovery, this data can
255
+ be used to estimate the echo intensity threshold.
256
+ The plots below show the echo intensity from the first
257
+ and last ensembles. The noise level is typically around
258
+ 30-40 counts throughout the entire profile.
259
+ """
260
+ )
261
+ dn = st.number_input("Deployment Ensemble", x[0] + 1, x[-1] + 1, x[0] + 1)
262
+ # r = st.number_input("Recovery Ensemble", -1 * (x[-1] + 1), -1 * (x[0] + 1), -1)
263
+ rn = st.number_input("Recovery Ensemble", x[0] + 1, x[-1] + 1, x[-1] + 1)
264
+ dn = dn - 1
265
+ rn = rn - 1
266
+
267
+ plot_noise(dep=dn, rec=rn)
268
+
269
+
270
+ ################## QC Test ###################
271
+ with tab2:
272
+ st.header("Quality Control Tests", divider="blue")
273
+ st.write("")
274
+
275
+ left, right = st.columns([1, 1])
276
+ with left:
277
+ st.write(""" Teledyne RDI recommends these quality control tests,
278
+ some of which can be configured before deployment.
279
+ The pre-deployment values configured for the ADCP are listed
280
+ in the table below. The noise-floor identification graph above
281
+ can assist in determining the echo intensity threshold.
282
+ For more information about these tests,
283
+ refer to *Acoustic Doppler Current Profiler Principles of
284
+ Operation: A Practical Primer* by Teledyne RDI.""")
285
+ fdata = st.session_state.flead.field()
286
+ st.divider()
287
+ st.write(":blue-background[Additional Information:]")
288
+ st.write(f"Number of Pings per Ensemble: `{fdata["Pings"]}`")
289
+ st.write(f"Number of Beams: `{fdata["Beams"]}`")
290
+ st.divider()
291
+ st.write(":red-background[Thresholds used during deployment:]")
292
+ thresh = pd.DataFrame(
293
+ [
294
+ ["Correlation", fdata["Correlation Thresh"]],
295
+ ["Error Velocity", fdata["Error Velocity Thresh"]],
296
+ ["Echo Intensity", 0],
297
+ ["False Target", fdata["False Target Thresh"]],
298
+ ["Percentage Good", fdata["Percent Good Min"]],
299
+ ],
300
+ columns=["Threshold", "Values"],
301
+ )
302
+
303
+ st.write(thresh)
304
+
305
+ with right:
306
+ # with st.form(key="my_form"):
307
+ st.write("Would you like to apply new threshold?")
308
+
309
+ st.session_state.ct_QCT = st.number_input(
310
+ "Select Correlation Threshold",
311
+ 0,
312
+ 255,
313
+ fdata["Correlation Thresh"],
314
+ )
315
+
316
+ st.session_state.evt_QCT = st.number_input(
317
+ "Select Error Velocity Threshold",
318
+ 0,
319
+ 9999,
320
+ fdata["Error Velocity Thresh"],
321
+ )
322
+
323
+ st.session_state.et_QCT = st.number_input(
324
+ "Select Echo Intensity Threshold",
325
+ 0,
326
+ 255,
327
+ 0,
328
+ )
329
+
330
+ st.session_state.ft_QCT = st.number_input(
331
+ "Select False Target Threshold",
332
+ 0,
333
+ 255,
334
+ fdata["False Target Thresh"],
335
+ )
336
+
337
+ st.session_state.is3beam_QCT = st.selectbox(
338
+ "Would you like to use a three-beam solution?", (True, False)
339
+ )
340
+
341
+ if st.session_state.is3beam_QCT:
342
+ beam_label_to_value = {
343
+ "None": None,
344
+ "Beam 1": 0,
345
+ "Beam 2": 1,
346
+ "Beam 3": 2,
347
+ "Beam 4": 3
348
+ }
349
+
350
+ selected_beam = st.selectbox(
351
+ "Select Beam to Ignore",
352
+ options=list(beam_label_to_value.keys()),
353
+ index=0 # Default is "None"
354
+ )
355
+ st.session_state.beam_to_ignore = beam_label_to_value[selected_beam]
356
+
357
+ st.session_state.pgt_QCT = st.number_input(
358
+ "Select Percent Good Threshold",
359
+ 0,
360
+ 101,
361
+ fdata["Percent Good Min"],
362
+ )
363
+ submit_button = st.button(label="Submit", on_click=qc_submit)
364
+
365
+ # mask = st.session_state.qc_mask_temp
366
+ with left:
367
+ if submit_button:
368
+ st.session_state.isQCCheck_QCT = True
369
+ st.session_state.newthresh = pd.DataFrame(
370
+ [
371
+ ["Correlation", str(st.session_state.ct_QCT)],
372
+ ["Error Velocity", str(st.session_state.evt_QCT)],
373
+ ["Echo Intensity", str(st.session_state.et_QCT)],
374
+ ["False Target", str(st.session_state.ft_QCT)],
375
+ ["Three Beam", str(st.session_state.is3beam_QCT)],
376
+ ["Percentage Good", str(st.session_state.pgt_QCT)],
377
+ ],
378
+ columns=["Threshold", "Values"],
379
+ )
380
+
381
+ if st.session_state.isQCCheck_QCT:
382
+ st.write(":green-background[Current Thresholds]")
383
+ st.write(st.session_state.newthresh)
384
+
385
+
386
+ with tab3:
387
+ st.header("Mask File", divider="blue")
388
+ st.write(
389
+ """
390
+ Display the mask file.
391
+ Ensure to save any necessary changes or apply additional thresholds if needed.
392
+ """
393
+ )
394
+
395
+ leftplot, rightplot = st.columns([1, 1])
396
+ if st.button("Display mask file"):
397
+ with leftplot:
398
+ st.subheader("Default Mask File")
399
+ st.write(
400
+ """
401
+ CAPTION:
402
+ ADCP assigns missing values based on thresholds
403
+ set before deployment. These values cannot be
404
+ recovered and are part of default mask file.
405
+ """
406
+ )
407
+ fillplot_plotly(st.session_state.orig_mask, colorscale="greys")
408
+ with rightplot:
409
+ st.subheader("Updated Mask File")
410
+ # values, counts = np.unique(mask, return_counts=True)
411
+ st.write(
412
+ """
413
+ CAPTION:
414
+ Updated mask displayed after applying threshold.
415
+ If thresholds are not saved, default mask
416
+ is displayed.
417
+ """
418
+ )
419
+ fillplot_plotly(st.session_state.qc_mask_temp, colorscale="greys")
420
+
421
+ with tab4:
422
+ ################## Fix Orientation ###################
423
+ st.subheader("Fix Orientation", divider="orange")
424
+
425
+ if st.session_state.beam_direction == "Up":
426
+ beamalt = "Down"
427
+ else:
428
+ beamalt = "Up"
429
+ st.write(
430
+ f"The current orientation of ADCP is `{st.session_state.beam_direction}`. Use the below option to correct the orientation."
431
+ )
432
+
433
+ beamdir_select = st.radio(f"Change orientation to {beamalt}", ["No", "Yes"])
434
+ if beamdir_select == "Yes":
435
+ st.session_state.beam_direction_QCT = beamalt
436
+ st.session_state.isBeamModified_QCT = True
437
+ st.write(f"The orientation changed to `{st.session_state.beam_direction_QCT}`")
438
+
439
+ with tab5:
440
+ ################## Save Button #############
441
+ st.header("Save Data", divider="blue")
442
+ col1, col2 = st.columns([1, 1])
443
+ with col1:
444
+ save_mask_button = st.button(label="Save Mask Data", on_click=save_qctest)
445
+
446
+ if save_mask_button:
447
+ # st.session_state.qc_mask_temp = mask
448
+ st.success("Mask file saved")
449
+ # Table summarizing changes
450
+ changes_summary = pd.DataFrame(
451
+ [
452
+ [
453
+ "Quality Control Tests",
454
+ "True" if st.session_state.isQCCheck_QCT else "False",
455
+ ],
456
+ ["Fix Orientation", st.session_state.beam_direction_QCT],
457
+ ],
458
+ columns=["Test", "Status"],
459
+ )
460
+
461
+ # Define a mapping function for styling
462
+ def status_color_map(value):
463
+ if value == "True":
464
+ return "background-color: green; color: white"
465
+ elif value == "False":
466
+ return "background-color: red; color: white"
467
+ elif value == "Up":
468
+ return "background-color: blue; color: white"
469
+ elif value == "Down":
470
+ return "background-color: orange; color: white"
471
+ else:
472
+ return ""
473
+
474
+ # Apply styles using Styler.apply
475
+ styled_table = changes_summary.style.set_properties(
476
+ **{"text-align": "center"}
477
+ )
478
+ styled_table = styled_table.map(status_color_map, subset=["Status"])
479
+
480
+ # Display the styled table
481
+ st.write(styled_table.to_html(), unsafe_allow_html=True)
482
+ else:
483
+ st.warning("Mask data not saved")
484
+ with col2:
485
+ reset_mask_button = st.button("Reset mask Data", on_click=reset_qctest)
486
+ if reset_mask_button:
487
+ # st.session_state.qc_mask_temp = np.copy(st.session_state.orig_mask)
488
+ # st.session_state.isQCCheck_QCT = False
489
+ # st.session_state.isQCTest = False
490
+ # st.session_state.isGrid = False
491
+ # st.session_state.isProfileMask = False
492
+ # st.session_state.isVelocityMask = False
493
+ st.success("Mask data is reset to default")