pyadps 0.2.1b0__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.
- pyadps/Home_Page.py +11 -5
- pyadps/pages/01_Read_File.py +623 -215
- pyadps/pages/02_View_Raw_Data.py +97 -41
- pyadps/pages/03_Download_Raw_File.py +200 -67
- pyadps/pages/04_Sensor_Health.py +905 -0
- pyadps/pages/05_QC_Test.py +493 -0
- pyadps/pages/06_Profile_Test.py +971 -0
- pyadps/pages/07_Velocity_Test.py +600 -0
- pyadps/pages/08_Write_File.py +623 -0
- pyadps/pages/09_Add-Ons.py +168 -0
- pyadps/utils/__init__.py +5 -3
- pyadps/utils/autoprocess.py +371 -80
- pyadps/utils/logging_utils.py +269 -0
- pyadps/utils/metadata/config.ini +22 -4
- pyadps/utils/metadata/demo.000 +0 -0
- pyadps/utils/metadata/flmeta.json +420 -420
- pyadps/utils/metadata/vlmeta.json +611 -565
- pyadps/utils/multifile.py +292 -0
- pyadps/utils/plotgen.py +505 -3
- pyadps/utils/profile_test.py +720 -125
- pyadps/utils/pyreadrdi.py +164 -92
- pyadps/utils/readrdi.py +436 -186
- pyadps/utils/script.py +197 -147
- pyadps/utils/sensor_health.py +120 -0
- pyadps/utils/signal_quality.py +472 -68
- pyadps/utils/velocity_test.py +79 -31
- pyadps/utils/writenc.py +222 -39
- {pyadps-0.2.1b0.dist-info → pyadps-0.3.0.dist-info}/METADATA +13 -14
- pyadps-0.3.0.dist-info/RECORD +35 -0
- {pyadps-0.2.1b0.dist-info → pyadps-0.3.0.dist-info}/WHEEL +1 -1
- {pyadps-0.2.1b0.dist-info → pyadps-0.3.0.dist-info}/entry_points.txt +1 -0
- 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/pages/07_Write_File.py +0 -452
- 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.0.dist-info}/LICENSE +0 -0
pyadps/utils/readrdi.py
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
"""
|
4
4
|
RDI ADCP Binary File Reader
|
5
5
|
===========================
|
6
|
-
This module provides classes and functions to read and extract data from RDI Acoustic Doppler
|
7
|
-
Current Profiler (ADCP) binary files. The module supports Workhorse, Ocean Surveyor, and DVS ADCPs.
|
8
|
-
It allows for parsing of various data types such as Fixed Leader, Variable Leader, Velocity, Correlation,
|
6
|
+
This module provides classes and functions to read and extract data from RDI Acoustic Doppler
|
7
|
+
Current Profiler (ADCP) binary files. The module supports Workhorse, Ocean Surveyor, and DVS ADCPs.
|
8
|
+
It allows for parsing of various data types such as Fixed Leader, Variable Leader, Velocity, Correlation,
|
9
9
|
Echo Intensity, Percent Good, and Status data.
|
10
10
|
|
11
11
|
Classes
|
@@ -28,7 +28,7 @@ Status
|
|
28
28
|
Parses the status data from the ADCP.
|
29
29
|
ReadFile
|
30
30
|
Manages the entire data extraction process and unifies all data types.
|
31
|
-
|
31
|
+
|
32
32
|
Functions
|
33
33
|
---------
|
34
34
|
check_equal(array)
|
@@ -42,11 +42,11 @@ Creation Date
|
|
42
42
|
|
43
43
|
Last Modified Date
|
44
44
|
--------------
|
45
|
-
|
45
|
+
2025-08-14
|
46
46
|
|
47
47
|
Version
|
48
48
|
-------
|
49
|
-
0.
|
49
|
+
0.4.0
|
50
50
|
|
51
51
|
Author
|
52
52
|
------
|
@@ -101,7 +101,10 @@ import os
|
|
101
101
|
import sys
|
102
102
|
|
103
103
|
import numpy as np
|
104
|
+
import pandas as pd
|
105
|
+
from collections import Counter
|
104
106
|
from pyadps.utils import pyreadrdi
|
107
|
+
from pyadps.utils.pyreadrdi import bcolors
|
105
108
|
|
106
109
|
|
107
110
|
class DotDict:
|
@@ -129,9 +132,7 @@ class DotDict:
|
|
129
132
|
# with open(json_file_path, "r") as file:
|
130
133
|
# dictionary = json.load(file)
|
131
134
|
else:
|
132
|
-
dictionary =
|
133
|
-
{}
|
134
|
-
) # Initialize an empty dictionary if no JSON file is found
|
135
|
+
dictionary = {} # Initialize an empty dictionary if no JSON file is found
|
135
136
|
self._initialize_from_dict(dictionary)
|
136
137
|
|
137
138
|
def _initialize_from_dict(self, dictionary):
|
@@ -386,79 +387,6 @@ class FileHeader:
|
|
386
387
|
|
387
388
|
|
388
389
|
# FIXED LEADER CODES #
|
389
|
-
|
390
|
-
|
391
|
-
def flead_dict(fid, dim=2):
|
392
|
-
"""
|
393
|
-
Extracts Fixed Leader data from a file and assigns it a identifiable name.
|
394
|
-
|
395
|
-
Parameters
|
396
|
-
----------
|
397
|
-
fid : file object or array-like
|
398
|
-
The data source to extract Fixed Leader information from.
|
399
|
-
dim : int, optional
|
400
|
-
The dimension of the data, by default 2.
|
401
|
-
|
402
|
-
Returns
|
403
|
-
-------
|
404
|
-
dict
|
405
|
-
A dictionary containing Fixed Leader field and data.
|
406
|
-
"""
|
407
|
-
|
408
|
-
fname = {
|
409
|
-
"CPU Version": "int64",
|
410
|
-
"CPU Revision": "int64",
|
411
|
-
"System Config Code": "int64",
|
412
|
-
"Real Flag": "int64",
|
413
|
-
"Lag Length": "int64",
|
414
|
-
"Beams": "int64",
|
415
|
-
"Cells": "int64",
|
416
|
-
"Pings": "int64",
|
417
|
-
"Depth Cell Len": "int64",
|
418
|
-
"Blank Transmit": "int64",
|
419
|
-
"Signal Mode": "int64",
|
420
|
-
"Correlation Thresh": "int64",
|
421
|
-
"Code Reps": "int64",
|
422
|
-
"Percent Good Min": "int64",
|
423
|
-
"Error Velocity Thresh": "int64",
|
424
|
-
"TP Minute": "int64",
|
425
|
-
"TP Second": "int64",
|
426
|
-
"TP Hundredth": "int64",
|
427
|
-
"Coord Transform Code": "int64",
|
428
|
-
"Head Alignment": "int64",
|
429
|
-
"Head Bias": "int64",
|
430
|
-
"Sensor Source Code": "int64",
|
431
|
-
"Sensor Avail Code": "int64",
|
432
|
-
"Bin 1 Dist": "int64",
|
433
|
-
"Xmit Pulse Len": "int64",
|
434
|
-
"Ref Layer Avg": "int64",
|
435
|
-
"False Target Thresh": "int64",
|
436
|
-
"Spare 1": "int64",
|
437
|
-
"Transmit Lag Dist": "int64",
|
438
|
-
"CPU Serial No": "int64",
|
439
|
-
"System Bandwidth": "int64",
|
440
|
-
"System Power": "int64",
|
441
|
-
"Spare 2": "int64",
|
442
|
-
"Instrument No": "int64",
|
443
|
-
"Beam Angle": "int64",
|
444
|
-
}
|
445
|
-
|
446
|
-
flead = dict()
|
447
|
-
counter = 1
|
448
|
-
for key, value in fname.items():
|
449
|
-
if dim == 2:
|
450
|
-
flead[key] = getattr(np, value)(fid[:][counter])
|
451
|
-
elif dim == 1:
|
452
|
-
flead[key] = getattr(np, value)(fid[counter])
|
453
|
-
else:
|
454
|
-
print("ERROR: Higher dimensions not allowed")
|
455
|
-
sys.exit()
|
456
|
-
|
457
|
-
counter += 1
|
458
|
-
|
459
|
-
return flead
|
460
|
-
|
461
|
-
|
462
390
|
class FixedLeader:
|
463
391
|
"""
|
464
392
|
The class extracts Fixed Leader data from RDI File.
|
@@ -505,7 +433,8 @@ class FixedLeader:
|
|
505
433
|
)
|
506
434
|
self.warning = pyreadrdi.ErrorCode.get_message(self.error)
|
507
435
|
|
508
|
-
self.
|
436
|
+
# self.data = self.data.astype(np.uint64)
|
437
|
+
self.fleader = dict()
|
509
438
|
self._initialize_from_dict(DotDict(json_file_path="flmeta.json"))
|
510
439
|
|
511
440
|
def _initialize_from_dict(self, dotdict):
|
@@ -520,7 +449,9 @@ class FixedLeader:
|
|
520
449
|
i = 1
|
521
450
|
for key, value in dotdict.__dict__.items():
|
522
451
|
setattr(self, key, value)
|
523
|
-
|
452
|
+
data_slice = self.data[i]
|
453
|
+
setattr(getattr(self, key), "data", data_slice)
|
454
|
+
self.fleader[value.long_name] = data_slice
|
524
455
|
i = i + 1
|
525
456
|
|
526
457
|
def field(self, ens=0):
|
@@ -538,8 +469,7 @@ class FixedLeader:
|
|
538
469
|
A dictionary of Fixed Leader data for the specified ensemble.
|
539
470
|
"""
|
540
471
|
|
541
|
-
|
542
|
-
return flead_dict(f1[:, ens], dim=1)
|
472
|
+
return {key: value[ens] for key, value in self.fleader.items()}
|
543
473
|
|
544
474
|
def is_uniform(self):
|
545
475
|
"""
|
@@ -555,14 +485,15 @@ class FixedLeader:
|
|
555
485
|
output[key] = check_equal(value)
|
556
486
|
return output
|
557
487
|
|
558
|
-
def system_configuration(self, ens
|
488
|
+
def system_configuration(self, ens=-1):
|
559
489
|
"""
|
560
490
|
Extracts and interprets the system configuration from the Fixed Leader data.
|
561
491
|
|
562
492
|
Parameters
|
563
493
|
----------
|
564
494
|
ens : int, optional
|
565
|
-
Ensemble number to extract system configuration for
|
495
|
+
Ensemble number to extract system configuration for.
|
496
|
+
Use -1 to extract the most common configuration (default).
|
566
497
|
|
567
498
|
Returns
|
568
499
|
-------
|
@@ -586,7 +517,19 @@ class FixedLeader:
|
|
586
517
|
"5 Beam CFIG 2 DEMOD"]
|
587
518
|
"""
|
588
519
|
|
589
|
-
|
520
|
+
if ens == -1:
|
521
|
+
syscode = self.fleader["System Config Code"]
|
522
|
+
most_common_syscode = Counter(syscode).most_common()[0][0]
|
523
|
+
binary_bits = format(int(most_common_syscode), "016b")
|
524
|
+
elif ens > -1:
|
525
|
+
binary_bits = format(int(self.fleader["System Config Code"][ens]), "016b")
|
526
|
+
else:
|
527
|
+
raise ValueError(
|
528
|
+
bcolors.FAIL
|
529
|
+
+ "Ensemble number should be greater than or equal to -1"
|
530
|
+
+ bcolors.ENDC
|
531
|
+
)
|
532
|
+
|
590
533
|
# convert integer to binary format
|
591
534
|
# In '016b': 0 adds extra zeros to the binary string
|
592
535
|
# : 16 is the total number of binary bits
|
@@ -672,7 +615,7 @@ class FixedLeader:
|
|
672
615
|
A dictionary of coordinate transformation details.
|
673
616
|
"""
|
674
617
|
|
675
|
-
bit_group = format(self.fleader["Coord Transform Code"][ens], "08b")
|
618
|
+
bit_group = format(int(self.fleader["Coord Transform Code"][ens]), "08b")
|
676
619
|
transform = dict()
|
677
620
|
|
678
621
|
trans_code = {
|
@@ -709,9 +652,9 @@ class FixedLeader:
|
|
709
652
|
|
710
653
|
"""
|
711
654
|
if field == "source":
|
712
|
-
bit_group = format(self.fleader["Sensor Source Code"][ens], "08b")
|
655
|
+
bit_group = format(int(self.fleader["Sensor Source Code"][ens]), "08b")
|
713
656
|
elif field == "avail":
|
714
|
-
bit_group = format(self.fleader["Sensor Avail Code"][ens], "08b")
|
657
|
+
bit_group = format(int(self.fleader["Sensor Avail Code"][ens]), "08b")
|
715
658
|
else:
|
716
659
|
sys.exit("ERROR (function ez_sensor): Enter valid argument.")
|
717
660
|
|
@@ -731,82 +674,6 @@ class FixedLeader:
|
|
731
674
|
|
732
675
|
|
733
676
|
# VARIABLE LEADER CODES #
|
734
|
-
def vlead_dict(vid):
|
735
|
-
"""
|
736
|
-
Extracts Variable Leader data from a file and assigns it a identifiable name.
|
737
|
-
|
738
|
-
Parameters
|
739
|
-
----------
|
740
|
-
fid : file object or array-like
|
741
|
-
The data source to extract Fixed Leader information from.
|
742
|
-
|
743
|
-
Returns
|
744
|
-
-------
|
745
|
-
dict
|
746
|
-
A dictionary containing Variable Leader field and data.
|
747
|
-
"""
|
748
|
-
|
749
|
-
vname = {
|
750
|
-
"RDI Ensemble": "int16",
|
751
|
-
"RTC Year": "int16",
|
752
|
-
"RTC Month": "int16",
|
753
|
-
"RTC Day": "int16",
|
754
|
-
"RTC Hour": "int16",
|
755
|
-
"RTC Minute": "int16",
|
756
|
-
"RTC Second": "int16",
|
757
|
-
"RTC Hundredth": "int16",
|
758
|
-
"Ensemble MSB": "int16",
|
759
|
-
"Bit Result": "int16",
|
760
|
-
"Speed of Sound": "int16",
|
761
|
-
"Depth of Transducer": "int16",
|
762
|
-
"Heading": "int32",
|
763
|
-
"Pitch": "int16",
|
764
|
-
"Roll": "int16",
|
765
|
-
"Salinity": "int16",
|
766
|
-
"Temperature": "int16",
|
767
|
-
"MPT Minute": "int16",
|
768
|
-
"MPT Second": "int16",
|
769
|
-
"MPT Hundredth": "int16",
|
770
|
-
"Hdg Std Dev": "int16",
|
771
|
-
"Pitch Std Dev": "int16",
|
772
|
-
"Roll Std Dev": "int16",
|
773
|
-
"ADC Channel 0": "int16",
|
774
|
-
"ADC Channel 1": "int16",
|
775
|
-
"ADC Channel 2": "int16",
|
776
|
-
"ADC Channel 3": "int16",
|
777
|
-
"ADC Channel 4": "int16",
|
778
|
-
"ADC Channel 5": "int16",
|
779
|
-
"ADC Channel 6": "int16",
|
780
|
-
"ADC Channel 7": "int16",
|
781
|
-
"Error Status Word 1": "int16",
|
782
|
-
"Error Status Word 2": "int16",
|
783
|
-
"Error Status Word 3": "int16",
|
784
|
-
"Error Status Word 4": "int16",
|
785
|
-
"Reserved": "int16",
|
786
|
-
"Pressure": "int32",
|
787
|
-
"Pressure Variance": "int32",
|
788
|
-
"Spare": "int16",
|
789
|
-
"Y2K Century": "int16",
|
790
|
-
"Y2K Year": "int16",
|
791
|
-
"Y2K Month": "int16",
|
792
|
-
"Y2K Day": "int16",
|
793
|
-
"Y2K Hour": "int16",
|
794
|
-
"Y2K Minute": "int16",
|
795
|
-
"Y2K Second": "int16",
|
796
|
-
"Y2K Hundredth": "int16",
|
797
|
-
}
|
798
|
-
|
799
|
-
vlead = dict()
|
800
|
-
|
801
|
-
counter = 1
|
802
|
-
for key, value in vname.items():
|
803
|
-
# vlead[key] = getattr(np, value)(vid[:][counter])
|
804
|
-
vlead[key] = vid[:][counter]
|
805
|
-
counter += 1
|
806
|
-
|
807
|
-
return vlead
|
808
|
-
|
809
|
-
|
810
677
|
class VariableLeader:
|
811
678
|
"""
|
812
679
|
The class extracts Variable Leader Data.
|
@@ -859,8 +726,7 @@ class VariableLeader:
|
|
859
726
|
)
|
860
727
|
self.warning = pyreadrdi.ErrorCode.get_message(self.error)
|
861
728
|
|
862
|
-
|
863
|
-
self.vleader = vlead_dict(self.data)
|
729
|
+
self.vleader = dict()
|
864
730
|
self._initialize_from_dict(DotDict(json_file_path="vlmeta.json"))
|
865
731
|
|
866
732
|
def _initialize_from_dict(self, dotdict):
|
@@ -875,10 +741,12 @@ class VariableLeader:
|
|
875
741
|
i = 1
|
876
742
|
for key, value in dotdict.__dict__.items():
|
877
743
|
setattr(self, key, value)
|
878
|
-
|
744
|
+
data_slice = self.data[i]
|
745
|
+
setattr(getattr(self, key), "data", data_slice)
|
746
|
+
self.vleader[value.long_name] = data_slice
|
879
747
|
i = i + 1
|
880
748
|
|
881
|
-
def
|
749
|
+
def bitresult(self):
|
882
750
|
"""
|
883
751
|
Extracts Bit Results from Variable Leader (Byte 13 & 14)
|
884
752
|
This field is part of the WorkHorse ADCP’s Built-in Test function.
|
@@ -914,7 +782,7 @@ class VariableLeader:
|
|
914
782
|
test_field[key] = np.array([], dtype=value)
|
915
783
|
|
916
784
|
for item in bit_array:
|
917
|
-
bit_group = format(item, "016b")
|
785
|
+
bit_group = format(int(item), "016b")
|
918
786
|
bitpos = 8
|
919
787
|
for key, value in tfname.items():
|
920
788
|
bitappend = getattr(np, value)(bit_group[bitpos])
|
@@ -957,8 +825,6 @@ class VariableLeader:
|
|
957
825
|
|
958
826
|
scale_factor = scale_list.get(fixclass["Frequency"])
|
959
827
|
|
960
|
-
print(fixclass["Frequency"])
|
961
|
-
|
962
828
|
channel["Xmit Voltage"] = adc1 * (scale_factor[0] / 1000000)
|
963
829
|
|
964
830
|
channel["Xmit Current"] = adc0 * (scale_factor[1] / 1000000)
|
@@ -975,6 +841,84 @@ class VariableLeader:
|
|
975
841
|
|
976
842
|
return channel
|
977
843
|
|
844
|
+
def error_status_word(self, esw=1):
|
845
|
+
bitset1 = (
|
846
|
+
"Bus Error exception",
|
847
|
+
"Address Error exception",
|
848
|
+
"Zero Divide exception",
|
849
|
+
"Emulator exception",
|
850
|
+
"Unassigned exception",
|
851
|
+
"Watchdog restart occurred",
|
852
|
+
"Batter Saver Power",
|
853
|
+
)
|
854
|
+
|
855
|
+
bitset2 = (
|
856
|
+
"Pinging",
|
857
|
+
"Not Used 1",
|
858
|
+
"Not Used 2",
|
859
|
+
"Not Used 3",
|
860
|
+
"Not Used 4",
|
861
|
+
"Not Used 5",
|
862
|
+
"Cold Wakeup occured",
|
863
|
+
"Unknown Wakeup occured",
|
864
|
+
)
|
865
|
+
|
866
|
+
bitset3 = (
|
867
|
+
"Clock Read error occured",
|
868
|
+
"Unexpected alarm",
|
869
|
+
"Clock jump forward",
|
870
|
+
"Clock jump backward",
|
871
|
+
"Not Used 6",
|
872
|
+
"Not Used 7",
|
873
|
+
"Not Used 8",
|
874
|
+
"Not Used 9",
|
875
|
+
)
|
876
|
+
|
877
|
+
bitset4 = (
|
878
|
+
"Not Used 10",
|
879
|
+
"Not Used 11",
|
880
|
+
"Not Used 12",
|
881
|
+
"Power Fail Unrecorded",
|
882
|
+
"Spurious level 4 intr DSP",
|
883
|
+
"Spurious level 5 intr UART",
|
884
|
+
"Spurious level 6 intr CLOCK",
|
885
|
+
"Level 7 interrup occured",
|
886
|
+
)
|
887
|
+
|
888
|
+
if esw == 1:
|
889
|
+
bitset = bitset1
|
890
|
+
errorarray = self.vleader["Error Status Word 1"]
|
891
|
+
elif esw == 2:
|
892
|
+
bitset = bitset2
|
893
|
+
errorarray = self.vleader["Error Status Word 2"]
|
894
|
+
elif esw == 3:
|
895
|
+
bitset = bitset3
|
896
|
+
errorarray = self.vleader["Error Status Word 3"]
|
897
|
+
else:
|
898
|
+
bitset = bitset4
|
899
|
+
errorarray = self.vleader["Error Status Word 4"]
|
900
|
+
|
901
|
+
errorstatus = dict()
|
902
|
+
# bitarray = np.zeros(32, dtype='str')
|
903
|
+
|
904
|
+
for item in bitset:
|
905
|
+
errorstatus[item] = np.array([])
|
906
|
+
|
907
|
+
for data in errorarray:
|
908
|
+
if data != -32768:
|
909
|
+
byte_split = format(int(data), "08b")
|
910
|
+
bitposition = 0
|
911
|
+
for item in bitset:
|
912
|
+
errorstatus[item] = np.append(
|
913
|
+
errorstatus[item], int(byte_split[bitposition])
|
914
|
+
)
|
915
|
+
bitposition += 1
|
916
|
+
else:
|
917
|
+
for item in bitset:
|
918
|
+
errorstatus[item] = np.append(errorstatus[item], 0)
|
919
|
+
|
920
|
+
return errorstatus
|
921
|
+
|
978
922
|
|
979
923
|
class Velocity:
|
980
924
|
"""
|
@@ -1028,7 +972,7 @@ class Velocity:
|
|
1028
972
|
self.cells = cell
|
1029
973
|
self.beams = beam
|
1030
974
|
|
1031
|
-
self.
|
975
|
+
self.unit = "mm/s"
|
1032
976
|
self.missing_value = "-32768"
|
1033
977
|
self.scale_factor = 1
|
1034
978
|
self.valid_min = -32768
|
@@ -1087,7 +1031,7 @@ class Correlation:
|
|
1087
1031
|
self.cells = cell
|
1088
1032
|
self.beams = beam
|
1089
1033
|
|
1090
|
-
self.
|
1034
|
+
self.unit = ""
|
1091
1035
|
self.scale_factor = 1
|
1092
1036
|
self.valid_min = 0
|
1093
1037
|
self.valid_max = 255
|
@@ -1146,7 +1090,7 @@ class Echo:
|
|
1146
1090
|
self.cells = cell
|
1147
1091
|
self.beams = beam
|
1148
1092
|
|
1149
|
-
self.
|
1093
|
+
self.unit = "counts"
|
1150
1094
|
self.scale_factor = "0.45"
|
1151
1095
|
self.valid_min = 0
|
1152
1096
|
self.valid_max = 255
|
@@ -1205,7 +1149,7 @@ class PercentGood:
|
|
1205
1149
|
self.cells = cell
|
1206
1150
|
self.beams = beam
|
1207
1151
|
|
1208
|
-
self.
|
1152
|
+
self.unit = "percent"
|
1209
1153
|
self.valid_min = 0
|
1210
1154
|
self.valid_max = 100
|
1211
1155
|
self.long_name = "Percent Good"
|
@@ -1263,7 +1207,7 @@ class Status:
|
|
1263
1207
|
self.cells = cell
|
1264
1208
|
self.beams = beam
|
1265
1209
|
|
1266
|
-
self.
|
1210
|
+
self.unit = ""
|
1267
1211
|
self.valid_min = 0
|
1268
1212
|
self.valid_max = 1
|
1269
1213
|
self.long_name = "Status Data Format"
|
@@ -1280,7 +1224,7 @@ class ReadFile:
|
|
1280
1224
|
The RDI ADCP binary file to be read.
|
1281
1225
|
"""
|
1282
1226
|
|
1283
|
-
def __init__(self, filename):
|
1227
|
+
def __init__(self, filename, is_fix_ensemble=True, fix_time=False):
|
1284
1228
|
"""
|
1285
1229
|
Initializes the ReadFile object and extracts data from the RDI ADCP binary file.
|
1286
1230
|
"""
|
@@ -1305,8 +1249,8 @@ class ReadFile:
|
|
1305
1249
|
error_array["Fixed Leader"] = self.fixedleader.error
|
1306
1250
|
warning_array["Fixed Leader"] = self.fixedleader.warning
|
1307
1251
|
ensemble_array["Fixed Leader"] = self.fixedleader.ensembles
|
1308
|
-
cells = self.fixedleader.fleader["Cells"][0]
|
1309
|
-
beams = self.fixedleader.fleader["Beams"][0]
|
1252
|
+
cells = int(self.fixedleader.fleader["Cells"][0])
|
1253
|
+
beams = int(self.fixedleader.fleader["Beams"][0])
|
1310
1254
|
ensemble = self.fixedleader.ensembles
|
1311
1255
|
|
1312
1256
|
self.variableleader = VariableLeader(
|
@@ -1324,8 +1268,8 @@ class ReadFile:
|
|
1324
1268
|
if "Velocity" in datatype_array:
|
1325
1269
|
self.velocity = Velocity(
|
1326
1270
|
filename,
|
1327
|
-
cell=cells,
|
1328
|
-
beam=beams,
|
1271
|
+
cell=int(cells),
|
1272
|
+
beam=int(beams),
|
1329
1273
|
byteskip=byteskip,
|
1330
1274
|
offset=offset,
|
1331
1275
|
idarray=idarray,
|
@@ -1391,6 +1335,48 @@ class ReadFile:
|
|
1391
1335
|
warning_array["Status"] = self.status.warning
|
1392
1336
|
ensemble_array["Status"] = self.status.ensembles
|
1393
1337
|
|
1338
|
+
# Add Time Axis
|
1339
|
+
year = self.variableleader.vleader["RTC Year"]
|
1340
|
+
month = self.variableleader.vleader["RTC Month"]
|
1341
|
+
day = self.variableleader.vleader["RTC Day"]
|
1342
|
+
hour = self.variableleader.vleader["RTC Hour"]
|
1343
|
+
minute = self.variableleader.vleader["RTC Minute"]
|
1344
|
+
second = self.variableleader.vleader["RTC Second"]
|
1345
|
+
year = year + 2000
|
1346
|
+
date_df = pd.DataFrame(
|
1347
|
+
{
|
1348
|
+
"year": year,
|
1349
|
+
"month": month,
|
1350
|
+
"day": day,
|
1351
|
+
"hour": hour,
|
1352
|
+
"minute": minute,
|
1353
|
+
"second": second,
|
1354
|
+
}
|
1355
|
+
)
|
1356
|
+
self.time = pd.to_datetime(date_df)
|
1357
|
+
self.isTimeRegular = self.is_time_regular()
|
1358
|
+
|
1359
|
+
# Depth
|
1360
|
+
# Create a depth axis with mean depth in 'm'
|
1361
|
+
cell1 = self.fixedleader.field()["Cells"]
|
1362
|
+
bin1dist1 = self.fixedleader.field()["Bin 1 Dist"] / 100
|
1363
|
+
depth_cell_len1 = self.fixedleader.field()["Depth Cell Len"] / 100
|
1364
|
+
beam_direction1 = self.fixedleader.system_configuration()["Beam Direction"]
|
1365
|
+
mean_depth = np.mean(self.variableleader.vleader["Depth of Transducer"]) / 10
|
1366
|
+
mean_depth = np.trunc(mean_depth)
|
1367
|
+
if beam_direction1.lower() == "up":
|
1368
|
+
sgn = -1
|
1369
|
+
else:
|
1370
|
+
sgn = 1
|
1371
|
+
first_depth = mean_depth + sgn * bin1dist1
|
1372
|
+
last_depth = first_depth + sgn * cell1 * depth_cell_len1
|
1373
|
+
z = np.arange(first_depth, last_depth, sgn * depth_cell_len1)
|
1374
|
+
self.depth = z
|
1375
|
+
|
1376
|
+
# Add all attributes/method/data from FixedLeader and VariableLeader
|
1377
|
+
self._copy_attributes_from_var()
|
1378
|
+
|
1379
|
+
# Error Codes and Warnings
|
1394
1380
|
self.error_codes = error_array
|
1395
1381
|
self.warnings = warning_array
|
1396
1382
|
self.ensemble_array = ensemble_array
|
@@ -1406,6 +1392,256 @@ class ReadFile:
|
|
1406
1392
|
else:
|
1407
1393
|
self.isWarning = True
|
1408
1394
|
|
1395
|
+
# Add additional attributes
|
1396
|
+
# Ensemble
|
1397
|
+
dtens = self.ensemble_value_array
|
1398
|
+
minens = np.min(dtens)
|
1399
|
+
self.ensembles = minens
|
1400
|
+
|
1401
|
+
# Add attribute that lists all variables/functions
|
1402
|
+
self.list_vars = list(vars(self).keys())
|
1403
|
+
|
1404
|
+
# By default, fix the time axis if requested
|
1405
|
+
if fix_time:
|
1406
|
+
self.fix_time_axis()
|
1407
|
+
|
1408
|
+
# By default fix ensemble
|
1409
|
+
if is_fix_ensemble and not self.isEnsembleEqual:
|
1410
|
+
self.fixensemble()
|
1411
|
+
|
1412
|
+
def _copy_attributes_from_var(self):
|
1413
|
+
for attr_name, attr_value in self.variableleader.__dict__.items():
|
1414
|
+
# Copy each attribute of var into self
|
1415
|
+
setattr(self, attr_name, attr_value)
|
1416
|
+
for attr_name, attr_value in self.fixedleader.__dict__.items():
|
1417
|
+
# Copy each attribute of var into self
|
1418
|
+
setattr(self, attr_name, attr_value)
|
1419
|
+
|
1420
|
+
def __getattr__(self, name):
|
1421
|
+
# Delegate attribute/method access to self.var if not found in self
|
1422
|
+
if hasattr(self.variableleader, name):
|
1423
|
+
return getattr(self.variableleader, name)
|
1424
|
+
if hasattr(self.fixedleader, name):
|
1425
|
+
return getattr(self.fixedleader, name)
|
1426
|
+
raise AttributeError(
|
1427
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
1428
|
+
)
|
1429
|
+
|
1430
|
+
# Add this method inside your ReadFile class
|
1431
|
+
|
1432
|
+
def get_time_component_frequency(self, component="minute"):
|
1433
|
+
"""
|
1434
|
+
Calculates the frequency of a specific time component (hour, minute, or second).
|
1435
|
+
|
1436
|
+
This is a diagnostic tool to help identify irregularities in the time axis.
|
1437
|
+
For example, for ideal hourly data, the minute and second frequencies
|
1438
|
+
should show a single entry (0) with a count equal to the number of ensembles.
|
1439
|
+
|
1440
|
+
Parameters
|
1441
|
+
----------
|
1442
|
+
component : str, optional
|
1443
|
+
The time component to analyze. Must be one of 'hour', 'minute',
|
1444
|
+
or 'second', by default "minute".
|
1445
|
+
|
1446
|
+
Returns
|
1447
|
+
-------
|
1448
|
+
pandas.Series
|
1449
|
+
A pandas Series with the time component values as the index and
|
1450
|
+
their frequency (count) as the values, sorted in descending order.
|
1451
|
+
"""
|
1452
|
+
if component not in ["hour", "minute", "second"]:
|
1453
|
+
print(
|
1454
|
+
bcolors.FAIL
|
1455
|
+
+ "Error: Component must be 'hour', 'minute', or 'second'."
|
1456
|
+
+ bcolors.ENDC
|
1457
|
+
)
|
1458
|
+
return
|
1459
|
+
|
1460
|
+
if component == "hour":
|
1461
|
+
return self.time.dt.hour.value_counts()
|
1462
|
+
elif component == "minute":
|
1463
|
+
return self.time.dt.minute.value_counts()
|
1464
|
+
else: # second
|
1465
|
+
return self.time.dt.second.value_counts()
|
1466
|
+
|
1467
|
+
# In readrdi.py, replace the snap_time_axis method inside the ReadFile class
|
1468
|
+
|
1469
|
+
def snap_time_axis(self, freq="h", tolerance="5min", target_minute=None):
|
1470
|
+
"""
|
1471
|
+
Rounds the time axis, returning a status and message for the UI.
|
1472
|
+
"""
|
1473
|
+
if target_minute is not None:
|
1474
|
+
action_msg = f"snapping to nearest minute '{target_minute}'"
|
1475
|
+
try:
|
1476
|
+
offset = pd.to_timedelta(target_minute, unit="m")
|
1477
|
+
snapped_time = (self.time - offset).dt.round("h") + offset
|
1478
|
+
except Exception as e:
|
1479
|
+
return False, f"Error during rounding: {e}"
|
1480
|
+
else:
|
1481
|
+
action_msg = f"snapping to nearest frequency '{freq}'"
|
1482
|
+
try:
|
1483
|
+
snapped_time = self.time.dt.round(freq)
|
1484
|
+
except Exception as e:
|
1485
|
+
return False, f"Error during rounding: {e}"
|
1486
|
+
|
1487
|
+
tolerance_delta = pd.to_timedelta(tolerance)
|
1488
|
+
|
1489
|
+
# Safety Check 1: Tolerance
|
1490
|
+
max_diff = (self.time - snapped_time).abs().max()
|
1491
|
+
if max_diff > tolerance_delta:
|
1492
|
+
message = f"Operation Aborted: Maximum required correction is {max_diff}, which exceeds the tolerance of {tolerance}."
|
1493
|
+
return False, message
|
1494
|
+
|
1495
|
+
# Safety Check 2: Duplicates
|
1496
|
+
if snapped_time.duplicated().any():
|
1497
|
+
message = "Operation Aborted: Snapping would create duplicate timestamps. The data may be too irregular or have large gaps."
|
1498
|
+
return False, message
|
1499
|
+
|
1500
|
+
# Success Case
|
1501
|
+
self.time = snapped_time
|
1502
|
+
self.isTimeRegular = self.is_time_regular()
|
1503
|
+
success_message = f"Successfully snapped time axis. Maximum correction applied was {max_diff}."
|
1504
|
+
return True, success_message
|
1505
|
+
|
1506
|
+
def is_time_regular(self, tolerance_s=1):
|
1507
|
+
"""
|
1508
|
+
Checks if the time axis has a regular, equal interval.
|
1509
|
+
|
1510
|
+
Args:
|
1511
|
+
tolerance_ns (int): Tolerance in nanoseconds to consider intervals equal.
|
1512
|
+
Defaults to 1 second.
|
1513
|
+
|
1514
|
+
Returns:
|
1515
|
+
bool: True if the time axis is regular, False otherwise.
|
1516
|
+
"""
|
1517
|
+
if len(self.time) < 2:
|
1518
|
+
return False # Not enough data to determine regularity
|
1519
|
+
|
1520
|
+
diffs = pd.Series(self.time).diff().dropna()
|
1521
|
+
if diffs.empty:
|
1522
|
+
return False
|
1523
|
+
|
1524
|
+
# Get the most common difference (mode)
|
1525
|
+
common_interval = diffs.mode()[0]
|
1526
|
+
|
1527
|
+
# Check if all differences are approximately equal to the common_interval
|
1528
|
+
max_deviation_ns = (diffs - common_interval).abs().max().total_seconds()
|
1529
|
+
|
1530
|
+
return max_deviation_ns <= tolerance_s
|
1531
|
+
|
1532
|
+
def get_time_interval(self):
|
1533
|
+
"""
|
1534
|
+
Calculates the most common time interval (frequency) in the data.
|
1535
|
+
|
1536
|
+
Returns:
|
1537
|
+
pd.Timedelta or None: The detected modal interval, or None if insufficient data.
|
1538
|
+
"""
|
1539
|
+
if len(self.time) < 2:
|
1540
|
+
return None
|
1541
|
+
|
1542
|
+
diffs = pd.Series(self.time).diff().dropna()
|
1543
|
+
if diffs.empty:
|
1544
|
+
return None
|
1545
|
+
|
1546
|
+
# Find the most common interval (mode)
|
1547
|
+
return diffs.mode()[0]
|
1548
|
+
|
1549
|
+
def get_time_interval_frequency(self):
|
1550
|
+
if len(self.time) < 2:
|
1551
|
+
return None
|
1552
|
+
|
1553
|
+
diffs = pd.Series(self.time).diff().dropna()
|
1554
|
+
if diffs.empty:
|
1555
|
+
return None
|
1556
|
+
all_diffs = diffs.tolist()
|
1557
|
+
frequency = Counter(all_diffs)
|
1558
|
+
# return dict(frequency)
|
1559
|
+
return {str(td): count for td, count in frequency.items()}
|
1560
|
+
|
1561
|
+
def fill_time_axis(self):
|
1562
|
+
"""
|
1563
|
+
Resamples all data to a regular time axis, filling gaps with missing values.
|
1564
|
+
Returns a status and message for the UI.
|
1565
|
+
"""
|
1566
|
+
# 1. Determine the expected frequency
|
1567
|
+
frequency = self.get_time_interval()
|
1568
|
+
if frequency is None:
|
1569
|
+
return (
|
1570
|
+
False,
|
1571
|
+
"Operation Aborted: Could not determine a regular time interval from the data.",
|
1572
|
+
)
|
1573
|
+
|
1574
|
+
# 2. Create the new, complete time index
|
1575
|
+
full_time_index = pd.date_range(
|
1576
|
+
start=self.time.min(), end=self.time.max(), freq=frequency
|
1577
|
+
)
|
1578
|
+
|
1579
|
+
if len(full_time_index) == len(self.time) and self.isTimeRegular:
|
1580
|
+
return (
|
1581
|
+
False,
|
1582
|
+
"No action taken: The time axis is already regular and has no gaps.",
|
1583
|
+
)
|
1584
|
+
|
1585
|
+
# --- Handle Fixed Leader Data ---
|
1586
|
+
fleader_df = pd.DataFrame(self.fixedleader.fleader, index=self.time)
|
1587
|
+
fleader_df_resampled = fleader_df.reindex(full_time_index)
|
1588
|
+
for key in fleader_df_resampled.columns:
|
1589
|
+
mode_value = fleader_df[key].mode().iloc[0]
|
1590
|
+
original_dtype = fleader_df[key].dtype
|
1591
|
+
fleader_df_resampled[key] = (
|
1592
|
+
fleader_df_resampled[key].fillna(mode_value).astype(original_dtype)
|
1593
|
+
)
|
1594
|
+
for key in self.fixedleader.fleader:
|
1595
|
+
self.fixedleader.fleader[key] = fleader_df_resampled[key].values
|
1596
|
+
|
1597
|
+
# --- Handle Variable Leader Data ---
|
1598
|
+
vleader_df = pd.DataFrame(self.variableleader.vleader, index=self.time)
|
1599
|
+
vleader_df_resampled = vleader_df.reindex(full_time_index)
|
1600
|
+
for key, attrs in self.variableleader.vleader.items():
|
1601
|
+
missing_val = getattr(attrs, "missing_value", -32768)
|
1602
|
+
self.variableleader.vleader[key] = (
|
1603
|
+
vleader_df_resampled[key].fillna(missing_val).values.astype(np.int64)
|
1604
|
+
)
|
1605
|
+
|
1606
|
+
# --- Handle 3D/4D data types ---
|
1607
|
+
missing_indices = np.where(full_time_index.isin(self.time) == False)[0]
|
1608
|
+
data_to_resize = {
|
1609
|
+
"velocity": -32768,
|
1610
|
+
"correlation": 255,
|
1611
|
+
"echo": 255,
|
1612
|
+
"percentgood": 255,
|
1613
|
+
"status": 0,
|
1614
|
+
}
|
1615
|
+
for name, missing_val in data_to_resize.items():
|
1616
|
+
if hasattr(self, name):
|
1617
|
+
obj = getattr(self, name)
|
1618
|
+
if np.issubdtype(obj.data.dtype, np.integer):
|
1619
|
+
new_data = np.insert(
|
1620
|
+
obj.data, missing_indices, missing_val, axis=-1
|
1621
|
+
)
|
1622
|
+
obj.data = new_data
|
1623
|
+
obj.ensembles = new_data.shape[-1]
|
1624
|
+
|
1625
|
+
# --- Update the main object's state ---
|
1626
|
+
self.time = pd.Series(full_time_index)
|
1627
|
+
self.ensembles = len(full_time_index)
|
1628
|
+
self.isTimeRegular = True
|
1629
|
+
|
1630
|
+
message = f"Successfully filled time axis gaps. Total ensembles are now {self.ensembles}."
|
1631
|
+
return True, message
|
1632
|
+
|
1633
|
+
def resize_fixedleader(self, newshape):
|
1634
|
+
for key in self.fixedleader.fleader:
|
1635
|
+
attr_name = key.lower().replace(" ", "_")
|
1636
|
+
attr_obj = getattr(self.fixedleader, attr_name)
|
1637
|
+
attr_obj.data = attr_obj.data[:newshape]
|
1638
|
+
|
1639
|
+
def resize_variableleader(self, newshape):
|
1640
|
+
for key in self.variableleader.vleader:
|
1641
|
+
attr_name = key.lower().replace(" ", "_")
|
1642
|
+
attr_obj = getattr(self.variableleader, attr_name)
|
1643
|
+
attr_obj.data = attr_obj.data[:newshape]
|
1644
|
+
|
1409
1645
|
def fixensemble(self, min_cutoff=0):
|
1410
1646
|
"""
|
1411
1647
|
Fixes the ensemble size across all data types in the file if they differ.
|
@@ -1436,10 +1672,18 @@ class ReadFile:
|
|
1436
1672
|
self.fileheader.dataid = self.fileheader.dataid[:minens, :]
|
1437
1673
|
if "Fixed Leader" in datatype_array:
|
1438
1674
|
self.fixedleader.data = self.fixedleader.data[:, :minens]
|
1675
|
+
self.fixedleader.fleader = {
|
1676
|
+
k: v[:minens] for k, v in self.fixedleader.fleader.items()
|
1677
|
+
}
|
1439
1678
|
self.fixedleader.ensembles = minens
|
1679
|
+
self.resize_fixedleader(minens)
|
1440
1680
|
if "Variable Leader" in datatype_array:
|
1441
1681
|
self.variableleader.data = self.variableleader.data[:, :minens]
|
1682
|
+
self.variableleader.vleader = {
|
1683
|
+
k: v[:minens] for k, v in self.variableleader.vleader.items()
|
1684
|
+
}
|
1442
1685
|
self.variableleader.ensembles = minens
|
1686
|
+
self.resize_variableleader(minens)
|
1443
1687
|
if "Velocity" in datatype_array:
|
1444
1688
|
self.velocity.data = self.velocity.data[:, :, :minens]
|
1445
1689
|
self.velocity.ensembles = minens
|
@@ -1455,7 +1699,13 @@ class ReadFile:
|
|
1455
1699
|
if "Status" in datatype_array:
|
1456
1700
|
self.status.data = self.status.data[:, :, :minens]
|
1457
1701
|
self.status.ensembles = minens
|
1458
|
-
|
1702
|
+
|
1703
|
+
self.time = self.time[:minens]
|
1704
|
+
print(
|
1705
|
+
bcolors.OKBLUE
|
1706
|
+
+ f"Ensembles fixed to {minens}. All data types have same ensembles."
|
1707
|
+
+ bcolors.ENDC
|
1708
|
+
)
|
1459
1709
|
else:
|
1460
1710
|
print(
|
1461
1711
|
"WARNING: No response was initiated. All data types have same ensemble."
|