imap-processing 0.7.0__py3-none-any.whl → 0.8.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.

Potentially problematic release.


This version of imap-processing might be problematic. Click here for more details.

Files changed (124) hide show
  1. imap_processing/__init__.py +1 -1
  2. imap_processing/_version.py +2 -2
  3. imap_processing/ccsds/excel_to_xtce.py +34 -2
  4. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +1 -1
  5. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +145 -30
  6. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +36 -36
  7. imap_processing/cdf/config/imap_hi_variable_attrs.yaml +36 -8
  8. imap_processing/cdf/config/imap_hit_l1b_variable_attrs.yaml +9 -0
  9. imap_processing/cdf/config/imap_idex_global_cdf_attrs.yaml +7 -7
  10. imap_processing/cdf/config/imap_idex_l1a_variable_attrs.yaml +32 -33
  11. imap_processing/cdf/config/imap_mag_l1_variable_attrs.yaml +24 -28
  12. imap_processing/cdf/config/imap_ultra_l1a_variable_attrs.yaml +1 -0
  13. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +133 -78
  14. imap_processing/cdf/config/imap_variable_schema.yaml +13 -0
  15. imap_processing/cdf/imap_cdf_manager.py +31 -27
  16. imap_processing/cli.py +12 -10
  17. imap_processing/codice/codice_l1a.py +151 -61
  18. imap_processing/codice/constants.py +1 -1
  19. imap_processing/codice/decompress.py +4 -9
  20. imap_processing/codice/utils.py +1 -0
  21. imap_processing/glows/l1b/glows_l1b.py +3 -3
  22. imap_processing/glows/l1b/glows_l1b_data.py +59 -37
  23. imap_processing/glows/l2/glows_l2_data.py +123 -0
  24. imap_processing/hi/l1a/histogram.py +1 -1
  25. imap_processing/hi/l1a/science_direct_event.py +1 -1
  26. imap_processing/hi/l1b/hi_l1b.py +85 -11
  27. imap_processing/hi/l1c/hi_l1c.py +23 -1
  28. imap_processing/hi/utils.py +1 -1
  29. imap_processing/hit/hit_utils.py +221 -0
  30. imap_processing/hit/l0/constants.py +118 -0
  31. imap_processing/hit/l0/decom_hit.py +186 -153
  32. imap_processing/hit/l1a/hit_l1a.py +20 -175
  33. imap_processing/hit/l1b/hit_l1b.py +33 -153
  34. imap_processing/idex/idex_l1a.py +10 -9
  35. imap_processing/lo/l0/decompression_tables/decompression_tables.py +1 -1
  36. imap_processing/lo/l0/lo_science.py +1 -1
  37. imap_processing/lo/packet_definitions/lo_xtce.xml +1 -3296
  38. imap_processing/mag/l0/decom_mag.py +4 -3
  39. imap_processing/mag/l1a/mag_l1a.py +11 -11
  40. imap_processing/mag/l1b/mag_l1b.py +89 -7
  41. imap_processing/spice/geometry.py +126 -4
  42. imap_processing/swapi/l1/swapi_l1.py +1 -1
  43. imap_processing/swapi/l2/swapi_l2.py +1 -1
  44. imap_processing/swe/l1b/swe_l1b_science.py +8 -8
  45. imap_processing/tests/ccsds/test_data/expected_output.xml +1 -0
  46. imap_processing/tests/ccsds/test_excel_to_xtce.py +4 -4
  47. imap_processing/tests/cdf/test_imap_cdf_manager.py +0 -10
  48. imap_processing/tests/codice/conftest.py +1 -17
  49. imap_processing/tests/codice/data/imap_codice_l0_raw_20241110_v001.pkts +0 -0
  50. imap_processing/tests/codice/test_codice_l0.py +8 -2
  51. imap_processing/tests/codice/test_codice_l1a.py +127 -107
  52. imap_processing/tests/codice/test_codice_l1b.py +1 -0
  53. imap_processing/tests/codice/test_decompress.py +7 -7
  54. imap_processing/tests/conftest.py +54 -15
  55. imap_processing/tests/glows/conftest.py +6 -0
  56. imap_processing/tests/glows/test_glows_l1b.py +9 -9
  57. imap_processing/tests/glows/test_glows_l1b_data.py +9 -9
  58. imap_processing/tests/glows/test_glows_l2_data.py +0 -0
  59. imap_processing/tests/hi/test_data/l1a/imap_hi_l1a_45sensor-de_20250415_v000.cdf +0 -0
  60. imap_processing/tests/hi/test_hi_l1b.py +71 -1
  61. imap_processing/tests/hi/test_hi_l1c.py +10 -2
  62. imap_processing/tests/hi/test_utils.py +4 -3
  63. imap_processing/tests/hit/{test_hit_decom.py → test_decom_hit.py} +84 -35
  64. imap_processing/tests/hit/test_hit_l1a.py +2 -197
  65. imap_processing/tests/hit/test_hit_l1b.py +156 -25
  66. imap_processing/tests/hit/test_hit_utils.py +218 -0
  67. imap_processing/tests/idex/conftest.py +1 -1
  68. imap_processing/tests/idex/imap_idex_l0_raw_20231214_v001.pkts +0 -0
  69. imap_processing/tests/idex/impact_14_tof_high_data.txt +4444 -4444
  70. imap_processing/tests/idex/test_idex_l0.py +3 -3
  71. imap_processing/tests/idex/test_idex_l1a.py +1 -1
  72. imap_processing/tests/lo/test_lo_science.py +2 -2
  73. imap_processing/tests/mag/imap_mag_l1a_norm-magi_20251017_v001.cdf +0 -0
  74. imap_processing/tests/mag/test_mag_l1b.py +59 -3
  75. imap_processing/tests/spice/test_data/imap_ena_sim_metakernel.template +3 -1
  76. imap_processing/tests/spice/test_geometry.py +84 -4
  77. imap_processing/tests/swe/conftest.py +33 -0
  78. imap_processing/tests/swe/l1_validation/swe_l0_unpacked-data_20240510_v001_VALIDATION_L1B_v3.dat +4332 -0
  79. imap_processing/tests/swe/test_swe_l1b.py +29 -8
  80. imap_processing/tests/test_utils.py +1 -1
  81. imap_processing/tests/ultra/test_data/l1/dps_exposure_helio_45_E12.cdf +0 -0
  82. imap_processing/tests/ultra/test_data/l1/dps_exposure_helio_45_E24.cdf +0 -0
  83. imap_processing/tests/ultra/unit/test_de.py +108 -0
  84. imap_processing/tests/ultra/unit/test_ultra_l1b.py +27 -3
  85. imap_processing/tests/ultra/unit/test_ultra_l1b_annotated.py +31 -10
  86. imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +21 -11
  87. imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +9 -44
  88. imap_processing/ultra/constants.py +8 -3
  89. imap_processing/ultra/l1b/de.py +174 -30
  90. imap_processing/ultra/l1b/ultra_l1b_annotated.py +24 -10
  91. imap_processing/ultra/l1b/ultra_l1b_extended.py +21 -14
  92. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +70 -119
  93. {imap_processing-0.7.0.dist-info → imap_processing-0.8.0.dist-info}/METADATA +15 -14
  94. {imap_processing-0.7.0.dist-info → imap_processing-0.8.0.dist-info}/RECORD +98 -113
  95. imap_processing/cdf/cdf_attribute_manager.py +0 -322
  96. imap_processing/cdf/config/shared/default_global_cdf_attrs_schema.yaml +0 -246
  97. imap_processing/cdf/config/shared/default_variable_cdf_attrs_schema.yaml +0 -466
  98. imap_processing/hit/l0/data_classes/housekeeping.py +0 -240
  99. imap_processing/hit/l0/data_classes/science_packet.py +0 -259
  100. imap_processing/hit/l0/utils/hit_base.py +0 -57
  101. imap_processing/tests/cdf/shared/default_global_cdf_attrs_schema.yaml +0 -246
  102. imap_processing/tests/cdf/shared/default_variable_cdf_attrs_schema.yaml +0 -466
  103. imap_processing/tests/cdf/test_cdf_attribute_manager.py +0 -353
  104. imap_processing/tests/codice/data/imap_codice_l0_hi-counters-aggregated_20240429_v001.pkts +0 -0
  105. imap_processing/tests/codice/data/imap_codice_l0_hi-counters-singles_20240429_v001.pkts +0 -0
  106. imap_processing/tests/codice/data/imap_codice_l0_hi-omni_20240429_v001.pkts +0 -0
  107. imap_processing/tests/codice/data/imap_codice_l0_hi-pha_20240429_v001.pkts +0 -0
  108. imap_processing/tests/codice/data/imap_codice_l0_hi-sectored_20240429_v001.pkts +0 -0
  109. imap_processing/tests/codice/data/imap_codice_l0_hskp_20100101_v001.pkts +0 -0
  110. imap_processing/tests/codice/data/imap_codice_l0_lo-counters-aggregated_20240429_v001.pkts +0 -0
  111. imap_processing/tests/codice/data/imap_codice_l0_lo-counters-singles_20240429_v001.pkts +0 -0
  112. imap_processing/tests/codice/data/imap_codice_l0_lo-nsw-angular_20240429_v001.pkts +0 -0
  113. imap_processing/tests/codice/data/imap_codice_l0_lo-nsw-priority_20240429_v001.pkts +0 -0
  114. imap_processing/tests/codice/data/imap_codice_l0_lo-nsw-species_20240429_v001.pkts +0 -0
  115. imap_processing/tests/codice/data/imap_codice_l0_lo-pha_20240429_v001.pkts +0 -0
  116. imap_processing/tests/codice/data/imap_codice_l0_lo-sw-angular_20240429_v001.pkts +0 -0
  117. imap_processing/tests/codice/data/imap_codice_l0_lo-sw-priority_20240429_v001.pkts +0 -0
  118. imap_processing/tests/codice/data/imap_codice_l0_lo-sw-species_20240429_v001.pkts +0 -0
  119. imap_processing/tests/idex/imap_idex_l0_raw_20230725_v001.pkts +0 -0
  120. imap_processing/tests/mag/imap_mag_l1a_burst-magi_20231025_v001.cdf +0 -0
  121. /imap_processing/tests/hit/test_data/{imap_hit_l0_hk_20100105_v001.pkts → imap_hit_l0_raw_20100105_v001.pkts} +0 -0
  122. {imap_processing-0.7.0.dist-info → imap_processing-0.8.0.dist-info}/LICENSE +0 -0
  123. {imap_processing-0.7.0.dist-info → imap_processing-0.8.0.dist-info}/WHEEL +0 -0
  124. {imap_processing-0.7.0.dist-info → imap_processing-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -1,109 +1,112 @@
1
1
  """Decommutate HIT CCSDS science data."""
2
2
 
3
- from collections import namedtuple
4
-
5
3
  import numpy as np
6
4
  import xarray as xr
7
5
 
6
+ from imap_processing.hit.l0.constants import (
7
+ COUNTS_DATA_STRUCTURE,
8
+ EXPONENT_BITS,
9
+ FLAG_PATTERN,
10
+ FRAME_SIZE,
11
+ MANTISSA_BITS,
12
+ MOD_10_MAPPING,
13
+ )
8
14
  from imap_processing.utils import convert_to_binary_string
9
15
 
10
- # TODO: Consider moving global values into a config file
11
-
12
- # Structure to hold binary details for a
13
- # section of science data. Used to unpack
14
- # binary data.
15
- HITPacking = namedtuple(
16
- "HITPacking",
17
- [
18
- "bit_length",
19
- "section_length",
20
- "shape",
21
- ],
22
- )
23
16
 
24
- # Define data structure for counts rates data
25
- COUNTS_DATA_STRUCTURE = {
26
- # field: bit_length, section_length, shape
27
- # ------------------------------------------
28
- # science frame header
29
- "hdr_unit_num": HITPacking(2, 2, (1,)),
30
- "hdr_frame_version": HITPacking(6, 6, (1,)),
31
- "hdr_status_bits": HITPacking(8, 8, (1,)),
32
- "hdr_minute_cnt": HITPacking(8, 8, (1,)),
33
- # ------------------------------------------
34
- # spare bits. Contains no data
35
- "spare": HITPacking(24, 24, (1,)),
36
- # ------------------------------------------
37
- # erates - contains livetime counters
38
- "livetime": HITPacking(16, 16, (1,)), # livetime counter
39
- "num_trig": HITPacking(16, 16, (1,)), # number of triggers
40
- "num_reject": HITPacking(16, 16, (1,)), # number of rejected events
41
- "num_acc_w_pha": HITPacking(
42
- 16, 16, (1,)
43
- ), # number of accepted events with PHA data
44
- "num_acc_no_pha": HITPacking(16, 16, (1,)), # number of events without PHA data
45
- "num_haz_trig": HITPacking(16, 16, (1,)), # number of triggers with hazard flag
46
- "num_haz_reject": HITPacking(
47
- 16, 16, (1,)
48
- ), # number of rejected events with hazard flag
49
- "num_haz_acc_w_pha": HITPacking(
50
- 16, 16, (1,)
51
- ), # number of accepted hazard events with PHA data
52
- "num_haz_acc_no_pha": HITPacking(
53
- 16, 16, (1,)
54
- ), # number of hazard events without PHA data
55
- # -------------------------------------------
56
- "sngrates": HITPacking(16, 1856, (2, 58)), # single rates
57
- # -------------------------------------------
58
- # evprates - contains event processing rates
59
- "nread": HITPacking(16, 16, (1,)), # events read from event fifo
60
- "nhazard": HITPacking(16, 16, (1,)), # events tagged with hazard flag
61
- "nadcstim": HITPacking(16, 16, (1,)), # adc-stim events
62
- "nodd": HITPacking(16, 16, (1,)), # odd events
63
- "noddfix": HITPacking(16, 16, (1,)), # odd events that were fixed in sw
64
- "nmulti": HITPacking(
65
- 16, 16, (1,)
66
- ), # events with multiple hits in a single detector
67
- "nmultifix": HITPacking(16, 16, (1,)), # multi events that were fixed in sw
68
- "nbadtraj": HITPacking(16, 16, (1,)), # bad trajectory
69
- "nl2": HITPacking(16, 16, (1,)), # events sorted into L12 event category
70
- "nl3": HITPacking(16, 16, (1,)), # events sorted into L123 event category
71
- "nl4": HITPacking(16, 16, (1,)), # events sorted into L1423 event category
72
- "npen": HITPacking(16, 16, (1,)), # events sorted into penetrating event category
73
- "nformat": HITPacking(16, 16, (1,)), # nothing currently goes in this slot
74
- "naside": HITPacking(16, 16, (1,)), # A-side events
75
- "nbside": HITPacking(16, 16, (1,)), # B-side events
76
- "nerror": HITPacking(16, 16, (1,)), # events that caused a processing error
77
- "nbadtags": HITPacking(
78
- 16, 16, (1,)
79
- ), # events with inconsistent tags vs pulse heights
80
- # -------------------------------------------
81
- # other count rates
82
- "coinrates": HITPacking(16, 416, (26,)), # coincidence rates
83
- "bufrates": HITPacking(16, 512, (32,)), # priority buffer rates
84
- "l2fgrates": HITPacking(16, 2112, (132,)), # range 2 foreground rates
85
- "l2bgrates": HITPacking(16, 192, (12,)), # range 2 background rates
86
- "l3fgrates": HITPacking(16, 2672, (167,)), # range 3 foreground rates
87
- "l3bgrates": HITPacking(16, 192, (12,)), # range 3 background rates
88
- "penfgrates": HITPacking(16, 528, (33,)), # range 4 foreground rates
89
- "penbgrates": HITPacking(16, 240, (15,)), # range 4 background rates
90
- "ialirtrates": HITPacking(16, 320, (20,)), # ialirt rates
91
- "sectorates": HITPacking(16, 1920, (120,)), # sectored rates
92
- "l4fgrates": HITPacking(16, 768, (48,)), # all range foreground rates
93
- "l4bgrates": HITPacking(16, 384, (24,)), # all range foreground rates
94
- }
95
-
96
- # Define data structure for pulse height event data
97
- PHA_DATA_STRUCTURE = {
98
- # field: bit_length, section_length, shape
99
- "pha_records": HITPacking(2, 29344, (917,)),
100
- }
101
-
102
- # Define the pattern of grouping flags in a complete science frame.
103
- FLAG_PATTERN = np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2])
104
-
105
- # Define size of science frame (num of packets)
106
- FRAME_SIZE = len(FLAG_PATTERN)
17
+ def subcom_sectorates(sci_dataset: xr.Dataset) -> None:
18
+ """
19
+ Subcommutate sectorates data.
20
+
21
+ Sector rates data contains rates for 5 species and 10
22
+ energy ranges. This function subcommutates the sector
23
+ rates data by organizing the rates by species. Which
24
+ species and energy range the data belongs to is determined
25
+ by taking the mod 10 value of the corresponding header
26
+ minute count value in the dataset. A mapping of mod 10
27
+ values to species and energy ranges is provided in constants.py.
28
+
29
+ MOD_10_MAPPING = {
30
+ 0: {"species": "H", "energy_min": 1.8, "energy_max": 3.6},
31
+ 1: {"species": "H", "energy_min": 4, "energy_max": 6},
32
+ 2: {"species": "H", "energy_min": 6, "energy_max": 10},
33
+ 3: {"species": "4He", "energy_min": 4, "energy_max": 6},
34
+ ...
35
+ 9: {"species": "Fe", "energy_min": 4, "energy_max": 12}}
36
+
37
+ The data is added to the dataset as new data fields named
38
+ according to their species. They have 4 dimensions: epoch
39
+ energy index, declination, and azimuth. The energy index
40
+ dimension is used to distinguish between the different energy
41
+ ranges the data belongs to. The energy min and max values for
42
+ each species are also added to the dataset as new data fields.
43
+
44
+ Parameters
45
+ ----------
46
+ sci_dataset : xr.Dataset
47
+ Xarray dataset containing parsed HIT science data.
48
+ """
49
+ # TODO:
50
+ # - Update to use fill values defined in attribute manager which
51
+ # isn't passed into this module nor defined for L1A sci data yet
52
+ # - Determine naming convention for species data fields in dataset
53
+ # (i.e. h, H, hydrogen, Hydrogen, etc.)
54
+ # - Remove raw "sectorates" data from dataset after processing is complete?
55
+ # - consider moving this function to hit_l1a.py
56
+
57
+ # Calculate mod 10 values
58
+ hdr_min_count_mod_10 = sci_dataset.hdr_minute_cnt.values % 10
59
+
60
+ # Reference mod 10 mapping to initialize data structure for species and
61
+ # energy ranges and add 8x15 arrays with fill values for each science frame.
62
+ num_frames = len(hdr_min_count_mod_10)
63
+ data_by_species_and_energy_range = {
64
+ key: {**value, "rates": np.full((num_frames, 8, 15), fill_value=np.nan)}
65
+ for key, value in MOD_10_MAPPING.items()
66
+ }
67
+
68
+ # Update rates for science frames where data is available
69
+ for i, mod_10 in enumerate(hdr_min_count_mod_10):
70
+ data_by_species_and_energy_range[mod_10]["rates"][i] = sci_dataset[
71
+ "sectorates"
72
+ ].values[i]
73
+
74
+ # H has 3 energy ranges, 4He, CNO, NeMgSi have 2, and Fe has 1.
75
+ # Aggregate sector rates and energy min/max values for each species.
76
+ # First, initialize dictionaries to store rates and min/max energy values by species
77
+ data_by_species: dict = {
78
+ value["species"]: {"rates": [], "energy_min": [], "energy_max": []}
79
+ for value in data_by_species_and_energy_range.values()
80
+ }
81
+
82
+ for value in data_by_species_and_energy_range.values():
83
+ species = value["species"]
84
+ data_by_species[species]["rates"].append(value["rates"])
85
+ data_by_species[species]["energy_min"].append(value["energy_min"])
86
+ data_by_species[species]["energy_max"].append(value["energy_max"])
87
+
88
+ # Add sector rates by species to the dataset
89
+ for species, data in data_by_species.items():
90
+ # Rates data has shape: energy_index, epoch, declination, azimuth
91
+ # Convert rates to numpy array and transpose axes to get
92
+ # shape: epoch, energy_index, declination, azimuth
93
+ rates_data = np.transpose(np.array(data["rates"]), axes=(1, 0, 2, 3))
94
+
95
+ sci_dataset[species] = xr.DataArray(
96
+ data=rates_data,
97
+ dims=["epoch", f"{species}_energy_index", "declination", "azimuth"],
98
+ name=species,
99
+ )
100
+ sci_dataset[f"{species}_energy_min"] = xr.DataArray(
101
+ data=np.array(data["energy_min"]),
102
+ dims=[f"{species}_energy_index"],
103
+ name=f"{species}_energy_min",
104
+ )
105
+ sci_dataset[f"{species}_energy_max"] = xr.DataArray(
106
+ data=np.array(data["energy_max"]),
107
+ dims=[f"{species}_energy_index"],
108
+ name=f"{species}_energy_max",
109
+ )
107
110
 
108
111
 
109
112
  def parse_data(bin_str: str, bits_per_index: int, start: int, end: int) -> list:
@@ -138,13 +141,13 @@ def parse_count_rates(sci_dataset: xr.Dataset) -> None:
138
141
  Parse binary count rates data and update dataset.
139
142
 
140
143
  This function parses the binary count rates data,
141
- stored as count_rates_binary in the dataset,
144
+ stored as count_rates_raw in the dataset,
142
145
  according to data structure details provided in
143
146
  COUNTS_DATA_STRUCTURE. The parsed data, representing
144
147
  integers, is added to the dataset as new data
145
148
  fields.
146
149
 
147
- Note: count_rates_binary is added to the dataset by
150
+ Note: count_rates_raw is added to the dataset by
148
151
  the assemble_science_frames function, which organizes
149
152
  the binary science data packets by science frames.
150
153
 
@@ -154,7 +157,7 @@ def parse_count_rates(sci_dataset: xr.Dataset) -> None:
154
157
  Xarray dataset containing HIT science packets
155
158
  from a CCSDS file.
156
159
  """
157
- counts_binary = sci_dataset.count_rates_binary
160
+ counts_binary = sci_dataset.count_rates_raw
158
161
  # initialize the starting bit for the sections of data
159
162
  section_start = 0
160
163
  # Decommutate binary data for each counts data field
@@ -176,17 +179,18 @@ def parse_count_rates(sci_dataset: xr.Dataset) -> None:
176
179
  low_gain = data[1::2] # Items at odd indices 1, 3, 5, etc.
177
180
  parsed_data[i] = [high_gain, low_gain]
178
181
 
179
- # TODO
180
- # - status bits needs to be further parsed (table 10 in algorithm doc)
181
- # - subcommutate sectorates
182
- # - decompress data
183
- # - Follow up with HIT team about erates and evrates.
184
- # (i.e.Should these be arrays containing all the sub fields
185
- # or should each subfield be it's own data field/array)
182
+ # Decompress data where needed
183
+ if all(x not in field for x in ["hdr", "spare", "pha"]):
184
+ parsed_data = np.vectorize(decompress_rates_16_to_32)(parsed_data)
186
185
 
187
186
  # Get dims for data variables (yaml file not created yet)
188
187
  if len(field_meta.shape) > 1:
189
- dims = ["epoch", "gain", f"{field}_index"]
188
+ if "sectorates" in field:
189
+ # Reshape data to 8x15 for declination and azimuth look directions
190
+ parsed_data = np.array(parsed_data).reshape((-1, *field_meta.shape))
191
+ dims = ["epoch", "declination", "azimuth"]
192
+ elif "sngrates" in field:
193
+ dims = ["epoch", "gain", f"{field}_index"]
190
194
  elif field_meta.shape[0] > 1:
191
195
  dims = ["epoch", f"{field}_index"]
192
196
  else:
@@ -214,7 +218,7 @@ def is_sequential(counters: np.ndarray) -> np.bool_:
214
218
  return np.all(np.diff(counters) == 1)
215
219
 
216
220
 
217
- def find_valid_starting_indices(flags: np.ndarray, counters: np.ndarray) -> np.ndarray:
221
+ def get_valid_starting_indices(flags: np.ndarray, counters: np.ndarray) -> np.ndarray:
218
222
  """
219
223
  Find valid starting indices for science frames.
220
224
 
@@ -241,9 +245,6 @@ def find_valid_starting_indices(flags: np.ndarray, counters: np.ndarray) -> np.n
241
245
  valid_indices : np.ndarray
242
246
  Array of valid indices for science frames.
243
247
  """
244
- # TODO: consider combining functions to get valid indices to reduce
245
- # code tracing
246
-
247
248
  # Use sliding windows to compare segments of the array (20 packets) with the
248
249
  # pattern. This generates an array of overlapping sub-arrays, each of length
249
250
  # 20, from the flags array and is used to slide the "window" across the array
@@ -254,40 +255,13 @@ def find_valid_starting_indices(flags: np.ndarray, counters: np.ndarray) -> np.n
254
255
  # Get the starting indices of matches
255
256
  match_indices = np.where(matches)[0]
256
257
  # Filter for only indices from valid science frames with sequential counters
257
- valid_indices = get_valid_indices(match_indices, counters, FRAME_SIZE)
258
+ sequential_check = [
259
+ is_sequential(counters[idx : idx + FRAME_SIZE]) for idx in match_indices
260
+ ]
261
+ valid_indices: np.ndarray = np.array(match_indices[sequential_check], dtype=int)
258
262
  return valid_indices
259
263
 
260
264
 
261
- def get_valid_indices(
262
- indices: np.ndarray, counters: np.ndarray, size: int
263
- ) -> np.ndarray:
264
- """
265
- Get valid indices for science frames.
266
-
267
- Check if the packet sequence counters for the science frames
268
- are sequential. If they are, the science frame is valid and
269
- an updated array of valid indices is returned.
270
-
271
- Parameters
272
- ----------
273
- indices : np.ndarray
274
- Array of indices where the packet grouping flags match the pattern.
275
- counters : np.ndarray
276
- Array of packet sequence counters.
277
- size : int
278
- Size of science frame. 20 packets per science frame.
279
-
280
- Returns
281
- -------
282
- valid_indices : np.ndarray
283
- Array of valid indices for science frames.
284
- """
285
- # Check if the packet sequence counters are sequential by getting an array
286
- # of boolean values where True indicates the counters are sequential.
287
- sequential_check = [is_sequential(counters[idx : idx + size]) for idx in indices]
288
- return indices[sequential_check]
289
-
290
-
291
265
  def update_ccsds_header_dims(sci_dataset: xr.Dataset) -> xr.Dataset:
292
266
  """
293
267
  Update dimensions of CCSDS header fields.
@@ -334,8 +308,8 @@ def assemble_science_frames(sci_dataset: xr.Dataset) -> xr.Dataset:
334
308
  The first six packets contain count rates data
335
309
  The last 14 packets contain pulse height event data
336
310
 
337
- These groups are added to the dataset as count_rates_binary
338
- and pha_binary.
311
+ These groups are added to the dataset as count_rates_raw
312
+ and pha_raw.
339
313
 
340
314
  Parameters
341
315
  ----------
@@ -368,7 +342,7 @@ def assemble_science_frames(sci_dataset: xr.Dataset) -> xr.Dataset:
368
342
  total_packets = len(epoch_data)
369
343
 
370
344
  # Find starting indices for valid science frames
371
- starting_indices = find_valid_starting_indices(seq_flgs, seq_ctrs)
345
+ starting_indices = get_valid_starting_indices(seq_flgs, seq_ctrs)
372
346
 
373
347
  # Check for extra packets at start and end of file
374
348
  # TODO: Will need to handle these extra packets when processing multiple files
@@ -400,19 +374,75 @@ def assemble_science_frames(sci_dataset: xr.Dataset) -> xr.Dataset:
400
374
  pha.append("".join(science_data_frame[6:]))
401
375
  # Get first packet's epoch for the science frame
402
376
  epoch_per_science_frame = np.append(epoch_per_science_frame, epoch_data[idx])
403
- # TODO: Filter ccsds header fields to only include packets from the
404
- # valid science frames. Doesn't need to be grouped by frames though
405
377
 
406
378
  # Add new data variables to the dataset
407
379
  sci_dataset = sci_dataset.drop_vars("epoch")
408
380
  sci_dataset.coords["epoch"] = epoch_per_science_frame
409
- sci_dataset["count_rates_binary"] = xr.DataArray(
410
- count_rates, dims=["epoch"], name="count_rates_binary"
381
+ sci_dataset["count_rates_raw"] = xr.DataArray(
382
+ count_rates, dims=["epoch"], name="count_rates_raw"
411
383
  )
412
- sci_dataset["pha_binary"] = xr.DataArray(pha, dims=["epoch"], name="pha_binary")
384
+ sci_dataset["pha_raw"] = xr.DataArray(pha, dims=["epoch"], name="pha_raw")
413
385
  return sci_dataset
414
386
 
415
387
 
388
+ def decompress_rates_16_to_32(packed: int) -> int:
389
+ """
390
+ Will decompress rates data from 16 bits to 32 bits.
391
+
392
+ This function decompresses the rates data from 16-bit integers
393
+ to 32-bit integers. The compressed integer (packed) combines
394
+ two parts:
395
+
396
+ 1. Mantissa: Represents the significant digits of the value.
397
+ 2. Exponent: Determines how much to scale the mantissa (using powers of 2).
398
+
399
+ These parts are packed together into a single 16-bit integer.
400
+
401
+ Parameters
402
+ ----------
403
+ packed : int
404
+ Compressed 16-bit integer.
405
+
406
+ Returns
407
+ -------
408
+ decompressed_int : int
409
+ Decompressed integer.
410
+ """
411
+ # In compressed formats, the exponent and mantissa are tightly packed together.
412
+ # The mask ensures you correctly separate the mantissa (useful for reconstructing
413
+ # the value) from the exponent (used for scaling).
414
+ # set to 16 bits
415
+ output_mask = 0xFFFF
416
+
417
+ # Packed is the compressed integer
418
+ # Right bit shift to get the exponent
419
+ power = packed >> MANTISSA_BITS
420
+
421
+ # Decompress the data depending on the value of the exponent
422
+ # If the exponent (power) extracted from the packed 16-bit integer is greater
423
+ # than 1, the compressed value needs to be decompressed by reconstructing the
424
+ # integer using the mantissa and exponent. If the condition is false, the
425
+ # compressed and uncompressed values are considered the same.
426
+ decompressed_int: int
427
+ if power > 1:
428
+ # Retrieve the "mantissa" portion of the packed value by masking out the
429
+ # exponent bits
430
+ mantissa_mask = output_mask >> EXPONENT_BITS
431
+ mantissa = packed & mantissa_mask
432
+
433
+ # Shift the mantissa to the left by 1 to account for the hidden bit
434
+ # (always set to 1)
435
+ mantissa_with_hidden_bit = mantissa | (0x0001 << MANTISSA_BITS)
436
+
437
+ # Scale the mantissa by the exponent by shifting it to the left by (power - 1)
438
+ decompressed_int = mantissa_with_hidden_bit << (power - 1)
439
+ else:
440
+ # The compressed and uncompressed values are the same
441
+ decompressed_int = packed
442
+
443
+ return decompressed_int
444
+
445
+
416
446
  def decom_hit(sci_dataset: xr.Dataset) -> xr.Dataset:
417
447
  """
418
448
  Group and decode HIT science data packets.
@@ -465,7 +495,10 @@ def decom_hit(sci_dataset: xr.Dataset) -> xr.Dataset:
465
495
  # Parse count rates data from binary and add to dataset
466
496
  parse_count_rates(sci_dataset)
467
497
 
498
+ # Further organize sector rates by species type
499
+ subcom_sectorates(sci_dataset)
500
+
468
501
  # TODO:
469
- # Parse binary PHA data and add to dataset (function call)
502
+ # -clean up dataset - remove raw binary data, raw sectorates? Any other fields?
470
503
 
471
504
  return sci_dataset