imap-processing 0.18.0__py3-none-any.whl → 0.19.2__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 (122) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ancillary/ancillary_dataset_combiner.py +161 -1
  3. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -0
  4. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +221 -1057
  5. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +307 -283
  6. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1044 -203
  7. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  8. imap_processing/cdf/config/imap_enamaps_l2-common_variable_attrs.yaml +11 -0
  9. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +15 -1
  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_idex_l2a_variable_attrs.yaml +33 -4
  13. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +8 -91
  14. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +106 -16
  15. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +5 -4
  16. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  17. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  18. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +85 -2
  19. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  20. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +20 -8
  21. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +45 -35
  22. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +110 -7
  23. imap_processing/cli.py +138 -93
  24. imap_processing/codice/codice_l0.py +2 -1
  25. imap_processing/codice/codice_l1a.py +167 -69
  26. imap_processing/codice/codice_l1b.py +42 -32
  27. imap_processing/codice/codice_l2.py +215 -9
  28. imap_processing/codice/constants.py +790 -603
  29. imap_processing/codice/data/lo_stepping_values.csv +1 -1
  30. imap_processing/decom.py +1 -4
  31. imap_processing/ena_maps/ena_maps.py +71 -43
  32. imap_processing/ena_maps/utils/corrections.py +291 -0
  33. imap_processing/ena_maps/utils/map_utils.py +20 -4
  34. imap_processing/ena_maps/utils/naming.py +8 -2
  35. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  36. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  37. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  38. imap_processing/glows/ancillary/imap_glows_pipeline-settings_20250923_v002.json +54 -0
  39. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  40. imap_processing/glows/l1b/glows_l1b.py +123 -18
  41. imap_processing/glows/l1b/glows_l1b_data.py +358 -47
  42. imap_processing/glows/l2/glows_l2.py +11 -0
  43. imap_processing/hi/hi_l1a.py +124 -3
  44. imap_processing/hi/hi_l1b.py +154 -71
  45. imap_processing/hi/hi_l1c.py +4 -109
  46. imap_processing/hi/hi_l2.py +104 -60
  47. imap_processing/hi/utils.py +262 -8
  48. imap_processing/hit/l0/constants.py +3 -0
  49. imap_processing/hit/l0/decom_hit.py +3 -6
  50. imap_processing/hit/l1a/hit_l1a.py +311 -21
  51. imap_processing/hit/l1b/hit_l1b.py +54 -126
  52. imap_processing/hit/l2/hit_l2.py +6 -6
  53. imap_processing/ialirt/calculate_ingest.py +219 -0
  54. imap_processing/ialirt/constants.py +12 -2
  55. imap_processing/ialirt/generate_coverage.py +15 -2
  56. imap_processing/ialirt/l0/ialirt_spice.py +6 -2
  57. imap_processing/ialirt/l0/parse_mag.py +293 -42
  58. imap_processing/ialirt/l0/process_hit.py +5 -3
  59. imap_processing/ialirt/l0/process_swapi.py +41 -25
  60. imap_processing/ialirt/process_ephemeris.py +70 -14
  61. imap_processing/ialirt/utils/create_xarray.py +1 -1
  62. imap_processing/idex/idex_l0.py +2 -2
  63. imap_processing/idex/idex_l1a.py +2 -3
  64. imap_processing/idex/idex_l1b.py +2 -3
  65. imap_processing/idex/idex_l2a.py +130 -4
  66. imap_processing/idex/idex_l2b.py +158 -143
  67. imap_processing/idex/idex_utils.py +1 -3
  68. imap_processing/lo/ancillary_data/imap_lo_hydrogen-geometric-factor_v001.csv +75 -0
  69. imap_processing/lo/ancillary_data/imap_lo_oxygen-geometric-factor_v001.csv +75 -0
  70. imap_processing/lo/l0/lo_science.py +25 -24
  71. imap_processing/lo/l1b/lo_l1b.py +93 -19
  72. imap_processing/lo/l1c/lo_l1c.py +273 -93
  73. imap_processing/lo/l2/lo_l2.py +949 -135
  74. imap_processing/lo/lo_ancillary.py +55 -0
  75. imap_processing/mag/l1a/mag_l1a.py +1 -0
  76. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  77. imap_processing/mag/l1b/mag_l1b.py +3 -2
  78. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  79. imap_processing/mag/l1c/mag_l1c.py +23 -6
  80. imap_processing/mag/l1d/mag_l1d.py +57 -14
  81. imap_processing/mag/l1d/mag_l1d_data.py +202 -32
  82. imap_processing/mag/l2/mag_l2.py +2 -0
  83. imap_processing/mag/l2/mag_l2_data.py +14 -5
  84. imap_processing/quality_flags.py +23 -1
  85. imap_processing/spice/geometry.py +89 -39
  86. imap_processing/spice/pointing_frame.py +4 -8
  87. imap_processing/spice/repoint.py +78 -2
  88. imap_processing/spice/spin.py +28 -8
  89. imap_processing/spice/time.py +12 -22
  90. imap_processing/swapi/l1/swapi_l1.py +10 -4
  91. imap_processing/swapi/l2/swapi_l2.py +15 -17
  92. imap_processing/swe/l1b/swe_l1b.py +1 -2
  93. imap_processing/ultra/constants.py +30 -24
  94. imap_processing/ultra/l0/ultra_utils.py +9 -11
  95. imap_processing/ultra/l1a/ultra_l1a.py +1 -2
  96. imap_processing/ultra/l1b/badtimes.py +35 -11
  97. imap_processing/ultra/l1b/de.py +95 -31
  98. imap_processing/ultra/l1b/extendedspin.py +31 -16
  99. imap_processing/ultra/l1b/goodtimes.py +112 -0
  100. imap_processing/ultra/l1b/lookup_utils.py +281 -28
  101. imap_processing/ultra/l1b/quality_flag_filters.py +10 -1
  102. imap_processing/ultra/l1b/ultra_l1b.py +7 -7
  103. imap_processing/ultra/l1b/ultra_l1b_culling.py +169 -7
  104. imap_processing/ultra/l1b/ultra_l1b_extended.py +311 -69
  105. imap_processing/ultra/l1c/helio_pset.py +139 -37
  106. imap_processing/ultra/l1c/l1c_lookup_utils.py +289 -0
  107. imap_processing/ultra/l1c/spacecraft_pset.py +140 -29
  108. imap_processing/ultra/l1c/ultra_l1c.py +33 -24
  109. imap_processing/ultra/l1c/ultra_l1c_culling.py +92 -0
  110. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +400 -292
  111. imap_processing/ultra/l2/ultra_l2.py +54 -11
  112. imap_processing/ultra/utils/ultra_l1_utils.py +37 -7
  113. imap_processing/utils.py +3 -4
  114. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/METADATA +2 -2
  115. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/RECORD +118 -109
  116. imap_processing/idex/idex_l2c.py +0 -84
  117. imap_processing/spice/kernels.py +0 -187
  118. imap_processing/ultra/l1b/cullingmask.py +0 -87
  119. imap_processing/ultra/l1c/histogram.py +0 -36
  120. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/LICENSE +0 -0
  121. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/WHEEL +0 -0
  122. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/entry_points.txt +0 -0
@@ -12,7 +12,7 @@ from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
- TIME_PER_BIN = 0.167 # seconds
15
+ SWAPI_LIVETIME = 0.145 # seconds
16
16
 
17
17
 
18
18
  def solve_full_sweep_energy(
@@ -66,7 +66,7 @@ def solve_full_sweep_energy(
66
66
 
67
67
  first_63_energies = []
68
68
 
69
- for time, sweep_id in zip(data_time, sweep_table):
69
+ for time, sweep_id in zip(data_time, sweep_table, strict=False):
70
70
  # Find the sweep's ESA data for the given time and sweep_id
71
71
  subset = esa_table_df[
72
72
  (esa_table_df["timestamp"] <= time) & (esa_table_df["Sweep #"] == sweep_id)
@@ -159,14 +159,12 @@ def swapi_l2(
159
159
 
160
160
  To process science data to L2, we need to:
161
161
  - convert counts to rates. This is done by dividing the counts by the
162
- TIME_PER_BIN time. TIME_PER_BIN is the exposure time per energy bin which is
163
- obtained by dividing the time for one complete sweep
164
- (12 s, coarse + fine sweep) by the total energy steps (72),
165
- i.e., TIME_PER_BIN = 12/72 = 0.167 s. This will be constant.
162
+ SWAPI_LIVETIME time. LIVETIME is data acquisition time. It will
163
+ be constant, SWAPI_LIVETIME = 0.145 s.
166
164
 
167
165
  - update uncertainty. Calculate new uncertainty value using
168
- SWP_PCEM_ERR data from level one and divide by TIME_PER_BIN. Eg.
169
- SWP_PCEM_UNC = SWP_PCEM_ERR / TIME_PER_BIN
166
+ SWP_PCEM_ERR data from level one and divide by SWAPI_LIVETIME. Eg.
167
+ SWP_PCEM_UNC = SWP_PCEM_ERR / SWAPI_LIVETIME
170
168
  Do the same for SCEM and COIN data.
171
169
 
172
170
  Parameters
@@ -233,9 +231,9 @@ def swapi_l2(
233
231
  ]
234
232
 
235
233
  # convert counts to rate
236
- l2_dataset["swp_pcem_rate"] = l1_dataset["swp_pcem_counts"] / TIME_PER_BIN
237
- l2_dataset["swp_scem_rate"] = l1_dataset["swp_scem_counts"] / TIME_PER_BIN
238
- l2_dataset["swp_coin_rate"] = l1_dataset["swp_coin_counts"] / TIME_PER_BIN
234
+ l2_dataset["swp_pcem_rate"] = l1_dataset["swp_pcem_counts"] / SWAPI_LIVETIME
235
+ l2_dataset["swp_scem_rate"] = l1_dataset["swp_scem_counts"] / SWAPI_LIVETIME
236
+ l2_dataset["swp_coin_rate"] = l1_dataset["swp_coin_counts"] / SWAPI_LIVETIME
239
237
  # update attrs
240
238
  l2_dataset["swp_pcem_rate"].attrs = cdf_manager.get_variable_attributes("pcem_rate")
241
239
  l2_dataset["swp_scem_rate"].attrs = cdf_manager.get_variable_attributes("scem_rate")
@@ -243,22 +241,22 @@ def swapi_l2(
243
241
 
244
242
  # update uncertainty
245
243
  l2_dataset["swp_pcem_rate_stat_uncert_plus"] = (
246
- l1_dataset["swp_pcem_counts_stat_uncert_plus"] / TIME_PER_BIN
244
+ l1_dataset["swp_pcem_counts_stat_uncert_plus"] / SWAPI_LIVETIME
247
245
  )
248
246
  l2_dataset["swp_pcem_rate_stat_uncert_minus"] = (
249
- l1_dataset["swp_pcem_counts_stat_uncert_minus"] / TIME_PER_BIN
247
+ l1_dataset["swp_pcem_counts_stat_uncert_minus"] / SWAPI_LIVETIME
250
248
  )
251
249
  l2_dataset["swp_scem_rate_stat_uncert_plus"] = (
252
- l1_dataset["swp_scem_counts_stat_uncert_plus"] / TIME_PER_BIN
250
+ l1_dataset["swp_scem_counts_stat_uncert_plus"] / SWAPI_LIVETIME
253
251
  )
254
252
  l2_dataset["swp_scem_rate_stat_uncert_minus"] = (
255
- l1_dataset["swp_scem_counts_stat_uncert_minus"] / TIME_PER_BIN
253
+ l1_dataset["swp_scem_counts_stat_uncert_minus"] / SWAPI_LIVETIME
256
254
  )
257
255
  l2_dataset["swp_coin_rate_stat_uncert_plus"] = (
258
- l1_dataset["swp_coin_counts_stat_uncert_plus"] / TIME_PER_BIN
256
+ l1_dataset["swp_coin_counts_stat_uncert_plus"] / SWAPI_LIVETIME
259
257
  )
260
258
  l2_dataset["swp_coin_rate_stat_uncert_minus"] = (
261
- l1_dataset["swp_coin_counts_stat_uncert_minus"] / TIME_PER_BIN
259
+ l1_dataset["swp_coin_counts_stat_uncert_minus"] / SWAPI_LIVETIME
262
260
  )
263
261
  # update attrs
264
262
  l2_dataset[
@@ -2,7 +2,6 @@
2
2
 
3
3
  import logging
4
4
  from pathlib import Path
5
- from typing import Union
6
5
 
7
6
  import numpy as np
8
7
  import numpy.typing as npt
@@ -51,7 +50,7 @@ def get_esa_dataframe(esa_table_number: int) -> pd.DataFrame:
51
50
 
52
51
 
53
52
  def deadtime_correction(
54
- counts: np.ndarray, acq_duration: Union[int, npt.NDArray]
53
+ counts: np.ndarray, acq_duration: int | npt.NDArray
55
54
  ) -> npt.NDArray:
56
55
  """
57
56
  Calculate deadtime correction.
@@ -51,7 +51,6 @@ class UltraConstants:
51
51
  Z_DSTOP: float = 2.6 / 2 # Position of stop foil on Z axis [mm]
52
52
  Z_DS: float = 46.19 - (2.6 / 2) # Position of slit on Z axis [mm]
53
53
  DF: float = 3.39 # Distance from slit to foil [mm]
54
-
55
54
  # Derived constants
56
55
  DMIN_PH_CTOF: float = (
57
56
  Z_DS - (2**0.5) * DF
@@ -79,53 +78,60 @@ class UltraConstants:
79
78
  CULLING_RPM_MIN = 2.0
80
79
  CULLING_RPM_MAX = 6.0
81
80
 
82
- # Thresholds for culling based on counts.
81
+ # Thresholds for culling based on counts (keV).
83
82
  CULLING_ENERGY_BIN_EDGES: ClassVar[list] = [
83
+ 3.0,
84
+ 10.0,
85
+ 20.0,
86
+ 50.0,
87
+ 300.0,
88
+ 1e5,
89
+ ]
90
+ PSET_ENERGY_BIN_EDGES: ClassVar[list] = [
84
91
  3.385,
85
92
  4.13722222222222,
86
- 4.13722222222222,
87
93
  5.05660493827161,
88
- 5.05660493827161,
89
- 6.18029492455419,
90
94
  6.18029492455419,
91
95
  7.55369379667734,
92
- 7.55369379667734,
93
96
  9.23229241816119,
94
- 9.23229241816119,
95
- 11.2839129555303,
96
97
  11.2839129555303,
97
98
  13.7914491678704,
98
- 13.7914491678704,
99
- 16.8562156496194,
100
99
  16.8562156496194,
101
100
  20.6020413495348,
102
- 20.6020413495348,
103
- 25.1802727605426,
104
101
  25.1802727605426,
105
102
  30.775888929552,
106
- 30.775888929552,
107
103
  37.6149753583414,
108
- 37.6149753583414,
109
- 45.9738587713061,
110
104
  45.9738587713061,
111
105
  56.1902718315964,
112
- 56.1902718315964,
113
- 68.6769989052845,
114
106
  68.6769989052845,
115
107
  83.93855421757,
116
- 83.93855421757,
117
- 102.591566265919,
118
108
  102.591566265919,
119
109
  125.38969210279,
120
- 125.38969210279,
121
110
  153.254068125632,
122
- 153.254068125632,
123
- 187.310527709106,
124
111
  187.310527709106,
125
112
  228.93508942224,
126
- 228.93508942224,
127
- 279.809553738294,
128
113
  279.809553738294,
129
114
  341.989454569026,
130
115
  1e5,
131
116
  ]
117
+
118
+ # Valid event filter constants
119
+ # Note these appear similar to image params constants
120
+ # but they should be used only for the valid event filter.
121
+ ETOFOFF1_EVENTFILTER = 100
122
+ ETOFOFF2_EVENTFILTER = -50
123
+ ETOFSLOPE1_EVENTFILTER = 6667
124
+ ETOFSLOPE2_EVENTFILTER = 7500
125
+ ETOFMAX_EVENTFILTER = 90
126
+ ETOFMIN_EVENTFILTER = -400
127
+ TOFDIFFTPMIN_EVENTFILTER = 226
128
+ TOFDIFFTPMAX_EVENTFILTER = 266
129
+
130
+ TOFXE_SPECIES_GROUPS: ClassVar[dict[str, list[int]]] = {
131
+ "proton": [3],
132
+ "non_proton": [20, 28, 36],
133
+ }
134
+ TOFXPH_SPECIES_GROUPS: ClassVar[dict[str, list[int]]] = {
135
+ "proton": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
136
+ "non_proton": [20, 21, 22, 23, 24, 25, 26],
137
+ }
@@ -1,6 +1,6 @@
1
1
  """Contains data classes to support Ultra L0 processing."""
2
2
 
3
- from typing import NamedTuple, Union
3
+ from typing import NamedTuple
4
4
 
5
5
 
6
6
  class PacketProperties(NamedTuple):
@@ -9,23 +9,21 @@ class PacketProperties(NamedTuple):
9
9
  apid: list # List of APIDs
10
10
  logical_source: list # List of logical sources
11
11
  addition_to_logical_desc: str # Description of the logical source
12
- width: Union[int, None] # Width of binary data (could be None).
12
+ width: int | None # Width of binary data (could be None).
13
13
  # Block, image_planes, pixel_window_rows, and pixel_window_columns are important for
14
14
  # decompressing the images and a description is available on page 171 of IMAP-Ultra
15
15
  # Flight Software Specification document (7523-9009_Rev_-.pdf).
16
- block: Union[int, None] # Number of values in each block (could be None).
17
- len_array: Union[
18
- int, None
19
- ] # Length of the array to be decompressed (could be None).
20
- mantissa_bit_length: Union[int, None] # used to determine the level of
16
+ block: int | None # Number of values in each block (could be None).
17
+ len_array: int | None # Length of the array to be decompressed (could be None).
18
+ mantissa_bit_length: int | None # used to determine the level of
21
19
  # precision that can be recovered from compressed data (could be None).
22
- image_planes: Union[int, None] = None
20
+ image_planes: int | None = None
23
21
  # number of images. See table 11 in the FSSD.
24
- pixel_window_rows: Union[int, None] = None
22
+ pixel_window_rows: int | None = None
25
23
  # number of rows in each image. See table 49 in the FSSD.
26
- pixel_window_columns: Union[int, None] = None
24
+ pixel_window_columns: int | None = None
27
25
  # number of columns in each image. See table 49 in the FSSD.
28
- image_planes_per_packet: Union[int, None] = None
26
+ image_planes_per_packet: int | None = None
29
27
  # number of image planes in each packet. See table 52 in the FSSD.
30
28
 
31
29
 
@@ -1,7 +1,6 @@
1
1
  """Generate ULTRA L1a CDFs."""
2
2
 
3
3
  import logging
4
- from typing import Optional
5
4
 
6
5
  import xarray as xr
7
6
 
@@ -44,7 +43,7 @@ logger = logging.getLogger(__name__)
44
43
 
45
44
 
46
45
  def ultra_l1a( # noqa: PLR0912
47
- packet_file: str, apid_input: Optional[int] = None
46
+ packet_file: str, apid_input: int | None = None
48
47
  ) -> list[xr.Dataset]:
49
48
  """
50
49
  Will process ULTRA L0 data into L1A CDF files at output_filepath.
@@ -7,13 +7,14 @@ from numpy.typing import NDArray
7
7
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset, extract_data_dict
8
8
 
9
9
  FILLVAL_UINT16 = 65535
10
+ FILLVAL_FLOAT32 = -1.0e31
10
11
  FILLVAL_FLOAT64 = -1.0e31
11
12
  FILLVAL_UINT32 = 4294967295
12
13
 
13
14
 
14
15
  def calculate_badtimes(
15
16
  extendedspin_dataset: xr.Dataset,
16
- cullingmask_spins: NDArray,
17
+ goodtimes_spins: NDArray,
17
18
  name: str,
18
19
  ) -> xr.Dataset:
19
20
  """
@@ -23,7 +24,7 @@ def calculate_badtimes(
23
24
  ----------
24
25
  extendedspin_dataset : xarray.Dataset
25
26
  Dataset containing the data.
26
- cullingmask_spins : np.ndarray
27
+ goodtimes_spins : np.ndarray
27
28
  Dataset containing the culled data.
28
29
  name : str
29
30
  Name of the dataset.
@@ -33,11 +34,9 @@ def calculate_badtimes(
33
34
  badtimes_dataset : xarray.Dataset
34
35
  Dataset containing the extendedspin data that has been culled.
35
36
  """
37
+ n_bins = extendedspin_dataset.dims["energy_bin_geometric_mean"]
36
38
  culled_spins = np.setdiff1d(
37
- extendedspin_dataset["spin_number"].values, cullingmask_spins
38
- )
39
- extendedspin_dataset = extendedspin_dataset.assign_coords(
40
- epoch=("spin_number", extendedspin_dataset["epoch"].values)
39
+ extendedspin_dataset["spin_number"].values, goodtimes_spins
41
40
  )
42
41
  filtered_dataset = extendedspin_dataset.sel(spin_number=culled_spins)
43
42
 
@@ -48,9 +47,6 @@ def calculate_badtimes(
48
47
  if badtimes_dataset["spin_number"].size == 0:
49
48
  badtimes_dataset = badtimes_dataset.drop_dims("spin_number")
50
49
  badtimes_dataset = badtimes_dataset.expand_dims(spin_number=[FILLVAL_UINT32])
51
- badtimes_dataset = badtimes_dataset.assign_coords(
52
- epoch=("spin_number", [extendedspin_dataset["epoch"].values[0]])
53
- )
54
50
  badtimes_dataset["spin_start_time"] = xr.DataArray(
55
51
  np.array([FILLVAL_FLOAT64], dtype="float64"), dims=["spin_number"]
56
52
  )
@@ -60,16 +56,44 @@ def calculate_badtimes(
60
56
  badtimes_dataset["spin_rate"] = xr.DataArray(
61
57
  np.array([FILLVAL_FLOAT64], dtype="float64"), dims=["spin_number"]
62
58
  )
59
+ badtimes_dataset["start_pulses_per_spin"] = xr.DataArray(
60
+ np.array([FILLVAL_FLOAT32], dtype="float32"),
61
+ dims=["spin_number"],
62
+ )
63
+ badtimes_dataset["stop_pulses_per_spin"] = xr.DataArray(
64
+ np.array([FILLVAL_FLOAT32], dtype="float32"),
65
+ dims=["spin_number"],
66
+ )
67
+ badtimes_dataset["coin_pulses_per_spin"] = xr.DataArray(
68
+ np.array([FILLVAL_FLOAT32], dtype="float32"),
69
+ dims=["spin_number"],
70
+ )
71
+ badtimes_dataset["rejected_events_per_spin"] = xr.DataArray(
72
+ np.array([FILLVAL_UINT32], dtype="uint32"),
73
+ dims=["spin_number"],
74
+ )
63
75
  badtimes_dataset["quality_attitude"] = xr.DataArray(
64
76
  np.array([FILLVAL_UINT16], dtype="uint16"), dims=["spin_number"]
65
77
  )
78
+ badtimes_dataset["quality_hk"] = xr.DataArray(
79
+ np.array([FILLVAL_UINT16], dtype="uint16"),
80
+ dims=["spin_number"],
81
+ )
82
+ badtimes_dataset["quality_instruments"] = xr.DataArray(
83
+ np.array([FILLVAL_UINT16], dtype="uint16"),
84
+ dims=["spin_number"],
85
+ )
66
86
  badtimes_dataset["quality_ena_rates"] = (
67
87
  ("energy_bin_geometric_mean", "spin_number"),
68
- np.full((3, 1), FILLVAL_UINT16, dtype="uint16"),
88
+ np.full((n_bins, 1), FILLVAL_UINT16, dtype="uint16"),
69
89
  )
70
90
  badtimes_dataset["ena_rates"] = (
71
91
  ("energy_bin_geometric_mean", "spin_number"),
72
- np.full((3, 1), FILLVAL_FLOAT64, dtype="float64"),
92
+ np.full((n_bins, 1), FILLVAL_FLOAT64, dtype="float64"),
93
+ )
94
+ badtimes_dataset["ena_rates_threshold"] = (
95
+ ("energy_bin_geometric_mean", "spin_number"),
96
+ np.full((n_bins, 1), FILLVAL_FLOAT32, dtype="float32"),
73
97
  )
74
98
 
75
99
  return badtimes_dataset
@@ -4,12 +4,16 @@ import numpy as np
4
4
  import xarray as xr
5
5
 
6
6
  from imap_processing.cdf.utils import parse_filename_like
7
- from imap_processing.quality_flags import ImapDEUltraFlags
7
+ from imap_processing.quality_flags import (
8
+ ImapDEOutliersUltraFlags,
9
+ ImapDEScatteringUltraFlags,
10
+ )
8
11
  from imap_processing.spice.geometry import SpiceFrame
9
12
  from imap_processing.ultra.l1b.lookup_utils import get_geometric_factor
10
13
  from imap_processing.ultra.l1b.ultra_l1b_annotated import (
11
14
  get_annotated_particle_velocity,
12
15
  )
16
+ from imap_processing.ultra.l1b.ultra_l1b_culling import flag_scattering
13
17
  from imap_processing.ultra.l1b.ultra_l1b_extended import (
14
18
  StopType,
15
19
  determine_ebin_pulse_height,
@@ -32,10 +36,13 @@ from imap_processing.ultra.l1b.ultra_l1b_extended import (
32
36
  get_spin_number,
33
37
  get_ssd_back_position_and_tof_offset,
34
38
  get_ssd_tof,
39
+ is_back_tof_valid,
40
+ is_coin_ph_valid,
35
41
  )
36
42
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
37
43
 
38
44
  FILLVAL_UINT8 = 255
45
+ FILLVAL_UINT32 = 4294967295
39
46
  FILLVAL_FLOAT32 = -1.0e31
40
47
 
41
48
 
@@ -76,7 +83,6 @@ def calculate_de(
76
83
  "event_type",
77
84
  "de_event_met",
78
85
  "phase_angle",
79
- "spin",
80
86
  ]
81
87
  dataset_keys = [
82
88
  "coin_type",
@@ -84,11 +90,13 @@ def calculate_de(
84
90
  "stop_type",
85
91
  "shcoarse",
86
92
  "phase_angle",
87
- "spin",
88
93
  ]
89
94
 
90
95
  de_dict.update(
91
- {key: de_dataset[dataset_key] for key, dataset_key in zip(keys, dataset_keys)}
96
+ {
97
+ key: de_dataset[dataset_key]
98
+ for key, dataset_key in zip(keys, dataset_keys, strict=False)
99
+ }
92
100
  )
93
101
 
94
102
  valid_mask = de_dataset["start_type"].data != FILLVAL_UINT8
@@ -114,9 +122,11 @@ def calculate_de(
114
122
  tof = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
115
123
  etof = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
116
124
  ctof = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
125
+ tof_energy = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
117
126
  magnitude_v = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
118
127
  energy = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
119
128
  e_bin = np.full(len(de_dataset["epoch"]), FILLVAL_UINT8, dtype=np.uint8)
129
+ e_bin_l1a = np.full(len(de_dataset["epoch"]), FILLVAL_UINT8, dtype=np.uint8)
120
130
  species_bin = np.full(len(de_dataset["epoch"]), FILLVAL_UINT8, dtype=np.uint8)
121
131
  t2 = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
122
132
  event_times = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
@@ -125,10 +135,19 @@ def calculate_de(
125
135
  sc_dps_velocity = np.full(shape, FILLVAL_FLOAT32, dtype=np.float32)
126
136
  helio_velocity = np.full(shape, FILLVAL_FLOAT32, dtype=np.float32)
127
137
  spin_starts = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float64)
138
+ velocities = np.full(shape, FILLVAL_FLOAT32, dtype=np.float32)
139
+ v_hat = np.full(shape, FILLVAL_FLOAT32, dtype=np.float32)
140
+ r_hat = np.full(shape, FILLVAL_FLOAT32, dtype=np.float32)
128
141
 
129
142
  start_type = np.full(len(de_dataset["epoch"]), FILLVAL_UINT8, dtype=np.uint8)
130
143
  quality_flags = np.full(
131
- de_dataset["epoch"].shape, ImapDEUltraFlags.NONE.value, dtype=np.uint16
144
+ de_dataset["epoch"].shape, ImapDEOutliersUltraFlags.NONE.value, dtype=np.uint16
145
+ )
146
+
147
+ scattering_quality_flags = np.full(
148
+ de_dataset["epoch"].shape,
149
+ ImapDEScatteringUltraFlags.NONE.value,
150
+ dtype=np.uint16,
132
151
  )
133
152
 
134
153
  xf[valid_indices] = get_front_x_position(
@@ -149,9 +168,13 @@ def calculate_de(
149
168
  )
150
169
 
151
170
  # Pulse height
152
- tof[ph_indices], t2[ph_indices], xb[ph_indices], yb[ph_indices] = (
153
- get_ph_tof_and_back_positions(de_dataset, xf, f"ultra{sensor}", ancillary_files)
171
+ ph_result = get_ph_tof_and_back_positions(
172
+ de_dataset, xf, f"ultra{sensor}", ancillary_files
154
173
  )
174
+ tof[ph_indices] = ph_result.tof
175
+ t2[ph_indices] = ph_result.t2
176
+ xb[ph_indices] = ph_result.xb
177
+ yb[ph_indices] = ph_result.yb
155
178
  d[ph_indices], yf[ph_indices] = get_front_y_position(
156
179
  de_dataset["start_type"].data[ph_indices], yb[ph_indices], ancillary_files
157
180
  )
@@ -174,16 +197,39 @@ def calculate_de(
174
197
  (xb[ph_indices], yb[ph_indices]),
175
198
  d[ph_indices],
176
199
  )
177
- e_bin[ph_indices] = determine_ebin_pulse_height(
178
- energy[ph_indices], tof[ph_indices], r[ph_indices]
179
- )
180
- species_bin[ph_indices] = determine_species(tof[ph_indices], r[ph_indices], "PH")
181
200
  etof[ph_indices], xc[ph_indices] = get_coincidence_positions(
182
201
  de_dataset.isel(epoch=ph_indices),
183
202
  t2[ph_indices],
184
203
  f"ultra{sensor}",
185
204
  ancillary_files,
186
205
  )
206
+ backtofvalid = is_back_tof_valid(
207
+ de_dataset,
208
+ xf,
209
+ f"ultra{sensor}",
210
+ ancillary_files,
211
+ )
212
+ coinphvalid = is_coin_ph_valid(
213
+ etof[ph_indices],
214
+ xc[ph_indices],
215
+ xb[ph_indices],
216
+ de_dataset["stop_north_tdc"][ph_indices].values,
217
+ de_dataset["stop_south_tdc"][ph_indices].values,
218
+ de_dataset["stop_east_tdc"][ph_indices].values,
219
+ de_dataset["stop_west_tdc"][ph_indices].values,
220
+ f"ultra{sensor}",
221
+ ancillary_files,
222
+ quality_flags[ph_indices],
223
+ )
224
+ e_bin[ph_indices] = determine_ebin_pulse_height(
225
+ energy[ph_indices],
226
+ tof[ph_indices],
227
+ r[ph_indices],
228
+ backtofvalid,
229
+ coinphvalid,
230
+ ancillary_files,
231
+ )
232
+ species_bin[ph_indices] = determine_species(e_bin[ph_indices], "PH")
187
233
  ctof[ph_indices], magnitude_v[ph_indices] = get_ctof(
188
234
  tof[ph_indices], r[ph_indices], "PH"
189
235
  )
@@ -211,11 +257,13 @@ def calculate_de(
211
257
  d[ssd_indices],
212
258
  )
213
259
  e_bin[ssd_indices] = determine_ebin_ssd(
214
- energy[ssd_indices], tof[ssd_indices], r[ssd_indices]
215
- )
216
- species_bin[ssd_indices] = determine_species(
217
- tof[ssd_indices], r[ssd_indices], "SSD"
260
+ energy[ssd_indices],
261
+ tof[ssd_indices],
262
+ r[ssd_indices],
263
+ f"ultra{sensor}",
264
+ ancillary_files,
218
265
  )
266
+ species_bin[ssd_indices] = determine_species(e_bin[ssd_indices], "SSD")
219
267
  ctof[ssd_indices], magnitude_v[ssd_indices] = get_ctof(
220
268
  tof[ssd_indices], r[ssd_indices], "SSD"
221
269
  )
@@ -237,19 +285,26 @@ def calculate_de(
237
285
  de_dict["phi"] = phi
238
286
  de_dict["theta"] = theta
239
287
 
240
- v, vhat, r = get_de_velocity(
241
- (de_dict["x_front"], de_dict["y_front"]),
242
- (de_dict["x_back"], de_dict["y_back"]),
243
- de_dict["front_back_distance"],
244
- de_dict["tof_start_stop"],
288
+ velocities[valid_indices], v_hat[valid_indices], r_hat[valid_indices] = (
289
+ get_de_velocity(
290
+ (de_dict["x_front"][valid_indices], de_dict["y_front"][valid_indices]),
291
+ (de_dict["x_back"][valid_indices], de_dict["y_back"][valid_indices]),
292
+ de_dict["front_back_distance"][valid_indices],
293
+ de_dict["tof_start_stop"][valid_indices],
294
+ )
245
295
  )
246
- de_dict["direct_event_velocity"] = v.astype(np.float32)
247
- de_dict["direct_event_unit_velocity"] = vhat.astype(np.float32)
248
- de_dict["direct_event_unit_position"] = r.astype(np.float32)
296
+ de_dict["direct_event_unit_velocity"] = v_hat.astype(np.float32)
297
+ de_dict["direct_event_unit_position"] = r_hat.astype(np.float32)
249
298
 
250
- de_dict["tof_energy"] = get_de_energy_kev(v, species_bin)
299
+ tof_energy[valid_indices] = get_de_energy_kev(
300
+ velocities[valid_indices], species_bin[valid_indices]
301
+ )
302
+ de_dict["tof_energy"] = tof_energy
251
303
  de_dict["energy"] = energy
252
- de_dict["ebin"] = e_bin
304
+ de_dict["computed_ebin"] = e_bin
305
+ valid_ebin = de_dataset["bin"].values != FILLVAL_UINT32
306
+ e_bin_l1a[valid_ebin] = de_dataset["bin"].values[valid_ebin]
307
+ de_dict["ebin"] = e_bin_l1a
253
308
  de_dict["species"] = species_bin
254
309
 
255
310
  # Annotated Events.
@@ -264,7 +319,7 @@ def calculate_de(
264
319
  helio_velocity[valid_events],
265
320
  ) = get_annotated_particle_velocity(
266
321
  event_times[valid_events],
267
- de_dict["direct_event_velocity"][valid_events],
322
+ velocities.astype(np.float32)[valid_events],
268
323
  ultra_frame,
269
324
  SpiceFrame.IMAP_DPS,
270
325
  SpiceFrame.IMAP_SPACECRAFT,
@@ -289,20 +344,29 @@ def calculate_de(
289
344
  de_dict["tof_energy"], de_dict["phi"], de_dict["theta"], ancillary_files
290
345
  )
291
346
  de_dict["geometric_factor_blades"] = get_geometric_factor(
292
- ancillary_files,
293
- "l1b-sensor-gf-blades",
294
347
  de_dict["phi"],
295
348
  de_dict["theta"],
296
349
  quality_flags,
350
+ ancillary_files,
351
+ "l1b-sensor-gf-blades",
297
352
  )
298
353
  de_dict["geometric_factor_noblades"] = get_geometric_factor(
299
- ancillary_files,
300
- "l1b-sensor-gf-noblades",
301
354
  de_dict["phi"],
302
355
  de_dict["theta"],
303
356
  quality_flags,
357
+ ancillary_files,
358
+ "l1b-sensor-gf-noblades",
359
+ )
360
+ de_dict["quality_outliers"] = quality_flags
361
+ flag_scattering(
362
+ de_dict["tof_energy"],
363
+ de_dict["theta"],
364
+ de_dict["phi"],
365
+ ancillary_files,
366
+ sensor,
367
+ scattering_quality_flags,
304
368
  )
305
- de_dict["quality_fov"] = quality_flags
369
+ de_dict["quality_scattering"] = scattering_quality_flags
306
370
 
307
371
  dataset = create_dataset(de_dict, name, "l1b")
308
372
 
@@ -2,8 +2,10 @@
2
2
 
3
3
  import numpy as np
4
4
  import xarray as xr
5
+ from numpy.typing import NDArray
5
6
 
6
7
  from imap_processing.ultra.l1b.ultra_l1b_culling import (
8
+ count_rejected_events_per_spin,
7
9
  flag_attitude,
8
10
  flag_hk,
9
11
  flag_imap_instruments,
@@ -14,6 +16,7 @@ from imap_processing.ultra.l1b.ultra_l1b_culling import (
14
16
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
15
17
 
16
18
  FILLVAL_UINT16 = 65535
19
+ FILLVAL_FLOAT32 = -1.0e31
17
20
 
18
21
 
19
22
  def calculate_extendedspin(
@@ -43,7 +46,7 @@ def calculate_extendedspin(
43
46
  de_dataset = dict_datasets[f"imap_ultra_l1b_{instrument_id}sensor-de"]
44
47
 
45
48
  extendedspin_dict = {}
46
- rates_qf, spin, energy_midpoints, n_sigma_per_energy = flag_rates(
49
+ rates_qf, spin, energy_bin_geometric_mean, n_sigma_per_energy = flag_rates(
47
50
  de_dataset["spin"].values,
48
51
  de_dataset["energy"].values,
49
52
  )
@@ -57,34 +60,46 @@ def calculate_extendedspin(
57
60
  hk_qf = flag_hk(de_dataset["spin"].values)
58
61
  inst_qf = flag_imap_instruments(de_dataset["spin"].values)
59
62
 
60
- # Get the first epoch for each spin.
61
- mask = xr.DataArray(np.isin(de_dataset["spin"], spin), dims="epoch")
62
- filtered_dataset = de_dataset.where(mask, drop=True)
63
- _, first_indices = np.unique(filtered_dataset["spin"].values, return_index=True)
64
- first_epochs = filtered_dataset["epoch"].values[first_indices]
65
-
66
63
  # Get the number of pulses per spin.
67
- start_per_spin, stop_per_spin, coin_per_spin = get_pulses_per_spin(rates_dataset)
64
+ pulses = get_pulses_per_spin(rates_dataset)
68
65
 
66
+ # Track rejected events in each spin based on
67
+ # quality flags in de l1b data.
68
+ rejected_counts = count_rejected_events_per_spin(
69
+ de_dataset["spin"].values,
70
+ de_dataset["quality_scattering"].values,
71
+ de_dataset["quality_outliers"].values,
72
+ )
69
73
  # These will be the coordinates.
70
- extendedspin_dict["epoch"] = first_epochs
71
74
  extendedspin_dict["spin_number"] = spin
72
- extendedspin_dict["energy_bin_geometric_mean"] = energy_midpoints
75
+ extendedspin_dict["energy_bin_geometric_mean"] = energy_bin_geometric_mean
73
76
 
74
77
  extendedspin_dict["ena_rates"] = count_rates
75
78
  extendedspin_dict["ena_rates_threshold"] = n_sigma_per_energy
76
79
  extendedspin_dict["spin_start_time"] = spin_starttime
77
80
  extendedspin_dict["spin_period"] = spin_period
78
81
  extendedspin_dict["spin_rate"] = spin_rates
82
+
83
+ # Get index of pulses.unique_spins corresponding to each spin.
84
+ idx: NDArray[np.intp] = np.searchsorted(pulses.unique_spins, spin)
85
+
86
+ # Validate that the spin values match
87
+ valid = (idx < pulses.unique_spins.size) & (pulses.unique_spins[idx] == spin)
88
+
89
+ start_per_spin = np.full(len(spin), FILLVAL_FLOAT32, dtype=np.float32)
90
+ stop_per_spin = np.full(len(spin), FILLVAL_FLOAT32, dtype=np.float32)
91
+ coin_per_spin = np.full(len(spin), FILLVAL_FLOAT32, dtype=np.float32)
92
+
93
+ # Fill only the valid ones
94
+ start_per_spin[valid] = pulses.start_per_spin[idx[valid]]
95
+ stop_per_spin[valid] = pulses.stop_per_spin[idx[valid]]
96
+ coin_per_spin[valid] = pulses.coin_per_spin[idx[valid]]
97
+
98
+ # account for rates spins which are not in the direct event spins
79
99
  extendedspin_dict["start_pulses_per_spin"] = start_per_spin
80
100
  extendedspin_dict["stop_pulses_per_spin"] = stop_per_spin
81
101
  extendedspin_dict["coin_pulses_per_spin"] = coin_per_spin
82
- # TODO: this will be used to track rejected events in each
83
- # spin based on quality flags in de l1b data.
84
- extendedspin_dict["rejected_events_per_spin"] = np.full_like(
85
- spin, FILLVAL_UINT16, dtype=np.uint16
86
- )
87
-
102
+ extendedspin_dict["rejected_events_per_spin"] = rejected_counts
88
103
  extendedspin_dict["quality_attitude"] = attitude_qf
89
104
  extendedspin_dict["quality_ena_rates"] = rates_qf
90
105
  extendedspin_dict["quality_hk"] = hk_qf