pyadps 0.3.3b0__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.
@@ -0,0 +1,1610 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ RDI ADCP Binary File Reader
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,
9
+ Echo Intensity, Percent Good, and Status data.
10
+
11
+ Classes
12
+ -------
13
+ FileHeader
14
+ Extracts metadata from the ADCP file header.
15
+ FixedLeader
16
+ Handles the Fixed Leader Data that contains configuration and system settings.
17
+ VariableLeader
18
+ Extracts and processes the dynamic Variable Leader Data.
19
+ Velocity
20
+ Retrieves the velocity data from the ADCP file.
21
+ Correlation
22
+ Retrieves correlation data between ADCP beams.
23
+ Echo
24
+ Retrieves echo intensity data from the ADCP file.
25
+ PercentGood
26
+ Extracts the percentage of valid velocity data.
27
+ Status
28
+ Parses the status data from the ADCP.
29
+ ReadFile
30
+ Manages the entire data extraction process and unifies all data types.
31
+
32
+ Functions
33
+ ---------
34
+ check_equal(array)
35
+ Utility function to check if all values in an array are equal.
36
+ error_code(code)
37
+ Maps an error code to a human-readable message.
38
+
39
+ Creation Date
40
+ --------------
41
+ 2024-09-01
42
+
43
+ Last Modified Date
44
+ --------------
45
+ 2024-09-05
46
+
47
+ Version
48
+ -------
49
+ 0.3.0
50
+
51
+ Author
52
+ ------
53
+ [P. Amol] <your.email@example.com>
54
+
55
+ License
56
+ -------
57
+ This module is licensed under the MIT License. See LICENSE file for details.
58
+
59
+ Dependencies
60
+ ------------
61
+ - numpy : Used for array handling and mathematical operations.
62
+ - pyreadrdi : Python interface for reading RDI ADCP binary files (supports variableleader, fixedleader, etc.).
63
+ - sys : System-specific parameters and functions.
64
+ - DotDict : Utility class to handle dictionary-like objects with dot access.
65
+
66
+ Install the dependencies using:
67
+ pip install numpy
68
+
69
+ Usage
70
+ -----
71
+ Basic Example:
72
+ ```python
73
+ from readrdi import ReadFile
74
+
75
+ # Initialize the ReadFile class
76
+ adcp_obj = ReadFile('path_to_your_rdi_file.000')
77
+ velocity_data = adcp_obj.velocity.data
78
+ pressure_data = adcp_obj.variableleader.pressure.data
79
+
80
+ # Individual data types can be accessed without reading the entire file
81
+ # An example to access fixed leader data
82
+
83
+ import readrdi as rd
84
+
85
+ fixed_leader_obj = rd.FixedLeader('path_to_your_rdi_file.000')
86
+ isfixedleader_uniform_dict = fixed_leader_obj.is_uniform()
87
+
88
+ # Access velocity data
89
+ velocity_data = adcp_file.velocity.data
90
+
91
+ # Check for warnings and errors
92
+ if adcp_obj.isWarning:
93
+ print("Warning: Some errors were encountered during data extraction.")
94
+ else:
95
+ print("Data extracted successfully.")
96
+ """
97
+
98
+ import importlib.resources as pkg_resources
99
+ import json
100
+ import os
101
+ import sys
102
+
103
+ import numpy as np
104
+ import pandas as pd
105
+ from pyadps.utils import pyreadrdi
106
+
107
+
108
+ class DotDict:
109
+ """
110
+ A dictionary-like class that allows access to dictionary items as attributes.
111
+ If initialized with a dictionary, it converts it into attributes.
112
+ If initialized without a dictionary, it loads one from a JSON file or initializes an empty dictionary.
113
+
114
+ Parameters
115
+ ----------
116
+ dictionary : dict, optional
117
+ A dictionary to initialize the DotDict with. If not provided, a JSON file is loaded or an empty dictionary is created.
118
+ json_file_path : str, optional
119
+ Path to the JSON file to load the dictionary from, by default "data.json".
120
+ """
121
+
122
+ def __init__(self, dictionary=None, json_file_path="data.json"):
123
+ if dictionary is None:
124
+ if pkg_resources.is_resource("pyadps.utils.metadata", json_file_path):
125
+ with pkg_resources.open_text(
126
+ "pyadps.utils.metadata", json_file_path
127
+ ) as f:
128
+ dictionary = json.load(f)
129
+ # if os.path.exists(json_file_path):
130
+ # with open(json_file_path, "r") as file:
131
+ # dictionary = json.load(file)
132
+ else:
133
+ dictionary = {} # Initialize an empty dictionary if no JSON file is found
134
+ self._initialize_from_dict(dictionary)
135
+
136
+ def _initialize_from_dict(self, dictionary):
137
+ """
138
+ Recursively initializes DotDict attributes from a given dictionary.
139
+
140
+ Parameters
141
+ ----------
142
+ dictionary : dict
143
+ The dictionary to convert into attributes for the DotDict object.
144
+ """
145
+
146
+ for key, value in dictionary.items():
147
+ if isinstance(value, dict):
148
+ value = DotDict(value)
149
+ setattr(self, key, value)
150
+
151
+ def __getattr__(self, key):
152
+ """
153
+ Retrieves an attribute from the DotDict object by its key.
154
+
155
+ Parameters
156
+ ----------
157
+ key : str
158
+ The attribute key to retrieve.
159
+
160
+ Returns
161
+ -------
162
+ value : any
163
+ The value corresponding to the given key.
164
+ """
165
+ return self.__dict__.get(key)
166
+
167
+
168
+ def error_code(code):
169
+ if code == 0:
170
+ error_string = "Data type is healthy"
171
+ elif code == 1:
172
+ error_string = "End of file"
173
+ elif code == 2:
174
+ error_string = "File Corrupted (ID not recognized)"
175
+ elif code == 3:
176
+ error_string = "Wrong file type"
177
+ elif code == 4:
178
+ error_string = "Data type mismatch"
179
+ else:
180
+ error_string = "Unknown error"
181
+ return error_string
182
+
183
+
184
+ def check_equal(lst):
185
+ """
186
+ Checks if all elements in the list are equal.
187
+
188
+ Parameters
189
+ ----------
190
+ lst : list
191
+ A list of elements to check.
192
+
193
+ Returns
194
+ -------
195
+ bool
196
+ True if all elements in the list are equal, False otherwise.
197
+ """
198
+ return np.all(np.array(lst) == lst[0])
199
+
200
+
201
+ class FileHeader:
202
+ """
203
+ A class to handle the extraction and management of file header
204
+ data from an RDI ADCP binary data.
205
+
206
+ The `FileHeader` class uses the `fileheader` function (imported
207
+ from an external module) to extract various data sets from a
208
+ binary file. These data sets are assigned to instance variables within the class. The class provides methods
209
+ to perform operations on the extracted data, such as checking the file format and determining data types.
210
+ FileHeader class can be used for getting Header information from an
211
+ RDI ADCP File. The information can be accessed by the class instance
212
+ called 'header', which is a list.
213
+
214
+
215
+ Input:
216
+ ----------
217
+ rdi_file = TYPE STRING
218
+ RDI ADCP binary file. The class can currently extract Workhorse,
219
+ Ocean Surveyor, and DVS files.
220
+
221
+ Attributes:
222
+ -------------------
223
+ filename = TYPE CHARACTER
224
+ Returns the input filename
225
+ ensembles = TYPE INTEGER
226
+ Total number of ensembles in the file
227
+ header = DICTIONARY(STRING, LIST(INTEGER))
228
+ KEYS: 'Header ID', 'Source ID', 'Bytes', 'Spare',
229
+ 'Data Types', 'Address Offset'
230
+
231
+ Methods:
232
+ --------
233
+ datatypes(ens=0)
234
+ Lists out the data types for any one ensemble (default = 0)
235
+ check_file()
236
+ Checks if
237
+ 1. system file size and calculated file size are same
238
+ 2. no. of data types and no. of bytes are same for all ensembles
239
+
240
+ Example code to access the class
241
+ --------------------------------
242
+ myvar = FileHeader(rdi_file)
243
+ myvar.header['Header Id']
244
+ myvar.datatypes(ens=1)
245
+ myvar.check_file()
246
+
247
+ """
248
+
249
+ def __init__(self, rdi_file):
250
+ """
251
+ Initializes the FileHeader object, extracts header data, and assigns it to instance variables.
252
+
253
+ Parameters
254
+ ----------
255
+ rdi_file : str
256
+ The RDI ADCP binary file to extract data from.
257
+ """
258
+ self.filename = rdi_file
259
+ (
260
+ self.datatypes,
261
+ self.bytes,
262
+ self.byteskip,
263
+ self.address_offset,
264
+ self.dataid,
265
+ self.ensembles,
266
+ self.error,
267
+ ) = pyreadrdi.fileheader(rdi_file)
268
+ self.warning = pyreadrdi.ErrorCode.get_message(self.error)
269
+
270
+ def data_types(self, ens=0):
271
+ """
272
+ Finds the available data types for an ensemble.
273
+
274
+ Parameters
275
+ ----------
276
+ ens : int, optional
277
+ Ensemble number to get data types for, by default 0 or the first ensemble.
278
+
279
+ Returns
280
+ -------
281
+ list
282
+ A list of data type names corresponding to the ensemble.
283
+ """
284
+
285
+ data_id_array = self.dataid[ens]
286
+ id_name_array = list()
287
+ i = 0
288
+
289
+ for data_id in data_id_array:
290
+ # Checks dual mode IDs (BroadBand or NarrowBand)
291
+ # The first ID is generally the default ID
292
+ if data_id in (0, 1):
293
+ id_name = "Fixed Leader"
294
+ elif data_id in (128, 129):
295
+ id_name = "Variable Leader"
296
+ elif data_id in (256, 257):
297
+ id_name = "Velocity"
298
+ elif data_id in (512, 513):
299
+ id_name = "Correlation"
300
+ elif data_id in (768, 769):
301
+ id_name = "Echo"
302
+ elif data_id in (1024, 1025):
303
+ id_name = "Percent Good"
304
+ elif data_id == 1280:
305
+ id_name = "Status"
306
+ elif data_id == 1536:
307
+ id_name = "Bottom Track"
308
+ else:
309
+ id_name = "ID not Found"
310
+
311
+ id_name_array.append(id_name)
312
+ i += 1
313
+
314
+ return id_name_array
315
+
316
+ def check_file(self):
317
+ """
318
+ Checks if the system file size matches the calculated file size and
319
+ verifies uniformity across bytes and data types for all ensembles.
320
+
321
+ Returns
322
+ -------
323
+ dict
324
+ A dictionary containing file size and uniformity checks.
325
+ """
326
+ file_stats = os.stat(self.filename)
327
+ sys_file_size = file_stats.st_size
328
+ cal_file_size = sum((self.bytes).astype(int)) + 2 * len(self.bytes)
329
+
330
+ check = dict()
331
+
332
+ check["System File Size (B)"] = sys_file_size
333
+ check["Calculated File Size (B)"] = cal_file_size
334
+ check["File Size (MB)"] = cal_file_size / 1048576
335
+
336
+ if sys_file_size != cal_file_size:
337
+ check["File Size Match"] = False
338
+ else:
339
+ check["File Size Match"] = True
340
+
341
+ check["Byte Uniformity"] = check_equal(self.bytes.tolist())
342
+ check["Data Type Uniformity"] = check_equal(self.bytes.tolist())
343
+
344
+ return check
345
+
346
+ def print_check_file(self):
347
+ """
348
+ Prints a summary of the file size check results, including system file size, calculated file size,
349
+ and warnings if discrepancies are found.
350
+
351
+ Returns
352
+ -------
353
+ None
354
+ """
355
+ file_stats = os.stat(self.filename)
356
+ sys_file_size = file_stats.st_size
357
+ cal_file_size = sum(self.bytes) + 2 * len(self.bytes)
358
+
359
+ print("---------------RDI FILE SIZE CHECK-------------------")
360
+ print(f"System file size = {sys_file_size} B")
361
+ print(f"Calculated file size = {cal_file_size} B")
362
+ if sys_file_size != cal_file_size:
363
+ print("WARNING: The file sizes do not match")
364
+ else:
365
+ print(
366
+ "File size in MB (binary): % 8.2f MB\
367
+ \nFile sizes matches!"
368
+ % (cal_file_size / 1048576)
369
+ )
370
+ print("-----------------------------------------------------")
371
+
372
+ print(f"Total number of ensembles: {self.ensembles}")
373
+
374
+ if check_equal(self.bytes.tolist()):
375
+ print("No. of Bytes are same for all ensembles.")
376
+ else:
377
+ print("WARNING: No. of Bytes not equal for all ensembles.")
378
+
379
+ if check_equal(self.datatypes.tolist()):
380
+ print("No. of Data Types are same for all ensembles.")
381
+ else:
382
+ print("WARNING: No. of Data Types not equal for all ensembles.")
383
+
384
+ return
385
+
386
+
387
+ # FIXED LEADER CODES #
388
+
389
+
390
+ def flead_dict(fid, dim=2):
391
+ """
392
+ Extracts Fixed Leader data from a file and assigns it a identifiable name.
393
+
394
+ Parameters
395
+ ----------
396
+ fid : file object or array-like
397
+ The data source to extract Fixed Leader information from.
398
+ dim : int, optional
399
+ The dimension of the data, by default 2.
400
+
401
+ Returns
402
+ -------
403
+ dict
404
+ A dictionary containing Fixed Leader field and data.
405
+ """
406
+
407
+ fname = {
408
+ "CPU Version": "int64",
409
+ "CPU Revision": "int64",
410
+ "System Config Code": "int64",
411
+ "Real Flag": "int64",
412
+ "Lag Length": "int64",
413
+ "Beams": "int64",
414
+ "Cells": "int64",
415
+ "Pings": "int64",
416
+ "Depth Cell Len": "int64",
417
+ "Blank Transmit": "int64",
418
+ "Signal Mode": "int64",
419
+ "Correlation Thresh": "int64",
420
+ "Code Reps": "int64",
421
+ "Percent Good Min": "int64",
422
+ "Error Velocity Thresh": "int64",
423
+ "TP Minute": "int64",
424
+ "TP Second": "int64",
425
+ "TP Hundredth": "int64",
426
+ "Coord Transform Code": "int64",
427
+ "Head Alignment": "int64",
428
+ "Head Bias": "int64",
429
+ "Sensor Source Code": "int64",
430
+ "Sensor Avail Code": "int64",
431
+ "Bin 1 Dist": "int64",
432
+ "Xmit Pulse Len": "int64",
433
+ "Ref Layer Avg": "int64",
434
+ "False Target Thresh": "int64",
435
+ "Spare 1": "int64",
436
+ "Transmit Lag Dist": "int64",
437
+ "CPU Serial No": "int128",
438
+ "System Bandwidth": "int64",
439
+ "System Power": "int64",
440
+ "Spare 2": "int64",
441
+ "Instrument No": "int64",
442
+ "Beam Angle": "int64",
443
+ }
444
+
445
+ flead = dict()
446
+ counter = 1
447
+ for key, value in fname.items():
448
+ if dim == 2:
449
+ if key == "CPU Serial No":
450
+ flead[key] = np.uint64(fid[:][counter])
451
+ else:
452
+ flead[key] = np.int64(fid[:][counter])
453
+ elif dim == 1:
454
+ if key == "CPU Serial No":
455
+ flead[key] = np.uint64(fid[counter])
456
+ else:
457
+ flead[key] = np.int64(fid[counter])
458
+ else:
459
+ print("ERROR: Higher dimensions not allowed")
460
+ sys.exit()
461
+
462
+ counter += 1
463
+
464
+ return flead
465
+
466
+
467
+ class FixedLeader:
468
+ """
469
+ The class extracts Fixed Leader data from RDI File.
470
+
471
+ Fixed Leader data are non-dynamic or constants. They
472
+ contain hardware information and ADCP data that only
473
+ change based on certain ADCP input commands. The data,
474
+ generally, do not change within a file.
475
+ """
476
+
477
+ def __init__(
478
+ self,
479
+ rdi_file,
480
+ byteskip=None,
481
+ offset=None,
482
+ idarray=None,
483
+ ensemble=0,
484
+ ):
485
+ """
486
+ Initializes the FixedLeader object, extracts Fixed Leader data, and stores it in a dictionary.
487
+ The optional parameters can be obtained from FileHeader.
488
+
489
+ Parameters
490
+ ----------
491
+ rdi_file : str
492
+ The RDI ADCP binary file to extract data from.
493
+ byteskip : int, optional
494
+ Number of bytes to skip, by default None.
495
+ offset : int, optional
496
+ Offset value for data extraction, by default None.
497
+ idarray : array-like, optional
498
+ Array of IDs for data extraction, by default None.
499
+ ensemble : int, optional
500
+ Ensemble number to start extraction from, by default 0.
501
+ """
502
+ self.filename = rdi_file
503
+
504
+ self.data, self.ensembles, self.error = pyreadrdi.fixedleader(
505
+ self.filename,
506
+ byteskip=byteskip,
507
+ offset=offset,
508
+ idarray=idarray,
509
+ ensemble=ensemble,
510
+ )
511
+ self.warning = pyreadrdi.ErrorCode.get_message(self.error)
512
+
513
+ self.data = np.uint64(self.data)
514
+ self.fleader = flead_dict(self.data)
515
+ self._initialize_from_dict(DotDict(json_file_path="flmeta.json"))
516
+
517
+ def _initialize_from_dict(self, dotdict):
518
+ """
519
+ Initializes the FixedLeader object from a DotDict.
520
+
521
+ Parameters
522
+ ----------
523
+ dotdict : DotDict
524
+ A DotDict object containing metadata for the Fixed Leader.
525
+ """
526
+ i = 1
527
+ for key, value in dotdict.__dict__.items():
528
+ setattr(self, key, value)
529
+ setattr(getattr(self, key), "data", self.data[i])
530
+ i = i + 1
531
+
532
+ def field(self, ens=0):
533
+ """
534
+ Returns Fixed Leader dictionary pairs for a single ensemble.
535
+
536
+ Parameters
537
+ ----------
538
+ ens : int, optional
539
+ Ensemble number to extract, by default 0 or the first ensemble.
540
+
541
+ Returns
542
+ -------
543
+ dict
544
+ A dictionary of Fixed Leader data for the specified ensemble.
545
+ """
546
+
547
+ f1 = np.array(self.data)
548
+ return flead_dict(f1[:, ens], dim=1)
549
+
550
+ def is_uniform(self):
551
+ """
552
+ Checks whether Fixed Leader data fields are uniform across ensembles.
553
+
554
+ Returns
555
+ -------
556
+ dict
557
+ A dictionary indicating uniformity of each Fixed Leader data field.
558
+ """
559
+ output = dict()
560
+ for key, value in self.fleader.items():
561
+ output[key] = check_equal(value)
562
+ return output
563
+
564
+ def system_configuration(self, ens=0):
565
+ """
566
+ Extracts and interprets the system configuration from the Fixed Leader data.
567
+
568
+ Parameters
569
+ ----------
570
+ ens : int, optional
571
+ Ensemble number to extract system configuration for, by default 0.
572
+
573
+ Returns
574
+ -------
575
+ dict
576
+ A dictionary containing system configuration details.
577
+
578
+ List of Returnable Keys
579
+ -----------------------
580
+ Returns values for the following keys
581
+ No| Keys | Possible values
582
+ ---------------------------------
583
+ 1 | "Frequency" | ['75 kHz', '150 kHz', '300 kHz',
584
+ '600 kHz', '1200 kHz', '2400 kHz',
585
+ '38 kHz']
586
+ 2 | "Beam Pattern" | ['Concave', 'Convex']
587
+ 3 | "Sensor Configuration" | ['#1', '#2', '#3']
588
+ 4 | "XDCR HD" | ['Not Attached', 'Attached']
589
+ 5 | "Beam Direction" | ['Up', 'Down']
590
+ 6 | "Beam Angle | [15, 20, 30, 25, 45]
591
+ 7 | "Janus Configuration" | ["4 Beam", "5 Beam CFIG DEMOD",
592
+ "5 Beam CFIG 2 DEMOD"]
593
+ """
594
+
595
+ binary_bits = format(self.fleader["System Config Code"][ens], "016b")
596
+ # convert integer to binary format
597
+ # In '016b': 0 adds extra zeros to the binary string
598
+ # : 16 is the total number of binary bits
599
+ # : b is used to convert integer to binary format
600
+ # : Add '#' to get python binary format ('#016b')
601
+ sys_cfg = dict()
602
+
603
+ freq_code = {
604
+ "000": "75-kHz",
605
+ "001": "150-kHz",
606
+ "010": "300-kHz",
607
+ "011": "600-kHz",
608
+ "100": "1200-kHz",
609
+ "101": "2400-kHz",
610
+ "110": "38-kHz",
611
+ }
612
+
613
+ beam_code = {"0": "Concave", "1": "Convex"}
614
+
615
+ sensor_code = {
616
+ "00": "#1",
617
+ "01": "#2",
618
+ "10": "#3",
619
+ "11": "Sensor configuration not found",
620
+ }
621
+
622
+ xdcr_code = {"0": "Not attached", "1": "Attached"}
623
+
624
+ dir_code = {"0": "Down", "1": "Up"}
625
+
626
+ angle_code = {
627
+ "0000": "15",
628
+ "0001": "20",
629
+ "0010": "30",
630
+ "0011": "Other beam angle",
631
+ "0111": "25",
632
+ "1100": "45",
633
+ }
634
+
635
+ janus_code = {
636
+ "0100": "4 Beam",
637
+ "0101": "5 Beam CFIG DEMOD",
638
+ "1111": "5 Beam CFIG 2 DEMOD",
639
+ }
640
+
641
+ bit_group = binary_bits[13:16]
642
+ sys_cfg["Frequency"] = freq_code.get(bit_group, "Frequency not found")
643
+
644
+ bit_group = binary_bits[12]
645
+ sys_cfg["Beam Pattern"] = beam_code.get(bit_group)
646
+
647
+ bit_group = binary_bits[10:12]
648
+ sys_cfg["Sensor Configuration"] = sensor_code.get(bit_group)
649
+
650
+ bit_group = binary_bits[9]
651
+ sys_cfg["XDCR HD"] = xdcr_code.get(bit_group)
652
+
653
+ bit_group = binary_bits[8]
654
+ sys_cfg["Beam Direction"] = dir_code.get(bit_group)
655
+
656
+ bit_group = binary_bits[4:8]
657
+ sys_cfg["Beam Angle"] = angle_code.get(bit_group, "Angle not found")
658
+
659
+ bit_group = binary_bits[0:4]
660
+ sys_cfg["Janus Configuration"] = janus_code.get(
661
+ bit_group, "Janus cfg. not found"
662
+ )
663
+
664
+ return sys_cfg
665
+
666
+ def ex_coord_trans(self, ens=0):
667
+ """
668
+ Extracts the coordinate transformation configuration from the Fixed Leader data.
669
+
670
+ Parameters
671
+ ----------
672
+ ens : int, optional
673
+ Ensemble number to extract transformation configuration for, by default 0.
674
+
675
+ Returns
676
+ -------
677
+ dict
678
+ A dictionary of coordinate transformation details.
679
+ """
680
+
681
+ bit_group = format(self.fleader["Coord Transform Code"][ens], "08b")
682
+ transform = dict()
683
+
684
+ trans_code = {
685
+ "00": "Beam Coordinates",
686
+ "01": "Instrument Coordinates",
687
+ "10": "Ship Coordinates",
688
+ "11": "Earth Coordinates",
689
+ }
690
+
691
+ bool_code = {"1": True, "0": False}
692
+
693
+ transform["Coordinates"] = trans_code.get(bit_group[3:5])
694
+ transform["Tilt Correction"] = bool_code.get(bit_group[5])
695
+ transform["Three-Beam Solution"] = bool_code.get(bit_group[6])
696
+ transform["Bin Mapping"] = bool_code.get(bit_group[7])
697
+
698
+ return transform
699
+
700
+ def ez_sensor(self, ens=0, field="source"):
701
+ """
702
+ Checks for available or selected sensors from the Fixed Leader.
703
+
704
+ Parameters
705
+ ----------
706
+ ens : int, optional
707
+ Ensemble number to extract sensor information for, by default 0.
708
+ field : str, optional
709
+ Sensor field to extract ('source' or 'avail'), by default "source".
710
+
711
+ Returns
712
+ -------
713
+ dict
714
+ A dictionary of sensor availability or source selection.
715
+
716
+ """
717
+ if field == "source":
718
+ bit_group = format(self.fleader["Sensor Source Code"][ens], "08b")
719
+ elif field == "avail":
720
+ bit_group = format(self.fleader["Sensor Avail Code"][ens], "08b")
721
+ else:
722
+ sys.exit("ERROR (function ez_sensor): Enter valid argument.")
723
+
724
+ sensor = dict()
725
+
726
+ bool_code = {"1": True, "0": False}
727
+
728
+ sensor["Sound Speed"] = bool_code.get(bit_group[1])
729
+ sensor["Depth Sensor"] = bool_code.get(bit_group[2])
730
+ sensor["Heading Sensor"] = bool_code.get(bit_group[3])
731
+ sensor["Pitch Sensor"] = bool_code.get(bit_group[4])
732
+ sensor["Roll Sensor"] = bool_code.get(bit_group[5])
733
+ sensor["Conductivity Sensor"] = bool_code.get(bit_group[6])
734
+ sensor["Temperature Sensor"] = bool_code.get(bit_group[7])
735
+
736
+ return sensor
737
+
738
+
739
+ # VARIABLE LEADER CODES #
740
+ def vlead_dict(vid):
741
+ """
742
+ Extracts Variable Leader data from a file and assigns it a identifiable name.
743
+
744
+ Parameters
745
+ ----------
746
+ fid : file object or array-like
747
+ The data source to extract Fixed Leader information from.
748
+
749
+ Returns
750
+ -------
751
+ dict
752
+ A dictionary containing Variable Leader field and data.
753
+ """
754
+
755
+ vname = {
756
+ "RDI Ensemble": "int16",
757
+ "RTC Year": "int16",
758
+ "RTC Month": "int16",
759
+ "RTC Day": "int16",
760
+ "RTC Hour": "int16",
761
+ "RTC Minute": "int16",
762
+ "RTC Second": "int16",
763
+ "RTC Hundredth": "int16",
764
+ "Ensemble MSB": "int16",
765
+ "Bit Result": "int16",
766
+ "Speed of Sound": "int16",
767
+ "Depth of Transducer": "int16",
768
+ "Heading": "int32",
769
+ "Pitch": "int16",
770
+ "Roll": "int16",
771
+ "Salinity": "int16",
772
+ "Temperature": "int16",
773
+ "MPT Minute": "int16",
774
+ "MPT Second": "int16",
775
+ "MPT Hundredth": "int16",
776
+ "Hdg Std Dev": "int16",
777
+ "Pitch Std Dev": "int16",
778
+ "Roll Std Dev": "int16",
779
+ "ADC Channel 0": "int16",
780
+ "ADC Channel 1": "int16",
781
+ "ADC Channel 2": "int16",
782
+ "ADC Channel 3": "int16",
783
+ "ADC Channel 4": "int16",
784
+ "ADC Channel 5": "int16",
785
+ "ADC Channel 6": "int16",
786
+ "ADC Channel 7": "int16",
787
+ "Error Status Word 1": "int16",
788
+ "Error Status Word 2": "int16",
789
+ "Error Status Word 3": "int16",
790
+ "Error Status Word 4": "int16",
791
+ "Reserved": "int16",
792
+ "Pressure": "int32",
793
+ "Pressure Variance": "int32",
794
+ "Spare": "int16",
795
+ "Y2K Century": "int16",
796
+ "Y2K Year": "int16",
797
+ "Y2K Month": "int16",
798
+ "Y2K Day": "int16",
799
+ "Y2K Hour": "int16",
800
+ "Y2K Minute": "int16",
801
+ "Y2K Second": "int16",
802
+ "Y2K Hundredth": "int16",
803
+ }
804
+
805
+ vlead = dict()
806
+
807
+ counter = 1
808
+ for key, value in vname.items():
809
+ # vlead[key] = getattr(np, value)(vid[:][counter])
810
+ vlead[key] = vid[:][counter]
811
+ counter += 1
812
+
813
+ return vlead
814
+
815
+
816
+ class VariableLeader:
817
+ """
818
+ The class extracts Variable Leader Data.
819
+
820
+ Variable Leader data refers to the dynamic ADCP data
821
+ (from clocks/sensors) that change with each ping. The
822
+ WorkHorse ADCP always sends Variable Leader data as output
823
+ data (LSBs first).
824
+
825
+ Parameters
826
+ ----------
827
+ rdi_file : str
828
+ RDI ADCP binary file. The class can currently extract Workhorse,
829
+ Ocean Surveyor, and DVS files.
830
+ """
831
+
832
+ def __init__(
833
+ self,
834
+ rdi_file,
835
+ byteskip=None,
836
+ offset=None,
837
+ idarray=None,
838
+ ensemble=0,
839
+ ):
840
+ """
841
+ Initializes the VariableLeader object and extracts data from the RDI ADCP binary file.
842
+
843
+ Parameters
844
+ ----------
845
+ rdi_file : str
846
+ The RDI ADCP binary file to extract data from.
847
+ byteskip : int, optional
848
+ Number of bytes to skip, by default None.
849
+ offset : int, optional
850
+ Offset value for data extraction, by default None.
851
+ idarray : array-like, optional
852
+ Array of IDs for data extraction, by default None.
853
+ ensemble : int, optional
854
+ Ensemble number to start extraction from, by default 0.
855
+ """
856
+ self.filename = rdi_file
857
+
858
+ # Extraction starts here
859
+ self.data, self.ensembles, self.error = pyreadrdi.variableleader(
860
+ self.filename,
861
+ byteskip=byteskip,
862
+ offset=offset,
863
+ idarray=idarray,
864
+ ensemble=ensemble,
865
+ )
866
+ self.warning = pyreadrdi.ErrorCode.get_message(self.error)
867
+
868
+ # self.vdict = DotDict()
869
+ self.vleader = vlead_dict(self.data)
870
+ self._initialize_from_dict(DotDict(json_file_path="vlmeta.json"))
871
+
872
+ def _initialize_from_dict(self, dotdict):
873
+ """
874
+ Initializes the VariableLeader object attributes from a DotDict.
875
+
876
+ Parameters
877
+ ----------
878
+ dotdict : DotDict
879
+ A DotDict object containing metadata for the Variable Leader.
880
+ """
881
+ i = 1
882
+ for key, value in dotdict.__dict__.items():
883
+ setattr(self, key, value)
884
+ setattr(getattr(self, key), "data", self.data[i])
885
+ i = i + 1
886
+
887
+ def bitresult(self):
888
+ """
889
+ Extracts Bit Results from Variable Leader (Byte 13 & 14)
890
+ This field is part of the WorkHorse ADCP’s Built-in Test function.
891
+ A zero code indicates a successful BIT result.
892
+
893
+ Note: Byte 14 used for future use.
894
+
895
+ Returns
896
+ -------
897
+ dict
898
+ A dictionary of test field results.
899
+
900
+ """
901
+ tfname = {
902
+ "Reserved #1": "int16",
903
+ "Reserved #2": "int16",
904
+ "Reserved #3": "int16",
905
+ "DEMOD 1 Error": "int16",
906
+ "DEMOD 0 Error": "int16",
907
+ "Reserved #4": "int16",
908
+ "Timing Card Error": "int16",
909
+ "Reserved #5": "int16",
910
+ }
911
+
912
+ test_field = dict()
913
+ bit_array = self.vleader["Bit Result"]
914
+
915
+ # The bit result is read as single 16 bits variable instead of
916
+ # two 8-bits variable (Byte 13 & 14). The data is written in
917
+ # little endian format. Therefore, the Byte 14 comes before Byte 13.
918
+
919
+ for key, value in tfname.items():
920
+ test_field[key] = np.array([], dtype=value)
921
+
922
+ for item in bit_array:
923
+ bit_group = format(item, "016b")
924
+ bitpos = 8
925
+ for key, value in tfname.items():
926
+ bitappend = getattr(np, value)(bit_group[bitpos])
927
+ test_field[key] = np.append(test_field[key], bitappend)
928
+ bitpos += 1
929
+
930
+ return test_field
931
+
932
+ def adc_channel(self, offset=-0.20):
933
+ """
934
+ Extracts ADC Channel data and computes values for Xmit Voltage, Xmit Current,
935
+ and Ambient Temperature using system configuration.
936
+
937
+ Parameters
938
+ ----------
939
+ offset : float, optional
940
+ Offset value for temperature calculation, by default -0.20.
941
+
942
+ Returns
943
+ -------
944
+ dict
945
+ A dictionary of channel data including Xmit Voltage, Xmit Current, and Ambient Temperature.
946
+ """
947
+ # -----------CODE INCOMPLETE-------------- #
948
+ channel = dict()
949
+ scale_list = {
950
+ "75-kHz": [2092719, 43838],
951
+ "150-kHz": [592157, 11451],
952
+ "300-kHz": [592157, 11451],
953
+ "600-kHz": [380667, 11451],
954
+ "1200-kHz": [253765, 11451],
955
+ "2400-kHz": [253765, 11451],
956
+ }
957
+
958
+ adc0 = self.vleader["ADC Channel 0"]
959
+ adc1 = self.vleader["ADC Channel 1"]
960
+ adc2 = self.vleader["ADC Channel 2"]
961
+
962
+ fixclass = FixedLeader(self.filename).system_configuration()
963
+
964
+ scale_factor = scale_list.get(fixclass["Frequency"])
965
+
966
+ channel["Xmit Voltage"] = adc1 * (scale_factor[0] / 1000000)
967
+
968
+ channel["Xmit Current"] = adc0 * (scale_factor[1] / 1000000)
969
+
970
+ # Coefficients for temperature equation
971
+ a0 = 9.82697464e1
972
+ a1 = -5.86074151382e-3
973
+ a2 = 1.60433886495e-7
974
+ a3 = -2.32924716883e-12
975
+
976
+ channel["Ambient Temperature"] = (
977
+ offset + ((a3 * adc2 + a2) * adc2 + a1) * adc2 + a0
978
+ )
979
+
980
+ return channel
981
+
982
+ def error_status_word(self, esw=1):
983
+ bitset1 = (
984
+ "Bus Error exception",
985
+ "Address Error exception",
986
+ "Zero Divide exception",
987
+ "Emulator exception",
988
+ "Unassigned exception",
989
+ "Watchdog restart occurred",
990
+ "Batter Saver Power",
991
+ )
992
+
993
+ bitset2 = (
994
+ "Pinging",
995
+ "Not Used 1",
996
+ "Not Used 2",
997
+ "Not Used 3",
998
+ "Not Used 4",
999
+ "Not Used 5",
1000
+ "Cold Wakeup occured",
1001
+ "Unknown Wakeup occured",
1002
+ )
1003
+
1004
+ bitset3 = (
1005
+ "Clock Read error occured",
1006
+ "Unexpected alarm",
1007
+ "Clock jump forward",
1008
+ "Clock jump backward",
1009
+ "Not Used 6",
1010
+ "Not Used 7",
1011
+ "Not Used 8",
1012
+ "Not Used 9",
1013
+ )
1014
+
1015
+ bitset4 = (
1016
+ "Not Used 10",
1017
+ "Not Used 11",
1018
+ "Not Used 12",
1019
+ "Power Fail Unrecorded",
1020
+ "Spurious level 4 intr DSP",
1021
+ "Spurious level 5 intr UART",
1022
+ "Spurious level 6 intr CLOCK",
1023
+ "Level 7 interrup occured",
1024
+ )
1025
+
1026
+ if esw == 1:
1027
+ bitset = bitset1
1028
+ errorarray = self.vleader["Error Status Word 1"]
1029
+ elif esw == 2:
1030
+ bitset = bitset2
1031
+ errorarray = self.vleader["Error Status Word 2"]
1032
+ elif esw == 3:
1033
+ bitset = bitset3
1034
+ errorarray = self.vleader["Error Status Word 3"]
1035
+ else:
1036
+ bitset = bitset4
1037
+ errorarray = self.vleader["Error Status Word 4"]
1038
+
1039
+ errorstatus = dict()
1040
+ # bitarray = np.zeros(32, dtype='str')
1041
+
1042
+ for item in bitset:
1043
+ errorstatus[item] = np.array([])
1044
+
1045
+ for data in errorarray:
1046
+ byte_split = format(data, "08b")
1047
+ bitposition = 0
1048
+ for item in bitset:
1049
+ errorstatus[item] = np.append(
1050
+ errorstatus[item], byte_split[bitposition]
1051
+ )
1052
+ bitposition += 1
1053
+
1054
+ return errorstatus
1055
+
1056
+
1057
+ class Velocity:
1058
+ """
1059
+ The class extracts velocity data from RDI ADCP files.
1060
+
1061
+ Parameters
1062
+ ----------
1063
+ filename : str
1064
+ The RDI ADCP binary file to extract data from.
1065
+ cell : int, optional
1066
+ Cell number to extract, by default 0.
1067
+ beam : int, optional
1068
+ Beam number to extract, by default 0.
1069
+ byteskip : int, optional
1070
+ Number of bytes to skip, by default None.
1071
+ offset : int, optional
1072
+ Offset value for data extraction, by default None.
1073
+ idarray : array-like, optional
1074
+ Array of IDs for data extraction, by default None.
1075
+ ensemble : int, optional
1076
+ Ensemble number to start extraction from, by default 0.
1077
+ """
1078
+
1079
+ def __init__(
1080
+ self,
1081
+ filename,
1082
+ cell=0,
1083
+ beam=0,
1084
+ byteskip=None,
1085
+ offset=None,
1086
+ idarray=None,
1087
+ ensemble=0,
1088
+ ):
1089
+ self.filename = filename
1090
+ error = 0
1091
+ data, ens, cell, beam, error = pyreadrdi.datatype(
1092
+ self.filename,
1093
+ "velocity",
1094
+ cell=cell,
1095
+ beam=beam,
1096
+ byteskip=byteskip,
1097
+ offset=offset,
1098
+ idarray=idarray,
1099
+ ensemble=ensemble,
1100
+ )
1101
+ self.warning = pyreadrdi.ErrorCode.get_message(error)
1102
+
1103
+ self.data = data
1104
+ self.error = error
1105
+ self.ensembles = ens
1106
+ self.cells = cell
1107
+ self.beams = beam
1108
+
1109
+ self.unit = "mm/s"
1110
+ self.missing_value = "-32768"
1111
+ self.scale_factor = 1
1112
+ self.valid_min = -32768
1113
+ self.valid_max = 32768
1114
+
1115
+
1116
+ class Correlation:
1117
+ """
1118
+ The class extracts correlation data from RDI ADCP files.
1119
+
1120
+ Parameters
1121
+ ----------
1122
+ filename : str
1123
+ The RDI ADCP binary file to extract data from.
1124
+ cell : int, optional
1125
+ Cell number to extract, by default 0.
1126
+ beam : int, optional
1127
+ Beam number to extract, by default 0.
1128
+ byteskip : int, optional
1129
+ Number of bytes to skip, by default None.
1130
+ offset : int, optional
1131
+ Offset value for data extraction, by default None.
1132
+ idarray : array-like, optional
1133
+ Array of IDs for data extraction, by default None.
1134
+ ensemble : int, optional
1135
+ Ensemble number to start extraction from, by default 0.
1136
+ """
1137
+
1138
+ def __init__(
1139
+ self,
1140
+ filename,
1141
+ cell=0,
1142
+ beam=0,
1143
+ byteskip=None,
1144
+ offset=None,
1145
+ idarray=None,
1146
+ ensemble=0,
1147
+ ):
1148
+ self.filename = filename
1149
+ error = 0
1150
+ data, ens, cell, beam, error = pyreadrdi.datatype(
1151
+ self.filename,
1152
+ "correlation",
1153
+ cell=cell,
1154
+ beam=beam,
1155
+ byteskip=byteskip,
1156
+ offset=offset,
1157
+ idarray=idarray,
1158
+ ensemble=ensemble,
1159
+ )
1160
+ self.warning = pyreadrdi.ErrorCode.get_message(error)
1161
+
1162
+ self.data = data
1163
+ self.error = error
1164
+ self.ensembles = ens
1165
+ self.cells = cell
1166
+ self.beams = beam
1167
+
1168
+ self.unit = ""
1169
+ self.scale_factor = 1
1170
+ self.valid_min = 0
1171
+ self.valid_max = 255
1172
+ self.long_name = "Correlation Magnitude"
1173
+
1174
+
1175
+ class Echo:
1176
+ """
1177
+ The class extracts echo intensity data from RDI ADCP files.
1178
+
1179
+ Parameters
1180
+ ----------
1181
+ filename : str
1182
+ The RDI ADCP binary file to extract data from.
1183
+ cell : int, optional
1184
+ Cell number to extract, by default 0.
1185
+ beam : int, optional
1186
+ Beam number to extract, by default 0.
1187
+ byteskip : int, optional
1188
+ Number of bytes to skip, by default None.
1189
+ offset : int, optional
1190
+ Offset value for data extraction, by default None.
1191
+ idarray : array-like, optional
1192
+ Array of IDs for data extraction, by default None.
1193
+ ensemble : int, optional
1194
+ Ensemble number to start extraction from, by default 0.
1195
+ """
1196
+
1197
+ def __init__(
1198
+ self,
1199
+ filename,
1200
+ cell=0,
1201
+ beam=0,
1202
+ byteskip=None,
1203
+ offset=None,
1204
+ idarray=None,
1205
+ ensemble=0,
1206
+ ):
1207
+ self.filename = filename
1208
+ error = 0
1209
+ data, ens, cell, beam, error = pyreadrdi.datatype(
1210
+ self.filename,
1211
+ "echo",
1212
+ cell=cell,
1213
+ beam=beam,
1214
+ byteskip=byteskip,
1215
+ offset=offset,
1216
+ idarray=idarray,
1217
+ ensemble=ensemble,
1218
+ )
1219
+ self.warning = pyreadrdi.ErrorCode.get_message(error)
1220
+
1221
+ self.data = data
1222
+ self.error = error
1223
+ self.ensembles = ens
1224
+ self.cells = cell
1225
+ self.beams = beam
1226
+
1227
+ self.unit = "counts"
1228
+ self.scale_factor = "0.45"
1229
+ self.valid_min = 0
1230
+ self.valid_max = 255
1231
+ self.long_name = "Echo Intensity"
1232
+
1233
+
1234
+ class PercentGood:
1235
+ """
1236
+ The class extracts Percent Good data from RDI ADCP files.
1237
+
1238
+ Parameters
1239
+ ----------
1240
+ filename : str
1241
+ The RDI ADCP binary file to extract data from.
1242
+ cell : int, optional
1243
+ Cell number to extract, by default 0.
1244
+ beam : int, optional
1245
+ Beam number to extract, by default 0.
1246
+ byteskip : int, optional
1247
+ Number of bytes to skip, by default None.
1248
+ offset : int, optional
1249
+ Offset value for data extraction, by default None.
1250
+ idarray : array-like, optional
1251
+ Array of IDs for data extraction, by default None.
1252
+ ensemble : int, optional
1253
+ Ensemble number to start extraction from, by default 0.
1254
+ """
1255
+
1256
+ def __init__(
1257
+ self,
1258
+ filename,
1259
+ cell=0,
1260
+ beam=0,
1261
+ byteskip=None,
1262
+ offset=None,
1263
+ idarray=None,
1264
+ ensemble=0,
1265
+ ):
1266
+ self.filename = filename
1267
+ error = 0
1268
+ data, ens, cell, beam, error = pyreadrdi.datatype(
1269
+ self.filename,
1270
+ "percent good",
1271
+ cell=cell,
1272
+ beam=beam,
1273
+ byteskip=byteskip,
1274
+ offset=offset,
1275
+ idarray=idarray,
1276
+ ensemble=ensemble,
1277
+ )
1278
+ self.warning = pyreadrdi.ErrorCode.get_message(error)
1279
+
1280
+ self.data = data
1281
+ self.error = error
1282
+ self.ensembles = ens
1283
+ self.cells = cell
1284
+ self.beams = beam
1285
+
1286
+ self.unit = "percent"
1287
+ self.valid_min = 0
1288
+ self.valid_max = 100
1289
+ self.long_name = "Percent Good"
1290
+
1291
+
1292
+ class Status:
1293
+ """
1294
+ The class extracts Status data from RDI ADCP files.
1295
+
1296
+ Parameters
1297
+ ----------
1298
+ filename : str
1299
+ The RDI ADCP binary file to extract data from.
1300
+ cell : int, optional
1301
+ Cell number to extract, by default 0.
1302
+ beam : int, optional
1303
+ Beam number to extract, by default 0.
1304
+ byteskip : int, optional
1305
+ Number of bytes to skip, by default None.
1306
+ offset : int, optional
1307
+ Offset value for data extraction, by default None.
1308
+ idarray : array-like, optional
1309
+ Array of IDs for data extraction, by default None.
1310
+ ensemble : int, optional
1311
+ Ensemble number to start extraction from, by default 0.
1312
+ """
1313
+
1314
+ def __init__(
1315
+ self,
1316
+ filename,
1317
+ cell=0,
1318
+ beam=0,
1319
+ byteskip=None,
1320
+ offset=None,
1321
+ idarray=None,
1322
+ ensemble=0,
1323
+ ):
1324
+ self.filename = filename
1325
+ error = 0
1326
+ data, ens, cell, beam, error = pyreadrdi.datatype(
1327
+ self.filename,
1328
+ "status",
1329
+ cell=cell,
1330
+ beam=beam,
1331
+ byteskip=byteskip,
1332
+ offset=offset,
1333
+ idarray=idarray,
1334
+ ensemble=ensemble,
1335
+ )
1336
+ self.warning = pyreadrdi.ErrorCode.get_message(error)
1337
+
1338
+ self.data = data
1339
+ self.error = error
1340
+ self.ensembles = ens
1341
+ self.cells = cell
1342
+ self.beams = beam
1343
+
1344
+ self.unit = ""
1345
+ self.valid_min = 0
1346
+ self.valid_max = 1
1347
+ self.long_name = "Status Data Format"
1348
+
1349
+
1350
+ class ReadFile:
1351
+ """
1352
+ Class to read and extract data from RDI ADCP binary files, organizing data types like
1353
+ Fixed Leader, Variable Leader, Velocity, and others.
1354
+
1355
+ Parameters
1356
+ ----------
1357
+ filename : str
1358
+ The RDI ADCP binary file to be read.
1359
+ """
1360
+
1361
+ def __init__(self, filename):
1362
+ """
1363
+ Initializes the ReadFile object and extracts data from the RDI ADCP binary file.
1364
+ """
1365
+ self.fileheader = FileHeader(filename)
1366
+ datatype_array = self.fileheader.data_types()
1367
+ error_array = {"Fileheader": self.fileheader.error}
1368
+ warning_array = {"Fileheader": self.fileheader.warning}
1369
+ ensemble_array = {"Fileheader": self.fileheader.ensembles}
1370
+
1371
+ byteskip = self.fileheader.byteskip
1372
+ offset = self.fileheader.address_offset
1373
+ idarray = self.fileheader.dataid
1374
+ ensemble = self.fileheader.ensembles
1375
+
1376
+ self.fixedleader = FixedLeader(
1377
+ filename,
1378
+ byteskip=byteskip,
1379
+ offset=offset,
1380
+ idarray=idarray,
1381
+ ensemble=ensemble,
1382
+ )
1383
+ error_array["Fixed Leader"] = self.fixedleader.error
1384
+ warning_array["Fixed Leader"] = self.fixedleader.warning
1385
+ ensemble_array["Fixed Leader"] = self.fixedleader.ensembles
1386
+ cells = self.fixedleader.fleader["Cells"][0]
1387
+ beams = self.fixedleader.fleader["Beams"][0]
1388
+ ensemble = self.fixedleader.ensembles
1389
+
1390
+ self.variableleader = VariableLeader(
1391
+ filename,
1392
+ byteskip=byteskip,
1393
+ offset=offset,
1394
+ idarray=idarray,
1395
+ ensemble=ensemble,
1396
+ )
1397
+ error_array["Variable Leader"] = self.variableleader.error
1398
+ warning_array["Variable Leader"] = self.variableleader.warning
1399
+ ensemble_array["Variable Leader"] = self.variableleader.ensembles
1400
+ ensemble = self.fixedleader.ensembles
1401
+
1402
+ if "Velocity" in datatype_array:
1403
+ self.velocity = Velocity(
1404
+ filename,
1405
+ cell=cells,
1406
+ beam=beams,
1407
+ byteskip=byteskip,
1408
+ offset=offset,
1409
+ idarray=idarray,
1410
+ ensemble=ensemble,
1411
+ )
1412
+ error_array["Velocity"] = self.velocity.error
1413
+ warning_array["Velocity"] = self.velocity.warning
1414
+ ensemble_array["Velocity"] = self.velocity.ensembles
1415
+
1416
+ if "Correlation" in datatype_array:
1417
+ self.correlation = Correlation(
1418
+ filename,
1419
+ cell=cells,
1420
+ beam=beams,
1421
+ byteskip=byteskip,
1422
+ offset=offset,
1423
+ idarray=idarray,
1424
+ ensemble=ensemble,
1425
+ )
1426
+ error_array["Correlation"] = self.correlation.error
1427
+ warning_array["Correlation"] = self.correlation.warning
1428
+ ensemble_array["Correlation"] = self.correlation.ensembles
1429
+
1430
+ if "Echo" in datatype_array:
1431
+ self.echo = Echo(
1432
+ filename,
1433
+ cell=cells,
1434
+ beam=beams,
1435
+ byteskip=byteskip,
1436
+ offset=offset,
1437
+ idarray=idarray,
1438
+ ensemble=ensemble,
1439
+ )
1440
+ error_array["Echo"] = self.echo.error
1441
+ warning_array["Echo"] = self.echo.warning
1442
+ ensemble_array["Echo"] = self.echo.ensembles
1443
+
1444
+ if "Percent Good" in datatype_array:
1445
+ self.percentgood = PercentGood(
1446
+ filename,
1447
+ cell=cells,
1448
+ beam=beams,
1449
+ byteskip=byteskip,
1450
+ offset=offset,
1451
+ idarray=idarray,
1452
+ ensemble=ensemble,
1453
+ )
1454
+ error_array["Percent Good"] = self.percentgood.error
1455
+ warning_array["Percent Good"] = self.percentgood.warning
1456
+ ensemble_array["Percent Good"] = self.percentgood.ensembles
1457
+
1458
+ if "Status" in datatype_array:
1459
+ self.status = Status(
1460
+ filename,
1461
+ cell=cells,
1462
+ beam=beams,
1463
+ byteskip=byteskip,
1464
+ offset=offset,
1465
+ idarray=idarray,
1466
+ ensemble=ensemble,
1467
+ )
1468
+ error_array["Status"] = self.status.error
1469
+ warning_array["Status"] = self.status.warning
1470
+ ensemble_array["Status"] = self.status.ensembles
1471
+
1472
+ # Add Time Axis
1473
+ year = self.variableleader.vleader["RTC Year"]
1474
+ month = self.variableleader.vleader["RTC Month"]
1475
+ day = self.variableleader.vleader["RTC Day"]
1476
+ hour = self.variableleader.vleader["RTC Hour"]
1477
+ minute = self.variableleader.vleader["RTC Minute"]
1478
+ second = self.variableleader.vleader["RTC Second"]
1479
+ year = year + 2000
1480
+ date_df = pd.DataFrame(
1481
+ {
1482
+ "year": year,
1483
+ "month": month,
1484
+ "day": day,
1485
+ "hour": hour,
1486
+ "minute": minute,
1487
+ "second": second,
1488
+ }
1489
+ )
1490
+ self.time = pd.to_datetime(date_df)
1491
+
1492
+ # Depth
1493
+ # Create a depth axis with mean depth in 'm'
1494
+ cell1 = self.fixedleader.field()["Cells"]
1495
+ bin1dist1 = self.fixedleader.field()["Bin 1 Dist"] / 100
1496
+ depth_cell_len1 = self.fixedleader.field()["Depth Cell Len"] / 100
1497
+ beam_direction1 = self.fixedleader.system_configuration()["Beam Direction"]
1498
+ mean_depth = np.mean(self.variableleader.vleader["Depth of Transducer"]) / 10
1499
+ mean_depth = np.trunc(mean_depth)
1500
+ if beam_direction1.lower() == "up":
1501
+ sgn = -1
1502
+ else:
1503
+ sgn = 1
1504
+ first_depth = mean_depth + sgn * bin1dist1
1505
+ last_depth = first_depth + sgn * cell1 * depth_cell_len1
1506
+ z = np.arange(first_depth, last_depth, sgn * depth_cell_len1)
1507
+ self.depth = z
1508
+
1509
+ # Add all attributes/method/data from FixedLeader and VariableLeader
1510
+ self._copy_attributes_from_var()
1511
+
1512
+ # Error Codes and Warnings
1513
+ self.error_codes = error_array
1514
+ self.warnings = warning_array
1515
+ self.ensemble_array = ensemble_array
1516
+ self.ensemble_value_array = np.array(list(self.ensemble_array.values()))
1517
+
1518
+ self.isEnsembleEqual = check_equal(self.ensemble_value_array)
1519
+ self.isFixedEnsemble = False
1520
+
1521
+ ec = np.array(list(self.error_codes.values()))
1522
+
1523
+ if np.all(ec == 0):
1524
+ self.isWarning = False
1525
+ else:
1526
+ self.isWarning = True
1527
+
1528
+ # Add additional attributes
1529
+ # Ensemble
1530
+ dtens = self.ensemble_value_array
1531
+ minens = np.min(dtens)
1532
+ self.ensembles = minens
1533
+
1534
+ # Add attribute that lists all variables/functions
1535
+ self.list_vars = vars(self).keys()
1536
+
1537
+ def _copy_attributes_from_var(self):
1538
+ for attr_name, attr_value in self.variableleader.__dict__.items():
1539
+ # Copy each attribute of var into self
1540
+ setattr(self, attr_name, attr_value)
1541
+ for attr_name, attr_value in self.fixedleader.__dict__.items():
1542
+ # Copy each attribute of var into self
1543
+ setattr(self, attr_name, attr_value)
1544
+
1545
+ def __getattr__(self, name):
1546
+ # Delegate attribute/method access to self.var if not found in self
1547
+ if hasattr(self.variableleader, name):
1548
+ return getattr(self.variableleader, name)
1549
+ if hasattr(self.fixedleader, name):
1550
+ return getattr(self.fixedleader, name)
1551
+ raise AttributeError(
1552
+ f"'{self.__class__.__name__}' object has no attribute '{name}'"
1553
+ )
1554
+
1555
+ def fixensemble(self, min_cutoff=0):
1556
+ """
1557
+ Fixes the ensemble size across all data types in the file if they differ.
1558
+
1559
+ Parameters
1560
+ ----------
1561
+ min_cutoff : int, optional
1562
+ Minimum number of ensembles to consider when fixing, by default 0.
1563
+
1564
+ Returns
1565
+ -------
1566
+ None
1567
+ """
1568
+ datatype_array = self.fileheader.data_types()
1569
+ # Check if the number of ensembles in a data type
1570
+ # is less than min_cutoff.
1571
+ # Some data type can have zero ensembles
1572
+ dtens = self.ensemble_value_array
1573
+ new_array = dtens[dtens > min_cutoff]
1574
+ minens = np.min(new_array)
1575
+
1576
+ if not self.isEnsembleEqual:
1577
+ self.fileheader.ensembles = minens
1578
+ self.fileheader.datatypes = self.fileheader.datatypes[:minens]
1579
+ self.fileheader.bytes = self.fileheader.bytes[:minens]
1580
+ self.fileheader.byteskip = self.fileheader.byteskip[:minens]
1581
+ self.fileheader.address_offset = self.fileheader.address_offset[:minens, :]
1582
+ self.fileheader.dataid = self.fileheader.dataid[:minens, :]
1583
+ if "Fixed Leader" in datatype_array:
1584
+ self.fixedleader.data = self.fixedleader.data[:, :minens]
1585
+ self.fixedleader.ensembles = minens
1586
+ if "Variable Leader" in datatype_array:
1587
+ self.variableleader.data = self.variableleader.data[:, :minens]
1588
+ self.variableleader.ensembles = minens
1589
+ if "Velocity" in datatype_array:
1590
+ self.velocity.data = self.velocity.data[:, :, :minens]
1591
+ self.velocity.ensembles = minens
1592
+ if "Correlation" in datatype_array:
1593
+ self.correlation.data = self.correlation.data[:, :, :minens]
1594
+ self.correlation.ensembles = minens
1595
+ if "Echo" in datatype_array:
1596
+ self.echo.data = self.echo.data[:, :, :minens]
1597
+ self.echo.ensembles = minens
1598
+ if "Percent Good" in datatype_array:
1599
+ self.percentgood.data = self.percentgood.data[:, :, :minens]
1600
+ self.percentgood.ensembles = minens
1601
+ if "Status" in datatype_array:
1602
+ self.status.data = self.status.data[:, :, :minens]
1603
+ self.status.ensembles = minens
1604
+ print(f"Ensembles fixed to {minens}. All data types have same ensembles.")
1605
+ else:
1606
+ print(
1607
+ "WARNING: No response was initiated. All data types have same ensemble."
1608
+ )
1609
+
1610
+ self.isFixedEnsemble = True