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
@@ -4,43 +4,12 @@ import numpy as np
4
4
  import numpy.typing as npt
5
5
  import pandas as pd
6
6
  import xarray as xr
7
+ from numpy.typing import NDArray
7
8
 
8
- from imap_processing import imap_module_directory
9
-
10
- BASE_PATH = imap_module_directory / "ultra" / "lookup_tables"
11
-
12
- _YADJUST_DF = pd.read_csv(BASE_PATH / "yadjust.csv").set_index("dYLUT")
13
- _TDC_NORM_DF_ULTRA45 = pd.read_csv(
14
- BASE_PATH / "ultra45_tdc_norm.csv", header=1, index_col="Index"
15
- )
16
- _TDC_NORM_DF_ULTRA90 = pd.read_csv(
17
- BASE_PATH / "ultra90_tdc_norm.csv", header=1, index_col="Index"
18
- )
19
- _BACK_POS_DF_ULTRA45 = pd.read_csv(
20
- BASE_PATH / "ultra45_back-pos-luts.csv", index_col="Index_offset"
21
- )
22
- _BACK_POS_DF_ULTRA90 = pd.read_csv(
23
- BASE_PATH / "ultra90_back-pos-luts.csv", index_col="Index_offset"
24
- )
25
- _ENERGY_NORM_DF = pd.read_csv(BASE_PATH / "EgyNorm.mem.csv")
26
- _IMAGE_PARAMS_DF = {
27
- "ultra45": pd.read_csv(BASE_PATH / "FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv"),
28
- "ultra90": pd.read_csv(BASE_PATH / "FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv"),
29
- }
30
-
31
- _FWHM_TABLES = {
32
- ("left", "ultra45"): pd.read_csv(BASE_PATH / "Angular_Profiles_FM45_LeftSlit.csv"),
33
- ("right", "ultra45"): pd.read_csv(
34
- BASE_PATH / "Angular_Profiles_FM45_RightSlit.csv"
35
- ),
36
- ("left", "ultra90"): pd.read_csv(BASE_PATH / "Angular_Profiles_FM90_LeftSlit.csv"),
37
- ("right", "ultra90"): pd.read_csv(
38
- BASE_PATH / "Angular_Profiles_FM90_RightSlit.csv"
39
- ),
40
- }
41
-
42
-
43
- def get_y_adjust(dy_lut: np.ndarray) -> npt.NDArray:
9
+ from imap_processing.quality_flags import ImapDEOutliersUltraFlags
10
+
11
+
12
+ def get_y_adjust(dy_lut: np.ndarray, ancillary_files: dict) -> npt.NDArray:
44
13
  """
45
14
  Adjust the front yf position based on the particle's trajectory.
46
15
 
@@ -52,16 +21,21 @@ def get_y_adjust(dy_lut: np.ndarray) -> npt.NDArray:
52
21
  ----------
53
22
  dy_lut : np.ndarray
54
23
  Change in y direction used for the lookup table (mm).
24
+ ancillary_files : dict[Path]
25
+ Ancillary files containing the lookup tables.
55
26
 
56
27
  Returns
57
28
  -------
58
29
  yadj : np.ndarray
59
30
  Y adjustment (mm).
60
31
  """
61
- return _YADJUST_DF["dYAdj"].iloc[dy_lut].values
32
+ yadjust_df = pd.read_csv(ancillary_files["l1b-yadjust-lookup"]).set_index("dYLUT")
33
+ return yadjust_df["dYAdj"].iloc[dy_lut].values
62
34
 
63
35
 
64
- def get_norm(dn: xr.DataArray, key: str, file_label: str) -> npt.NDArray:
36
+ def get_norm(
37
+ dn: xr.DataArray, key: str, file_label: str, ancillary_files: dict
38
+ ) -> npt.NDArray:
65
39
  """
66
40
  Correct mismatches between the stop Time to Digital Converters (TDCs).
67
41
 
@@ -82,6 +56,8 @@ def get_norm(dn: xr.DataArray, key: str, file_label: str) -> npt.NDArray:
82
56
  BtSpNNorm, BtSpSNorm, BtSpENorm, or BtSpWNorm.
83
57
  file_label : str
84
58
  Instrument (ultra45 or ultra90).
59
+ ancillary_files : dict[Path]
60
+ Ancillary files containing the lookup tables.
85
61
 
86
62
  Returns
87
63
  -------
@@ -89,16 +65,22 @@ def get_norm(dn: xr.DataArray, key: str, file_label: str) -> npt.NDArray:
89
65
  Normalized DNs.
90
66
  """
91
67
  if file_label == "ultra45":
92
- tdc_norm_df = _TDC_NORM_DF_ULTRA45
68
+ tdc_norm_df = pd.read_csv(
69
+ ancillary_files["l1b-45sensor-tdc-norm-lookup"], header=1, index_col="Index"
70
+ )
93
71
  else:
94
- tdc_norm_df = _TDC_NORM_DF_ULTRA90
72
+ tdc_norm_df = pd.read_csv(
73
+ ancillary_files["l1b-90sensor-tdc-norm-lookup"], header=1, index_col="Index"
74
+ )
95
75
 
96
76
  dn_norm = tdc_norm_df[key].iloc[dn].values
97
77
 
98
78
  return dn_norm
99
79
 
100
80
 
101
- def get_back_position(back_index: np.ndarray, key: str, file_label: str) -> npt.NDArray:
81
+ def get_back_position(
82
+ back_index: np.ndarray, key: str, file_label: str, ancillary_files: dict
83
+ ) -> npt.NDArray:
102
84
  """
103
85
  Convert normalized TDC values using lookup tables.
104
86
 
@@ -117,6 +99,8 @@ def get_back_position(back_index: np.ndarray, key: str, file_label: str) -> npt.
117
99
  XBkTp, YBkTp, XBkBt, or YBkBt.
118
100
  file_label : str
119
101
  Instrument (ultra45 or ultra90).
102
+ ancillary_files : dict[Path]
103
+ Ancillary files containing the lookup tables.
120
104
 
121
105
  Returns
122
106
  -------
@@ -124,14 +108,20 @@ def get_back_position(back_index: np.ndarray, key: str, file_label: str) -> npt.
124
108
  Converted DNs to Units of hundredths of a millimeter.
125
109
  """
126
110
  if file_label == "ultra45":
127
- back_pos_df = _BACK_POS_DF_ULTRA45
111
+ back_pos_df = pd.read_csv(
112
+ ancillary_files["l1b-45sensor-back-pos-lookup"], index_col="Index_offset"
113
+ )
128
114
  else:
129
- back_pos_df = _BACK_POS_DF_ULTRA90
115
+ back_pos_df = pd.read_csv(
116
+ ancillary_files["l1b-90sensor-back-pos-lookup"], index_col="Index_offset"
117
+ )
130
118
 
131
119
  return back_pos_df[key].values[back_index]
132
120
 
133
121
 
134
- def get_energy_norm(ssd: np.ndarray, composite_energy: np.ndarray) -> npt.NDArray:
122
+ def get_energy_norm(
123
+ ssd: np.ndarray, composite_energy: np.ndarray, ancillary_files: dict
124
+ ) -> npt.NDArray:
135
125
  """
136
126
  Normalize composite energy per SSD using a lookup table.
137
127
 
@@ -146,6 +136,8 @@ def get_energy_norm(ssd: np.ndarray, composite_energy: np.ndarray) -> npt.NDArra
146
136
  Acts as index 1.
147
137
  composite_energy : np.ndarray
148
138
  Acts as index 2.
139
+ ancillary_files : dict[Path]
140
+ Ancillary files containing the lookup tables.
149
141
 
150
142
  Returns
151
143
  -------
@@ -153,11 +145,11 @@ def get_energy_norm(ssd: np.ndarray, composite_energy: np.ndarray) -> npt.NDArra
153
145
  Normalized composite energy.
154
146
  """
155
147
  row_number = ssd * 4096 + composite_energy
156
-
157
- return _ENERGY_NORM_DF["NormEnergy"].iloc[row_number]
148
+ norm_lookup = pd.read_csv(ancillary_files["l1b-egynorm-lookup"])
149
+ return norm_lookup["NormEnergy"].iloc[row_number]
158
150
 
159
151
 
160
- def get_image_params(image: str, sensor: str) -> np.float64:
152
+ def get_image_params(image: str, sensor: str, ancillary_files: dict) -> np.float64:
161
153
  """
162
154
  Lookup table for image parameters.
163
155
 
@@ -171,18 +163,26 @@ def get_image_params(image: str, sensor: str) -> np.float64:
171
163
  The column name to lookup in the CSV file, e.g., 'XFTLTOFF' or 'XFTRTOFF'.
172
164
  sensor : str
173
165
  Sensor name: "ultra45" or "ultra90".
166
+ ancillary_files : dict[Path]
167
+ Ancillary files containing the lookup tables.
174
168
 
175
169
  Returns
176
170
  -------
177
171
  value : np.float64
178
172
  Image parameter value from the CSV file.
179
173
  """
180
- lookup_table = _IMAGE_PARAMS_DF[sensor]
174
+ if sensor == "ultra45":
175
+ lookup_table = pd.read_csv(ancillary_files["l1b-45sensor-imgparams-lookup"])
176
+ else:
177
+ lookup_table = pd.read_csv(ancillary_files["l1b-90sensor-imgparams-lookup"])
178
+
181
179
  value: np.float64 = lookup_table[image].values[0]
182
180
  return value
183
181
 
184
182
 
185
- def get_angular_profiles(start_type: str, sensor: str) -> pd.DataFrame:
183
+ def get_angular_profiles(
184
+ start_type: str, sensor: str, ancillary_files: dict
185
+ ) -> pd.DataFrame:
186
186
  """
187
187
  Lookup table for FWHM for theta and phi.
188
188
 
@@ -195,13 +195,16 @@ def get_angular_profiles(start_type: str, sensor: str) -> pd.DataFrame:
195
195
  Start Type: Left, Right.
196
196
  sensor : str
197
197
  Sensor name: "ultra45" or "ultra90".
198
+ ancillary_files : dict[Path]
199
+ Ancillary files.
198
200
 
199
201
  Returns
200
202
  -------
201
203
  lookup_table : DataFrame
202
204
  Angular profile lookup table for a given start_type and sensor.
203
205
  """
204
- lookup_table = _FWHM_TABLES[(start_type.lower(), sensor)]
206
+ lut_descriptor = f"l1b-{sensor[-2:]}sensor-{start_type.lower()}slit-lookup"
207
+ lookup_table = pd.read_csv(ancillary_files[lut_descriptor])
205
208
 
206
209
  return lookup_table
207
210
 
@@ -227,3 +230,374 @@ def get_energy_efficiencies(ancillary_files: dict) -> pd.DataFrame:
227
230
  lookup_table = pd.read_csv(ancillary_files["l1b-45sensor-logistic-interpolation"])
228
231
 
229
232
  return lookup_table
233
+
234
+
235
+ def load_geometric_factor_tables(
236
+ ancillary_files: dict,
237
+ filename: str,
238
+ ) -> dict:
239
+ """
240
+ Lookup tables for geometric factor.
241
+
242
+ Parameters
243
+ ----------
244
+ ancillary_files : dict[Path]
245
+ Ancillary files.
246
+ filename : str
247
+ Name of the file in ancillary_files to use.
248
+
249
+ Returns
250
+ -------
251
+ geometric_factor_tables : dict
252
+ Geometric factor lookup tables.
253
+ """
254
+ gf_table = pd.read_csv(
255
+ ancillary_files[filename], header=None, skiprows=6, nrows=301
256
+ ).to_numpy(dtype=float)
257
+ theta_table = pd.read_csv(
258
+ ancillary_files[filename], header=None, skiprows=308, nrows=301
259
+ ).to_numpy(dtype=float)
260
+ phi_table = pd.read_csv(
261
+ ancillary_files[filename], header=None, skiprows=610, nrows=301
262
+ ).to_numpy(dtype=float)
263
+
264
+ return {
265
+ "gf_table": gf_table,
266
+ "theta_table": theta_table,
267
+ "phi_table": phi_table,
268
+ }
269
+
270
+
271
+ def get_geometric_factor(
272
+ phi: NDArray,
273
+ theta: NDArray,
274
+ quality_flag: NDArray,
275
+ ancillary_files: dict | None = None,
276
+ filename: str | None = None,
277
+ geometric_factor_tables: dict | None = None,
278
+ ) -> tuple[NDArray, NDArray]:
279
+ """
280
+ Lookup table for geometric factor using nearest neighbor.
281
+
282
+ Parameters
283
+ ----------
284
+ phi : NDArray
285
+ Azimuth angles in degrees.
286
+ theta : NDArray
287
+ Elevation angles in degrees.
288
+ quality_flag : NDArray
289
+ Quality flag to set when geometric factor is zero.
290
+ ancillary_files : dict[Path], optional
291
+ Ancillary files.
292
+ filename : str, optional
293
+ Name of the file in ancillary_files to use.
294
+ geometric_factor_tables : dict, optional
295
+ Preloaded geometric factor lookup tables. If not provided, will load.
296
+
297
+ Returns
298
+ -------
299
+ geometric_factor : NDArray
300
+ Geometric factor.
301
+ """
302
+ if geometric_factor_tables is None:
303
+ if ancillary_files is None or filename is None:
304
+ raise ValueError(
305
+ "ancillary_files and filename must be provided if "
306
+ "geometric_factor_tables is not supplied."
307
+ )
308
+ geometric_factor_tables = load_geometric_factor_tables(
309
+ ancillary_files, filename
310
+ )
311
+ # Assume uniform grids: extract 1D arrays from first row/col
312
+ theta_vals = geometric_factor_tables["theta_table"][0, :] # columns represent theta
313
+ phi_vals = geometric_factor_tables["phi_table"][:, 0] # rows represent phi
314
+
315
+ # Find nearest index in table for each input value
316
+ phi_idx = np.abs(phi_vals[:, None] - phi).argmin(axis=0)
317
+ theta_idx = np.abs(theta_vals[:, None] - theta).argmin(axis=0)
318
+
319
+ # Fetch geometric factor values at nearest (phi, theta) pairs
320
+ geometric_factor = geometric_factor_tables["gf_table"][phi_idx, theta_idx]
321
+
322
+ outside_fov = ~is_inside_fov(np.deg2rad(phi), np.deg2rad(theta))
323
+ quality_flag[outside_fov] |= ImapDEOutliersUltraFlags.FOV.value
324
+
325
+ return geometric_factor
326
+
327
+
328
+ def load_scattering_lookup_tables(ancillary_files: dict, instrument_id: int) -> dict:
329
+ """
330
+ Load scattering coefficient lookup tables for the specified instrument.
331
+
332
+ Parameters
333
+ ----------
334
+ ancillary_files : dict
335
+ Ancillary files.
336
+ instrument_id : int
337
+ Instrument ID, either 45 or 90.
338
+
339
+ Returns
340
+ -------
341
+ dict
342
+ Dictionary containing arrays for theta_grid, phi_grid, a_theta, g_theta,
343
+ a_phi, g_phi.
344
+ """
345
+ # TODO remove the line below when the 45 sensor scattering coefficients are
346
+ # delivered.
347
+ instrument_id = 90
348
+ descriptor = f"l1b-{instrument_id}sensor-scattering-calibration"
349
+ theta_grid = pd.read_csv(
350
+ ancillary_files[descriptor], header=None, skiprows=7, nrows=241
351
+ ).to_numpy(dtype=float)
352
+ phi_grid = pd.read_csv(
353
+ ancillary_files[descriptor], header=None, skiprows=249, nrows=241
354
+ ).to_numpy(dtype=float)
355
+ a_theta = pd.read_csv(
356
+ ancillary_files[descriptor], header=None, skiprows=491, nrows=241
357
+ ).to_numpy(dtype=float)
358
+ g_theta = pd.read_csv(
359
+ ancillary_files[descriptor], header=None, skiprows=733, nrows=241
360
+ ).to_numpy(dtype=float)
361
+ a_phi = pd.read_csv(
362
+ ancillary_files[descriptor], header=None, skiprows=975, nrows=241
363
+ ).to_numpy(dtype=float)
364
+ g_phi = pd.read_csv(
365
+ ancillary_files[descriptor], header=None, skiprows=1217, nrows=241
366
+ ).to_numpy(dtype=float)
367
+ return {
368
+ "theta_grid": theta_grid,
369
+ "phi_grid": phi_grid,
370
+ "a_theta": a_theta,
371
+ "g_theta": g_theta,
372
+ "a_phi": a_phi,
373
+ "g_phi": g_phi,
374
+ }
375
+
376
+
377
+ def get_scattering_coefficients(
378
+ theta: NDArray,
379
+ phi: NDArray,
380
+ lookup_tables: dict | None = None,
381
+ ancillary_files: dict | None = None,
382
+ instrument_id: int | None = None,
383
+ ) -> tuple[NDArray, NDArray]:
384
+ """
385
+ Get a and g coefficients for theta and phi to compute scattering FWHM.
386
+
387
+ Parameters
388
+ ----------
389
+ theta : NDArray
390
+ Elevation angles in degrees.
391
+ phi : NDArray
392
+ Azimuth angles in degrees.
393
+ lookup_tables : dict, optional
394
+ Preloaded lookup tables. If not provided, will load using ancillary_files and
395
+ instrument_id.
396
+ ancillary_files : dict, optional
397
+ Ancillary files, required if lookup_tables is not provided.
398
+ instrument_id : int, optional
399
+ Instrument ID, required if lookup_tables is not provided.
400
+
401
+ Returns
402
+ -------
403
+ tuple
404
+ Scattering a and g values corresponding to the given theta and phi values.
405
+ """
406
+ if lookup_tables is None:
407
+ if ancillary_files is None or instrument_id is None:
408
+ raise ValueError(
409
+ "ancillary_files and instrument_id must be provided if lookup_tables "
410
+ "is not supplied."
411
+ )
412
+ lookup_tables = load_scattering_lookup_tables(ancillary_files, instrument_id)
413
+
414
+ theta_grid = lookup_tables["theta_grid"]
415
+ phi_grid = lookup_tables["phi_grid"]
416
+ a_theta = lookup_tables["a_theta"]
417
+ g_theta = lookup_tables["g_theta"]
418
+ a_phi = lookup_tables["a_phi"]
419
+ g_phi = lookup_tables["g_phi"]
420
+
421
+ theta_vals = theta_grid[0, :] # columns represent theta
422
+ phi_vals = phi_grid[:, 0] # rows represent phi
423
+
424
+ phi_idx = np.abs(phi_vals[:, None] - phi).argmin(axis=0)
425
+ theta_idx = np.abs(theta_vals[:, None] - theta).argmin(axis=0)
426
+
427
+ a_theta_val = a_theta[phi_idx, theta_idx]
428
+ g_theta_val = g_theta[phi_idx, theta_idx]
429
+ a_phi_val = a_phi[phi_idx, theta_idx]
430
+ g_phi_val = g_phi[phi_idx, theta_idx]
431
+
432
+ return np.column_stack([a_theta_val, g_theta_val]), np.column_stack(
433
+ [a_phi_val, g_phi_val]
434
+ )
435
+
436
+
437
+ def is_inside_fov(phi: np.ndarray, theta: np.ndarray) -> np.ndarray:
438
+ """
439
+ Determine angles in the field of view (FOV).
440
+
441
+ This function is used in the deadtime correction to determine whether a given
442
+ (theta, phi) angle is within the instrument's Field of View (FOV).
443
+ Only pixels inside the FOV are considered for time accumulation. The FOV boundary
444
+ is defined by equation 19 in the Ultra Algorithm Document.
445
+
446
+ Parameters
447
+ ----------
448
+ phi : np.ndarray
449
+ Azimuth angles in radians.
450
+ theta : np.ndarray
451
+ Elevation angles in radians.
452
+
453
+ Returns
454
+ -------
455
+ numpy.ndarray
456
+ Boolean array indicating if the angle is in the FOV, False otherwise.
457
+ """
458
+ numerator = 5.0 * np.cos(phi)
459
+ denominator = 1 + 2.80 * np.cos(phi)
460
+ # Equation 19 in the Ultra Algorithm Document.
461
+ theta_nom = np.arctan(numerator / denominator)
462
+ return np.abs(theta) <= theta_nom
463
+
464
+
465
+ def get_ph_corrected(
466
+ sensor: str,
467
+ location: str,
468
+ ancillary_files: dict,
469
+ xlut: NDArray,
470
+ ylut: NDArray,
471
+ quality_flag: NDArray,
472
+ ) -> tuple[NDArray, NDArray]:
473
+ """
474
+ PH correction for stop anodes, top and bottom.
475
+
476
+ Further description is available starting on
477
+ page 207 of the Ultra Flight Software Document.
478
+
479
+ Parameters
480
+ ----------
481
+ sensor : str
482
+ Sensor name: "ultra45" or "ultra90".
483
+ location : str
484
+ Location: "tp" or "bt".
485
+ ancillary_files : dict[Path]
486
+ Ancillary files.
487
+ xlut : NDArray
488
+ X lookup index for PH correction.
489
+ ylut : NDArray
490
+ Y lookup index for PH correction.
491
+ quality_flag : NDArray
492
+ Quality flag to set when there is an outlier.
493
+
494
+ Returns
495
+ -------
496
+ ph_correction : NDArray
497
+ Correction for pulse height.
498
+ quality_flag : NDArray
499
+ Quality flag updated with PH correction flags.
500
+ """
501
+ ph_correct = pd.read_csv(
502
+ ancillary_files[f"l1b-{sensor[-2:]}sensor-sp{location}phcorr"], header=None
503
+ )
504
+ ph_correct_array = ph_correct.to_numpy()
505
+
506
+ max_x, max_y = ph_correct_array.shape[0] - 1, ph_correct_array.shape[1] - 1
507
+
508
+ # Clamp indices to nearest valid value
509
+ xlut_clamped = np.clip(xlut.astype(int), 0, max_x)
510
+ ylut_clamped = np.clip(ylut.astype(int), 0, max_y)
511
+
512
+ # Flag where clamping occurred
513
+ flagged_mask = (xlut != xlut_clamped) | (ylut != ylut_clamped)
514
+ quality_flag[flagged_mask] |= ImapDEOutliersUltraFlags.PHCORR.value
515
+
516
+ ph_correction = ph_correct_array[xlut_clamped, ylut_clamped]
517
+
518
+ return ph_correction, quality_flag
519
+
520
+
521
+ def get_ebins(
522
+ lut: str,
523
+ energy: NDArray,
524
+ ctof: NDArray,
525
+ ebins: NDArray,
526
+ ancillary_files: dict,
527
+ ) -> NDArray:
528
+ """
529
+ Get energy bins from the lookup table.
530
+
531
+ Parameters
532
+ ----------
533
+ lut : str
534
+ Lookup table name, e.g., "l1b-tofxpht".
535
+ energy : NDArray
536
+ Energy from the event (keV).
537
+ ctof : NDArray
538
+ Corrected TOF (tenths of a ns).
539
+ ebins : NDArray
540
+ Energy bins to fill with values.
541
+ ancillary_files : dict[Path]
542
+ Ancillary files.
543
+
544
+ Returns
545
+ -------
546
+ ebins : NDArray
547
+ Energy bins from the lookup table.
548
+ """
549
+ with open(ancillary_files[lut]) as f:
550
+ all_lines = f.readlines()
551
+ pixel_text = "".join(all_lines[4:])
552
+
553
+ lut_array = np.fromstring(pixel_text, sep=" ", dtype=int).reshape((2048, 4096))
554
+ # Note that the LUT is indexed [energy, ctof] for l1b-tofxph
555
+ # and [ctof, energy] for everything else.
556
+ if lut == "l1b-tofxph":
557
+ energy_lookup = (2048 - np.floor(energy)).astype(int)
558
+ ctof_lookup = np.floor(ctof).astype(int)
559
+ valid = (
560
+ (energy_lookup >= 0)
561
+ & (energy_lookup < 2048)
562
+ & (ctof_lookup >= 0)
563
+ & (ctof_lookup < 4096)
564
+ )
565
+ ebins[valid] = lut_array[energy_lookup[valid], ctof_lookup[valid]]
566
+ else:
567
+ energy_lookup = np.floor(energy).astype(int)
568
+ ctof_lookup = (2048 - np.floor(ctof)).astype(int)
569
+ valid = (
570
+ (energy_lookup >= 0)
571
+ & (energy_lookup < 4096)
572
+ & (ctof_lookup >= 0)
573
+ & (ctof_lookup < 2048)
574
+ )
575
+ ebins[valid] = lut_array[ctof_lookup[valid], energy_lookup[valid]]
576
+
577
+ return ebins
578
+
579
+
580
+ def get_scattering_thresholds(ancillary_files: dict) -> dict:
581
+ """
582
+ Load scattering culling thresholds as a function of energy from a lookup table.
583
+
584
+ Parameters
585
+ ----------
586
+ ancillary_files : dict[Path]
587
+ Ancillary files.
588
+
589
+ Returns
590
+ -------
591
+ threshold_dict
592
+ Dictionary containing energy ranges and the corresponding scattering culling
593
+ threshold.
594
+ """
595
+ # Culling FWHM Scattering values as a function of energy.
596
+ thresholds = pd.read_csv(
597
+ ancillary_files["l1b-scattering-thresholds-per-energy"], header=None, skiprows=1
598
+ ).to_numpy(dtype=np.float64)
599
+ # The first two columns represent the energy range (min, max) in keV, and the
600
+ # value is the FWHM scattering threshold in degrees
601
+ threshold_dict = {(row[0], row[1]): row[2] for row in thresholds}
602
+
603
+ return threshold_dict
@@ -0,0 +1,23 @@
1
+ """Contains list of QFs to use for filtering."""
2
+
3
+ from imap_processing.quality_flags import (
4
+ FlagNameMixin,
5
+ ImapDEOutliersUltraFlags,
6
+ ImapDEScatteringUltraFlags,
7
+ ImapRatesUltraFlags,
8
+ )
9
+
10
+ SPIN_QUALITY_FLAG_FILTERS: dict[str, list[FlagNameMixin]] = {
11
+ "quality_attitude": [],
12
+ "quality_ena_rates": [
13
+ ImapRatesUltraFlags.FIRSTSPIN,
14
+ ImapRatesUltraFlags.LASTSPIN,
15
+ ],
16
+ }
17
+
18
+ DE_QUALITY_FLAG_FILTERS: dict[str, list[FlagNameMixin]] = {
19
+ "quality_outliers": [ImapDEOutliersUltraFlags.FOV],
20
+ "quality_scattering": [
21
+ ImapDEScatteringUltraFlags.ABOVE_THRESHOLD,
22
+ ],
23
+ }