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.
Files changed (39) hide show
  1. pyadps/Home_Page.py +11 -5
  2. pyadps/pages/01_Read_File.py +623 -215
  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.1b0.dist-info → pyadps-0.3.0.dist-info}/METADATA +13 -14
  29. pyadps-0.3.0.dist-info/RECORD +35 -0
  30. {pyadps-0.2.1b0.dist-info → pyadps-0.3.0.dist-info}/WHEEL +1 -1
  31. {pyadps-0.2.1b0.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.1b0.dist-info/RECORD +0 -31
  39. {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
- 2024-09-05
45
+ 2025-08-14
46
46
 
47
47
  Version
48
48
  -------
49
- 0.3.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.fleader = flead_dict(self.data)
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
- setattr(getattr(self, key), "data", self.data[i])
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
- f1 = np.array(self.data)
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=0):
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, by default 0.
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
- binary_bits = format(self.fleader["System Config Code"][ens], "016b")
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
- # self.vdict = DotDict()
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
- setattr(getattr(self, key), "data", self.data[i])
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 bit_result(self):
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.units = "mm/s"
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.units = ""
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.units = "counts"
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.units = "percent"
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.units = ""
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
- print(f"Ensembles fixed to {minens}. All data types have same ensembles.")
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."