imap-processing 0.17.0__py3-none-any.whl → 0.19.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 (141) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ancillary/ancillary_dataset_combiner.py +161 -1
  3. imap_processing/ccsds/excel_to_xtce.py +12 -0
  4. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -6
  5. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +312 -274
  6. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +39 -28
  7. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1048 -183
  8. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  9. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +12 -0
  10. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +5 -0
  11. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +10 -4
  12. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +163 -100
  13. imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml +4 -4
  14. imap_processing/cdf/config/imap_ialirt_l1_variable_attrs.yaml +97 -54
  15. imap_processing/cdf/config/imap_idex_l2a_variable_attrs.yaml +33 -4
  16. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +44 -44
  17. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +77 -61
  18. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +30 -0
  19. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  20. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  21. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +99 -2
  22. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  23. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +60 -0
  24. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +99 -11
  25. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +50 -7
  26. imap_processing/cli.py +121 -44
  27. imap_processing/codice/codice_l1a.py +165 -77
  28. imap_processing/codice/codice_l1b.py +1 -1
  29. imap_processing/codice/codice_l2.py +118 -19
  30. imap_processing/codice/constants.py +1217 -1089
  31. imap_processing/decom.py +1 -4
  32. imap_processing/ena_maps/ena_maps.py +32 -25
  33. imap_processing/ena_maps/utils/naming.py +8 -2
  34. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  35. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  36. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  37. imap_processing/glows/ancillary/imap_glows_pipeline_settings_20250923_v002.json +54 -0
  38. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  39. imap_processing/glows/l1b/glows_l1b.py +99 -9
  40. imap_processing/glows/l1b/glows_l1b_data.py +350 -38
  41. imap_processing/glows/l2/glows_l2.py +11 -0
  42. imap_processing/hi/hi_l1a.py +124 -3
  43. imap_processing/hi/hi_l1b.py +154 -71
  44. imap_processing/hi/hi_l2.py +84 -51
  45. imap_processing/hi/utils.py +153 -8
  46. imap_processing/hit/l0/constants.py +3 -0
  47. imap_processing/hit/l0/decom_hit.py +5 -8
  48. imap_processing/hit/l1a/hit_l1a.py +375 -45
  49. imap_processing/hit/l1b/constants.py +5 -0
  50. imap_processing/hit/l1b/hit_l1b.py +61 -131
  51. imap_processing/hit/l2/constants.py +1 -1
  52. imap_processing/hit/l2/hit_l2.py +10 -11
  53. imap_processing/ialirt/calculate_ingest.py +219 -0
  54. imap_processing/ialirt/constants.py +32 -1
  55. imap_processing/ialirt/generate_coverage.py +201 -0
  56. imap_processing/ialirt/l0/ialirt_spice.py +5 -2
  57. imap_processing/ialirt/l0/parse_mag.py +337 -29
  58. imap_processing/ialirt/l0/process_hit.py +5 -3
  59. imap_processing/ialirt/l0/process_swapi.py +41 -25
  60. imap_processing/ialirt/l0/process_swe.py +23 -7
  61. imap_processing/ialirt/process_ephemeris.py +70 -14
  62. imap_processing/ialirt/utils/constants.py +22 -16
  63. imap_processing/ialirt/utils/create_xarray.py +42 -19
  64. imap_processing/idex/idex_constants.py +1 -5
  65. imap_processing/idex/idex_l0.py +2 -2
  66. imap_processing/idex/idex_l1a.py +2 -3
  67. imap_processing/idex/idex_l1b.py +2 -3
  68. imap_processing/idex/idex_l2a.py +130 -4
  69. imap_processing/idex/idex_l2b.py +313 -119
  70. imap_processing/idex/idex_utils.py +1 -3
  71. imap_processing/lo/l0/lo_apid.py +1 -0
  72. imap_processing/lo/l0/lo_science.py +25 -24
  73. imap_processing/lo/l1a/lo_l1a.py +44 -0
  74. imap_processing/lo/l1b/lo_l1b.py +3 -3
  75. imap_processing/lo/l1c/lo_l1c.py +116 -50
  76. imap_processing/lo/l2/lo_l2.py +29 -29
  77. imap_processing/lo/lo_ancillary.py +55 -0
  78. imap_processing/lo/packet_definitions/lo_xtce.xml +5359 -106
  79. imap_processing/mag/constants.py +1 -0
  80. imap_processing/mag/l1a/mag_l1a.py +1 -0
  81. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  82. imap_processing/mag/l1b/mag_l1b.py +3 -2
  83. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  84. imap_processing/mag/l1c/mag_l1c.py +23 -6
  85. imap_processing/mag/l1d/__init__.py +0 -0
  86. imap_processing/mag/l1d/mag_l1d.py +176 -0
  87. imap_processing/mag/l1d/mag_l1d_data.py +725 -0
  88. imap_processing/mag/l2/__init__.py +0 -0
  89. imap_processing/mag/l2/mag_l2.py +25 -20
  90. imap_processing/mag/l2/mag_l2_data.py +199 -130
  91. imap_processing/quality_flags.py +28 -2
  92. imap_processing/spice/geometry.py +101 -36
  93. imap_processing/spice/pointing_frame.py +1 -7
  94. imap_processing/spice/repoint.py +29 -2
  95. imap_processing/spice/spin.py +32 -8
  96. imap_processing/spice/time.py +60 -19
  97. imap_processing/swapi/l1/swapi_l1.py +10 -4
  98. imap_processing/swapi/l2/swapi_l2.py +66 -24
  99. imap_processing/swapi/swapi_utils.py +1 -1
  100. imap_processing/swe/l1b/swe_l1b.py +3 -6
  101. imap_processing/ultra/constants.py +28 -3
  102. imap_processing/ultra/l0/decom_tools.py +15 -8
  103. imap_processing/ultra/l0/decom_ultra.py +35 -11
  104. imap_processing/ultra/l0/ultra_utils.py +102 -12
  105. imap_processing/ultra/l1a/ultra_l1a.py +26 -6
  106. imap_processing/ultra/l1b/cullingmask.py +6 -3
  107. imap_processing/ultra/l1b/de.py +122 -26
  108. imap_processing/ultra/l1b/extendedspin.py +29 -2
  109. imap_processing/ultra/l1b/lookup_utils.py +424 -50
  110. imap_processing/ultra/l1b/quality_flag_filters.py +23 -0
  111. imap_processing/ultra/l1b/ultra_l1b_culling.py +356 -5
  112. imap_processing/ultra/l1b/ultra_l1b_extended.py +534 -90
  113. imap_processing/ultra/l1c/helio_pset.py +127 -7
  114. imap_processing/ultra/l1c/l1c_lookup_utils.py +256 -0
  115. imap_processing/ultra/l1c/spacecraft_pset.py +90 -15
  116. imap_processing/ultra/l1c/ultra_l1c.py +6 -0
  117. imap_processing/ultra/l1c/ultra_l1c_culling.py +85 -0
  118. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +446 -341
  119. imap_processing/ultra/l2/ultra_l2.py +0 -1
  120. imap_processing/ultra/utils/ultra_l1_utils.py +40 -3
  121. imap_processing/utils.py +3 -4
  122. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/METADATA +3 -3
  123. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/RECORD +126 -126
  124. imap_processing/idex/idex_l2c.py +0 -250
  125. imap_processing/spice/kernels.py +0 -187
  126. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +0 -526
  127. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +0 -526
  128. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +0 -526
  129. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +0 -524
  130. imap_processing/ultra/lookup_tables/EgyNorm.mem.csv +0 -32769
  131. imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  132. imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  133. imap_processing/ultra/lookup_tables/dps_grid45_compressed.cdf +0 -0
  134. imap_processing/ultra/lookup_tables/ultra45_back-pos-luts.csv +0 -4097
  135. imap_processing/ultra/lookup_tables/ultra45_tdc_norm.csv +0 -2050
  136. imap_processing/ultra/lookup_tables/ultra90_back-pos-luts.csv +0 -4097
  137. imap_processing/ultra/lookup_tables/ultra90_tdc_norm.csv +0 -2050
  138. imap_processing/ultra/lookup_tables/yadjust.csv +0 -257
  139. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/LICENSE +0 -0
  140. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/WHEEL +0 -0
  141. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/entry_points.txt +0 -0
@@ -37,28 +37,28 @@ HistPacking = namedtuple(
37
37
 
38
38
  HIST_DATA_META = {
39
39
  # field: bit_length, section_length, shape
40
- "start_a": HistPacking(12, 504, (6, 7)),
41
- "start_c": HistPacking(12, 504, (6, 7)),
42
- "stop_b0": HistPacking(12, 504, (6, 7)),
43
- "stop_b3": HistPacking(12, 504, (6, 7)),
44
- "tof0_count": HistPacking(8, 336, (6, 7)),
45
- "tof1_count": HistPacking(8, 336, (6, 7)),
46
- "tof2_count": HistPacking(8, 336, (6, 7)),
47
- "tof3_count": HistPacking(8, 336, (6, 7)),
48
- "tof0_tof1": HistPacking(8, 3360, (60, 7)),
49
- "tof0_tof2": HistPacking(8, 3360, (60, 7)),
50
- "tof1_tof2": HistPacking(8, 3360, (60, 7)),
51
- "silver": HistPacking(8, 3360, (60, 7)),
52
- "disc_tof0": HistPacking(8, 336, (6, 7)),
53
- "disc_tof1": HistPacking(8, 336, (6, 7)),
54
- "disc_tof2": HistPacking(8, 336, (6, 7)),
55
- "disc_tof3": HistPacking(8, 336, (6, 7)),
56
- "pos0": HistPacking(12, 504, (6, 7)),
57
- "pos1": HistPacking(12, 504, (6, 7)),
58
- "pos2": HistPacking(12, 504, (6, 7)),
59
- "pos3": HistPacking(12, 504, (6, 7)),
60
- "hydrogen": HistPacking(8, 3360, (60, 7)),
61
- "oxygen": HistPacking(8, 3360, (60, 7)),
40
+ "start_a": HistPacking(12, 504, (7, 6)),
41
+ "start_c": HistPacking(12, 504, (7, 6)),
42
+ "stop_b0": HistPacking(12, 504, (7, 6)),
43
+ "stop_b3": HistPacking(12, 504, (7, 6)),
44
+ "tof0_count": HistPacking(8, 336, (7, 6)),
45
+ "tof1_count": HistPacking(8, 336, (7, 6)),
46
+ "tof2_count": HistPacking(8, 336, (7, 6)),
47
+ "tof3_count": HistPacking(8, 336, (7, 6)),
48
+ "tof0_tof1": HistPacking(8, 3360, (7, 60)),
49
+ "tof0_tof2": HistPacking(8, 3360, (7, 60)),
50
+ "tof1_tof2": HistPacking(8, 3360, (7, 60)),
51
+ "silver": HistPacking(8, 3360, (7, 60)),
52
+ "disc_tof0": HistPacking(8, 336, (7, 6)),
53
+ "disc_tof1": HistPacking(8, 336, (7, 6)),
54
+ "disc_tof2": HistPacking(8, 336, (7, 6)),
55
+ "disc_tof3": HistPacking(8, 336, (7, 6)),
56
+ "pos0": HistPacking(12, 504, (7, 6)),
57
+ "pos1": HistPacking(12, 504, (7, 6)),
58
+ "pos2": HistPacking(12, 504, (7, 6)),
59
+ "pos3": HistPacking(12, 504, (7, 6)),
60
+ "hydrogen": HistPacking(8, 3360, (7, 60)),
61
+ "oxygen": HistPacking(8, 3360, (7, 60)),
62
62
  }
63
63
 
64
64
 
@@ -399,7 +399,7 @@ def combine_segmented_packets(dataset: xr.Dataset) -> xr.Dataset:
399
399
  # Combine the segmented packets into a single binary string
400
400
  dataset["events"] = [
401
401
  "".join(dataset["data"].values[start : end + 1])
402
- for start, end in zip(seg_starts, seg_ends)
402
+ for start, end in zip(seg_starts, seg_ends, strict=False)
403
403
  ]
404
404
 
405
405
  # drop any group of segmented packets that aren't sequential
@@ -441,7 +441,8 @@ def find_valid_groups(
441
441
  """
442
442
  # Check if the sequence counters from the CCSDS header are sequential
443
443
  grouped_seq_ctrs = [
444
- np.array(seq_ctrs[start : end + 1]) for start, end in zip(seg_starts, seg_ends)
444
+ np.array(seq_ctrs[start : end + 1])
445
+ for start, end in zip(seg_starts, seg_ends, strict=False)
445
446
  ]
446
447
  valid_groups = [is_sequential(seq_ctrs) for seq_ctrs in grouped_seq_ctrs]
447
448
  return valid_groups
@@ -45,6 +45,11 @@ def lo_l1a(dependency: Path) -> list[xr.Dataset]:
45
45
  xtce_packet_definition=xtce_file.resolve(),
46
46
  use_derived_value=False,
47
47
  )
48
+ datasets_by_apid_derived = packet_file_to_datasets(
49
+ packet_file=dependency.resolve(),
50
+ xtce_packet_definition=xtce_file.resolve(),
51
+ use_derived_value=True,
52
+ )
48
53
 
49
54
  # create the attribute manager for this data level
50
55
  attr_mgr = ImapCdfAttributes()
@@ -101,6 +106,45 @@ def lo_l1a(dependency: Path) -> list[xr.Dataset]:
101
106
  ds = process_star_sensor(ds)
102
107
  ds = add_dataset_attrs(ds, attr_mgr, logical_source)
103
108
  datasets_to_return.append(ds)
109
+ if LoAPID.ILO_DIAG_PCC in datasets_by_apid:
110
+ logger.info(
111
+ f"\nProcessing {LoAPID(LoAPID.ILO_DIAG_PCC).name} "
112
+ f"packet (APID: {LoAPID.ILO_DIAG_PCC.value})"
113
+ )
114
+ logical_source = "imap_lo_l1a_pcc"
115
+ ds = datasets_by_apid[LoAPID.ILO_DIAG_PCC]
116
+ ds = add_dataset_attrs(ds, attr_mgr, logical_source)
117
+ datasets_to_return.append(ds)
118
+ if LoAPID.ILO_APP_NHK in datasets_by_apid:
119
+ logger.info(
120
+ f"\nProcessing {LoAPID(LoAPID.ILO_APP_NHK).name} "
121
+ f"packet (APID: {LoAPID.ILO_APP_NHK.value})"
122
+ )
123
+ logical_source = "imap_lo_l1a_nhk"
124
+ ds = datasets_by_apid[LoAPID.ILO_APP_NHK]
125
+ ds = add_dataset_attrs(ds, attr_mgr, logical_source)
126
+ datasets_to_return.append(ds)
127
+
128
+ # Engineering units conversion
129
+ logical_source = "imap_lo_l1b_nhk"
130
+ ds = datasets_by_apid_derived[LoAPID.ILO_APP_NHK]
131
+ ds = add_dataset_attrs(ds, attr_mgr, logical_source)
132
+ datasets_to_return.append(ds)
133
+ if LoAPID.ILO_APP_SHK in datasets_by_apid:
134
+ logger.info(
135
+ f"\nProcessing {LoAPID(LoAPID.ILO_APP_SHK).name} "
136
+ f"packet (APID: {LoAPID.ILO_APP_SHK.value})"
137
+ )
138
+ logical_source = "imap_lo_l1a_shk"
139
+ ds = datasets_by_apid[LoAPID.ILO_APP_SHK]
140
+ ds = add_dataset_attrs(ds, attr_mgr, logical_source)
141
+ datasets_to_return.append(ds)
142
+
143
+ # Engineering units conversion
144
+ logical_source = "imap_lo_l1b_shk"
145
+ ds = datasets_by_apid_derived[LoAPID.ILO_APP_SHK]
146
+ ds = add_dataset_attrs(ds, attr_mgr, logical_source)
147
+ datasets_to_return.append(ds)
104
148
 
105
149
  logger.info(f"Returning [{len(datasets_to_return)}] datasets")
106
150
  return datasets_to_return
@@ -3,7 +3,7 @@
3
3
  import logging
4
4
  from dataclasses import Field
5
5
  from pathlib import Path
6
- from typing import Any, Union
6
+ from typing import Any
7
7
 
8
8
  import numpy as np
9
9
  import xarray as xr
@@ -204,7 +204,7 @@ def get_avg_spin_durations_per_cycle(
204
204
  return avg_spin_durations_per_cycle
205
205
 
206
206
 
207
- def get_spin_angle(l1a_de: xr.Dataset) -> Union[np.ndarray[np.float64], Any]:
207
+ def get_spin_angle(l1a_de: xr.Dataset) -> np.ndarray[np.float64] | Any:
208
208
  """
209
209
  Get the spin angle (0 - 360 degrees) for each DE.
210
210
 
@@ -587,7 +587,7 @@ def convert_tofs_to_eu(
587
587
  tof_conversions = [TOF0_CONV, TOF1_CONV, TOF2_CONV, TOF3_CONV]
588
588
 
589
589
  # Loop through the TOF fields and convert them to engineering units
590
- for tof, conv in zip(tof_fields, tof_conversions):
590
+ for tof, conv in zip(tof_fields, tof_conversions, strict=False):
591
591
  # Get the fill value for the L1A and L1B TOF
592
592
  fillval_1a = attr_mgr_l1a.get_variable_attributes(tof)["FILLVAL"]
593
593
  fillval_1b = attr_mgr_l1b.get_variable_attributes(tof)["FILLVAL"]
@@ -4,12 +4,26 @@ from dataclasses import Field
4
4
  from enum import Enum
5
5
 
6
6
  import numpy as np
7
- import pandas as pd
8
7
  import xarray as xr
9
8
  from scipy.stats import binned_statistic_dd
10
9
 
11
10
  from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
12
- from imap_processing.spice.time import met_to_ttj2000ns
11
+ from imap_processing.lo import lo_ancillary
12
+ from imap_processing.spice.repoint import get_pointing_times
13
+ from imap_processing.spice.spin import get_spin_number
14
+ from imap_processing.spice.time import met_to_ttj2000ns, ttj2000ns_to_met
15
+
16
+ N_ESA_ENERGY_STEPS = 7
17
+ N_SPIN_ANGLE_BINS = 3600
18
+ N_OFF_ANGLE_BINS = 40
19
+ # 1 time, 7 energy steps, 3600 spin angle bins, and 40 off angle bins
20
+ PSET_SHAPE = (1, N_ESA_ENERGY_STEPS, N_SPIN_ANGLE_BINS, N_OFF_ANGLE_BINS)
21
+ PSET_DIMS = ["epoch", "esa_energy_step", "spin_angle", "off_angle"]
22
+ ESA_ENERGY_STEPS = np.arange(N_ESA_ENERGY_STEPS) + 1 # 1 to 7 inclusive
23
+ SPIN_ANGLE_BIN_EDGES = np.linspace(0, 360, N_SPIN_ANGLE_BINS + 1)
24
+ SPIN_ANGLE_BIN_CENTERS = (SPIN_ANGLE_BIN_EDGES[:-1] + SPIN_ANGLE_BIN_EDGES[1:]) / 2
25
+ OFF_ANGLE_BIN_EDGES = np.linspace(-2, 2, N_OFF_ANGLE_BINS + 1)
26
+ OFF_ANGLE_BIN_CENTERS = (OFF_ANGLE_BIN_EDGES[:-1] + OFF_ANGLE_BIN_EDGES[1:]) / 2
13
27
 
14
28
 
15
29
  class FilterType(str, Enum):
@@ -52,10 +66,45 @@ def lo_l1c(sci_dependencies: dict, anc_dependencies: list) -> list[xr.Dataset]:
52
66
  if "imap_lo_l1b_de" in sci_dependencies:
53
67
  logical_source = "imap_lo_l1c_pset"
54
68
  l1b_de = sci_dependencies["imap_lo_l1b_de"]
55
-
56
69
  l1b_goodtimes_only = filter_goodtimes(l1b_de, anc_dependencies)
57
70
  pset = initialize_pset(l1b_goodtimes_only, attr_mgr, logical_source)
58
71
  full_counts = create_pset_counts(l1b_goodtimes_only)
72
+
73
+ # Set the pointing start and end times based on the first epoch
74
+ pointing_start_met, pointing_end_met = get_pointing_times(
75
+ ttj2000ns_to_met(l1b_goodtimes_only["epoch"][0].item())
76
+ )
77
+
78
+ pset["pointing_start_met"] = xr.DataArray(
79
+ np.array([pointing_start_met]),
80
+ dims="epoch",
81
+ attrs=attr_mgr.get_variable_attributes("pointing_start_met"),
82
+ )
83
+ pset["pointing_end_met"] = xr.DataArray(
84
+ np.array([pointing_end_met]),
85
+ dims="epoch",
86
+ attrs=attr_mgr.get_variable_attributes("pointing_end_met"),
87
+ )
88
+
89
+ # Set the epoch to the start of the pointing
90
+ pset["epoch"] = xr.DataArray(
91
+ met_to_ttj2000ns(pset["pointing_start_met"].values),
92
+ attrs=attr_mgr.get_variable_attributes("epoch"),
93
+ )
94
+
95
+ # Get the start and end spin numbers based on the pointing start and end MET
96
+ pset["start_spin_number"] = xr.DataArray(
97
+ [get_spin_number(pset["pointing_start_met"].item())],
98
+ dims="epoch",
99
+ attrs=attr_mgr.get_variable_attributes("start_spin_number"),
100
+ )
101
+ pset["end_spin_number"] = xr.DataArray(
102
+ [get_spin_number(pset["pointing_end_met"].item())],
103
+ dims="epoch",
104
+ attrs=attr_mgr.get_variable_attributes("end_spin_number"),
105
+ )
106
+
107
+ # Set the counts
59
108
  pset["triples_counts"] = create_pset_counts(
60
109
  l1b_goodtimes_only, FilterType.TRIPLES
61
110
  )
@@ -64,19 +113,18 @@ def lo_l1c(sci_dependencies: dict, anc_dependencies: list) -> list[xr.Dataset]:
64
113
  )
65
114
  pset["h_counts"] = create_pset_counts(l1b_goodtimes_only, FilterType.HYDROGEN)
66
115
  pset["o_counts"] = create_pset_counts(l1b_goodtimes_only, FilterType.OXYGEN)
116
+
117
+ # Set the exposure time
67
118
  pset["exposure_time"] = calculate_exposure_times(
68
119
  full_counts, l1b_goodtimes_only
69
120
  )
70
121
  pset.attrs = attr_mgr.get_global_attributes(logical_source)
71
- # TODO: Temp fix before adding attribute variables.
72
- # CDF won't open if DEPEND_0 is not deleted currently.
73
- del pset["epoch"].attrs["DEPEND_0"]
74
122
 
75
123
  pset = pset.assign_coords(
76
124
  {
77
- "energy": np.arange(1, 8),
78
- "longitude": np.arange(3600),
79
- "latitude": np.arange(40),
125
+ "esa_energy_step": ESA_ENERGY_STEPS,
126
+ "spin_angle": SPIN_ANGLE_BIN_CENTERS,
127
+ "off_angle": OFF_ANGLE_BIN_CENTERS,
80
128
  }
81
129
  )
82
130
 
@@ -141,7 +189,7 @@ def filter_goodtimes(l1b_de: xr.Dataset, anc_dependencies: list) -> xr.Dataset:
141
189
  Filtered L1B Direct Event dataset.
142
190
  """
143
191
  # the goodtimes are currently the only ancillary file needed for L1C processing
144
- goodtimes_table_df = pd.read_csv(anc_dependencies[0])
192
+ goodtimes_table_df = lo_ancillary.read_ancillary_file(anc_dependencies[0])
145
193
 
146
194
  # convert goodtimes from MET to TTJ2000
147
195
  goodtimes_start = met_to_ttj2000ns(goodtimes_table_df["GoodTime_strt"])
@@ -151,7 +199,7 @@ def filter_goodtimes(l1b_de: xr.Dataset, anc_dependencies: list) -> xr.Dataset:
151
199
  goodtimes_mask = np.zeros_like(l1b_de["epoch"], dtype=bool)
152
200
 
153
201
  # Iterate over the good times and create a mask
154
- for start, end in zip(goodtimes_start, goodtimes_end):
202
+ for start, end in zip(goodtimes_start, goodtimes_end, strict=False):
155
203
  goodtimes_mask |= (l1b_de["epoch"] >= start) & (l1b_de["epoch"] < end)
156
204
 
157
205
  # Filter the dataset using the mask
@@ -249,7 +297,7 @@ def create_pset_counts(
249
297
 
250
298
  counts = xr.DataArray(
251
299
  data=hist.astype(np.int16),
252
- dims=["epoch", "energy", "longitude", "latitude"],
300
+ dims=PSET_DIMS,
253
301
  )
254
302
 
255
303
  return counts
@@ -275,11 +323,6 @@ def calculate_exposure_times(counts: xr.DataArray, l1b_de: xr.Dataset) -> xr.Dat
275
323
  exposure_time : xarray.DataArray
276
324
  The exposure times for the L1B Direct Event dataset.
277
325
  """
278
- # Create bin edges
279
- lon_edges = np.arange(3601)
280
- lat_edges = np.arange(41)
281
- energy_edges = np.arange(8)
282
-
283
326
  data = np.column_stack(
284
327
  (l1b_de["esa_step"], l1b_de["pointing_bin_lon"], l1b_de["pointing_bin_lat"])
285
328
  )
@@ -289,14 +332,19 @@ def calculate_exposure_times(counts: xr.DataArray, l1b_de: xr.Dataset) -> xr.Dat
289
332
  # exposure time equation from Lo Alg Document 10.1.1.4
290
333
  4 * l1b_de["avg_spin_durations"].to_numpy() / 3600,
291
334
  statistic="mean",
292
- bins=[energy_edges, lon_edges, lat_edges],
335
+ # NOTE: The l1b pointing_bin_lon is bin number, not actual angle
336
+ bins=[
337
+ np.arange(N_ESA_ENERGY_STEPS + 1),
338
+ np.arange(N_SPIN_ANGLE_BINS + 1),
339
+ np.arange(N_OFF_ANGLE_BINS + 1),
340
+ ],
293
341
  )
294
342
 
295
343
  stat = result.statistic[np.newaxis, :, :, :]
296
344
 
297
345
  exposure_time = xr.DataArray(
298
346
  data=stat.astype(np.float16),
299
- dims=["epoch", "energy", "longitude", "latitude"],
347
+ dims=PSET_DIMS,
300
348
  )
301
349
 
302
350
  return exposure_time
@@ -328,8 +376,6 @@ def create_datasets(
328
376
  # can be used direction
329
377
  epoch_converted_time = [1]
330
378
 
331
- # Create a data array for the epoch time
332
- # TODO: might need to update the attrs to use new YAML file
333
379
  epoch_time = xr.DataArray(
334
380
  data=epoch_converted_time,
335
381
  name="epoch",
@@ -338,38 +384,54 @@ def create_datasets(
338
384
  )
339
385
 
340
386
  if logical_source == "imap_lo_l1c_pset":
341
- esa_step = xr.DataArray(
342
- data=[1, 2, 3, 4, 5, 6, 7],
343
- name="esa_step",
344
- dims=["esa_step"],
345
- attrs=attr_mgr.get_variable_attributes("esa_step"),
346
- )
347
- pointing_bins = xr.DataArray(
348
- data=np.arange(3600),
349
- name="pointing_bins",
350
- dims=["pointing_bins"],
351
- attrs=attr_mgr.get_variable_attributes("pointing_bins"),
387
+ esa_energy_step = xr.DataArray(
388
+ data=ESA_ENERGY_STEPS,
389
+ name="esa_energy_step",
390
+ dims=["esa_energy_step"],
391
+ attrs=attr_mgr.get_variable_attributes("esa_energy_step"),
352
392
  )
353
-
354
- esa_step_label = xr.DataArray(
355
- esa_step.values.astype(str),
393
+ esa_energy_step_label = xr.DataArray(
394
+ esa_energy_step.values.astype(str),
356
395
  name="esa_step_label",
357
396
  dims=["esa_step_label"],
358
397
  attrs=attr_mgr.get_variable_attributes("esa_step_label"),
359
398
  )
360
- pointing_bins_label = xr.DataArray(
361
- pointing_bins.values.astype(str),
362
- name="pointing_bins_label",
363
- dims=["pointing_bins_label"],
364
- attrs=attr_mgr.get_variable_attributes("pointing_bins_label"),
399
+
400
+ spin_angle = xr.DataArray(
401
+ data=SPIN_ANGLE_BIN_CENTERS,
402
+ name="spin_angle",
403
+ dims=["spin_angle"],
404
+ attrs=attr_mgr.get_variable_attributes("spin_angle"),
405
+ )
406
+ spin_angle_label = xr.DataArray(
407
+ spin_angle.values.astype(str),
408
+ name="spin_angle_label",
409
+ dims=["spin_angle_label"],
410
+ attrs=attr_mgr.get_variable_attributes("spin_angle_label"),
411
+ )
412
+
413
+ off_angle = xr.DataArray(
414
+ data=OFF_ANGLE_BIN_CENTERS,
415
+ name="off_angle",
416
+ dims=["off_angle"],
417
+ attrs=attr_mgr.get_variable_attributes("off_angle"),
418
+ )
419
+ off_angle_label = xr.DataArray(
420
+ off_angle.values.astype(str),
421
+ name="off_angle_label",
422
+ dims=["off_angle_label"],
423
+ attrs=attr_mgr.get_variable_attributes("off_angle_label"),
365
424
  )
425
+
366
426
  dataset = xr.Dataset(
367
427
  coords={
368
428
  "epoch": epoch_time,
369
- "pointing_bins": pointing_bins,
370
- "pointing_bins_label": pointing_bins_label,
371
- "esa_step": esa_step,
372
- "esa_step_label": esa_step_label,
429
+ "esa_energy_step": esa_energy_step,
430
+ "esa_energy_step_label": esa_energy_step_label,
431
+ "spin_angle": spin_angle,
432
+ "spin_angle_label": spin_angle_label,
433
+ "off_angle": off_angle,
434
+ "off_angle_label": off_angle_label,
373
435
  },
374
436
  attrs=attr_mgr.get_global_attributes(logical_source),
375
437
  )
@@ -389,30 +451,34 @@ def create_datasets(
389
451
 
390
452
  # Create a data array for the current field and add it to the dataset
391
453
  # TODO: TEMPORARY. need to update to use l1b data once that's available.
392
- if field in ["pointing_start", "pointing_end", "mode", "pivot_angle"]:
454
+ if field in [
455
+ "pointing_start_met",
456
+ "pointing_end_met",
457
+ "esa_mode",
458
+ "pivot_angle",
459
+ ]:
393
460
  dataset[field] = xr.DataArray(
394
461
  data=[1],
395
462
  dims=dims,
396
463
  attrs=attr_mgr.get_variable_attributes(field),
397
464
  )
398
465
  # TODO: This is temporary.
399
- # The data type will be set in the data class when that's created
400
466
  elif field == "exposure_time":
401
467
  dataset[field] = xr.DataArray(
402
- data=np.ones((1, 7), dtype=np.float16),
468
+ data=np.ones((1, 7, 3600, 40), dtype=np.float16),
403
469
  dims=dims,
404
470
  attrs=attr_mgr.get_variable_attributes(field),
405
471
  )
406
472
 
407
- elif "rate" in field:
473
+ elif "rates" in field:
408
474
  dataset[field] = xr.DataArray(
409
- data=np.ones((1, 3600, 7), dtype=np.float16),
475
+ data=np.ones(PSET_SHAPE, dtype=np.float16),
410
476
  dims=dims,
411
477
  attrs=attr_mgr.get_variable_attributes(field),
412
478
  )
413
479
  else:
414
480
  dataset[field] = xr.DataArray(
415
- data=np.ones((1, 3600, 7), dtype=np.int16),
481
+ data=np.ones(PSET_SHAPE, dtype=np.int16),
416
482
  dims=dims,
417
483
  attrs=attr_mgr.get_variable_attributes(field),
418
484
  )
@@ -5,12 +5,13 @@ import xarray as xr
5
5
 
6
6
  from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
7
7
  from imap_processing.ena_maps import ena_maps
8
- from imap_processing.ena_maps.ena_maps import RectangularSkyMap
9
- from imap_processing.spice import geometry
10
- from imap_processing.spice.geometry import SpiceFrame
8
+ from imap_processing.ena_maps.ena_maps import AbstractSkyMap, RectangularSkyMap
9
+ from imap_processing.ena_maps.utils.naming import MapDescriptor
11
10
 
12
11
 
13
- def lo_l2(sci_dependencies: dict, anc_dependencies: list) -> list[xr.Dataset]:
12
+ def lo_l2(
13
+ sci_dependencies: dict, anc_dependencies: list, descriptor: str
14
+ ) -> list[xr.Dataset]:
14
15
  """
15
16
  Will process IMAP-Lo L1C data into Le CDF data products.
16
17
 
@@ -20,6 +21,8 @@ def lo_l2(sci_dependencies: dict, anc_dependencies: list) -> list[xr.Dataset]:
20
21
  Dictionary of datasets needed for L2 data product creation in xarray Datasets.
21
22
  anc_dependencies : list
22
23
  Ancillary files needed for L2 data product creation.
24
+ descriptor : str
25
+ The map descriptor to be produced.
23
26
 
24
27
  Returns
25
28
  -------
@@ -37,18 +40,19 @@ def lo_l2(sci_dependencies: dict, anc_dependencies: list) -> list[xr.Dataset]:
37
40
  logical_source = "imap_lo_l2_l090-ena-h-sf-nsp-ram-hae-6deg-3mo"
38
41
  psets = sci_dependencies["imap_lo_l1c_pset"]
39
42
 
40
- # Create the rectangular sky map from the pointing set.
41
- lo_rect_map = project_pset_to_rect_map(
42
- psets, spacing_deg=6, spice_frame=geometry.SpiceFrame.ECLIPJ2000
43
- )
43
+ # Create an AbstractSkyMap (Rectangular or HEALPIX) from the pointing set
44
+ lo_sky_map = project_pset_to_sky_map(psets, descriptor)
45
+ if not isinstance(lo_sky_map, RectangularSkyMap):
46
+ raise NotImplementedError("HEALPix map output not supported for Lo")
47
+
44
48
  # Add the hydrogen rates to the rectangular map dataset.
45
- lo_rect_map.data_1d["h_rate"] = calculate_rates(
46
- lo_rect_map.data_1d["h_counts"], lo_rect_map.data_1d["exposure_time"]
49
+ lo_sky_map.data_1d["h_rate"] = calculate_rates(
50
+ lo_sky_map.data_1d["h_counts"], lo_sky_map.data_1d["exposure_time"]
47
51
  )
48
52
  # Add the hydrogen flux to the rectangular map dataset.
49
- lo_rect_map.data_1d["h_flux"] = calculate_fluxes(lo_rect_map.data_1d["h_rate"])
53
+ lo_sky_map.data_1d["h_flux"] = calculate_fluxes(lo_sky_map.data_1d["h_rate"])
50
54
  # Create the dataset from the rectangular map.
51
- lo_rect_map_ds = lo_rect_map.to_dataset()
55
+ lo_rect_map_ds = lo_sky_map.to_dataset()
52
56
  # Add the attributes to the dataset.
53
57
  lo_rect_map_ds = add_attributes(
54
58
  lo_rect_map_ds, attr_mgr, logical_source=logical_source
@@ -57,41 +61,37 @@ def lo_l2(sci_dependencies: dict, anc_dependencies: list) -> list[xr.Dataset]:
57
61
  return [lo_rect_map_ds]
58
62
 
59
63
 
60
- def project_pset_to_rect_map(
61
- psets: list[xr.Dataset], spacing_deg: int, spice_frame: SpiceFrame
62
- ) -> RectangularSkyMap:
64
+ def project_pset_to_sky_map(psets: list[xr.Dataset], descriptor: str) -> AbstractSkyMap:
63
65
  """
64
- Project the pointing set to a rectangular sky map.
66
+ Project the pointing set to a sky map.
65
67
 
66
- This function is used to create a rectangular sky map from the pointing set
68
+ This function is used to create a sky map from the pointing set
67
69
  data in the L1C dataset.
68
70
 
69
71
  Parameters
70
72
  ----------
71
73
  psets : list[xr.Dataset]
72
74
  List of pointing sets in xarray Dataset format.
73
- spacing_deg : int
74
- The spacing in degrees for the rectangular sky map.
75
- spice_frame : SpiceFrame
76
- The SPICE frame to use for the rectangular sky map projection.
75
+ descriptor : str
76
+ The map descriptor for the map to be produced,
77
+ contains details about the map projection.
77
78
 
78
79
  Returns
79
80
  -------
80
- RectangularSkyMap
81
- The rectangular sky map created from the pointing set data.
81
+ AbstractSkyMap
82
+ The sky map created from the pointing set data.
82
83
  """
83
- lo_rect_map = ena_maps.RectangularSkyMap(
84
- spacing_deg=spacing_deg,
85
- spice_frame=spice_frame,
86
- )
84
+ map_descriptor = MapDescriptor.from_string(descriptor)
85
+ output_map = map_descriptor.to_empty_map()
86
+
87
87
  for pset in psets:
88
88
  lo_pset = ena_maps.LoPointingSet(pset)
89
- lo_rect_map.project_pset_values_to_map(
89
+ output_map.project_pset_values_to_map(
90
90
  pointing_set=lo_pset,
91
91
  value_keys=["h_counts", "exposure_time"],
92
92
  index_match_method=ena_maps.IndexMatchMethod.PUSH,
93
93
  )
94
- return lo_rect_map
94
+ return output_map
95
95
 
96
96
 
97
97
  def calculate_rates(counts: xr.DataArray, exposure_time: xr.DataArray) -> xr.DataArray:
@@ -0,0 +1,55 @@
1
+ """Ancillary file reading for IMAP-Lo processing."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pandas as pd
6
+
7
+ # convert the YYYYDDD datetime format directly upon reading
8
+ _CONVERTERS = {
9
+ "YYYYDDD": lambda x: pd.to_datetime(str(x), format="%Y%j"),
10
+ "#YYYYDDD": lambda x: pd.to_datetime(str(x), format="%Y%j"),
11
+ "YYYYDDD_strt": lambda x: pd.to_datetime(str(x), format="%Y%j"),
12
+ "YYYYDDD_end": lambda x: pd.to_datetime(str(x), format="%Y%j"),
13
+ }
14
+
15
+ # Columns in the csv files to rename for consistency
16
+ _RENAME_COLUMNS = {
17
+ "YYYYDDD": "Date",
18
+ "#YYYYDDD": "Date",
19
+ "#Comments": "Comments",
20
+ "YYYYDDD_strt": "StartDate",
21
+ "YYYYDDD_end": "EndDate",
22
+ }
23
+
24
+
25
+ def read_ancillary_file(ancillary_file: str | Path) -> pd.DataFrame:
26
+ """
27
+ Read a generic ancillary CSV file into a pandas DataFrame.
28
+
29
+ Parameters
30
+ ----------
31
+ ancillary_file : str or Path
32
+ Path to the ancillary CSV file.
33
+
34
+ Returns
35
+ -------
36
+ pd.DataFrame
37
+ DataFrame containing the ancillary data.
38
+ """
39
+ skiprows = None
40
+ if "esa-mode-lut" in str(ancillary_file):
41
+ # skip the first row which is a comment
42
+ skiprows = [0]
43
+ elif "geometric-factor" in str(ancillary_file):
44
+ # skip the rows with comment headers indicating Hi_Res and Hi_Thr
45
+ skiprows = [1, 38]
46
+ df = pd.read_csv(ancillary_file, converters=_CONVERTERS, skiprows=skiprows)
47
+ df = df.rename(columns=_RENAME_COLUMNS)
48
+
49
+ if "geometric-factor" in str(ancillary_file):
50
+ # Add an ESA mode column based on the known structure of the file.
51
+ # The first 36 rows are ESA mode 0 (HiRes), the second 36 are ESA mode 1 (HiThr)
52
+ df["esa_mode"] = 0
53
+ df.loc[36:, "esa_mode"] = 1
54
+
55
+ return df