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
@@ -1,27 +1,46 @@
1
1
  """Calculate Pointing Set Grids."""
2
2
 
3
+ import logging
4
+
5
+ import astropy_healpix.healpy as hp
3
6
  import numpy as np
4
- import pandas as pd
5
7
  import xarray as xr
6
8
 
9
+ from imap_processing.cdf.utils import parse_filename_like
10
+ from imap_processing.quality_flags import ImapPSETUltraFlags
11
+ from imap_processing.spice.repoint import get_pointing_times
12
+ from imap_processing.spice.time import (
13
+ et_to_met,
14
+ met_to_ttj2000ns,
15
+ )
16
+ from imap_processing.ultra.l1b.ultra_l1b_culling import get_de_rejection_mask
17
+ from imap_processing.ultra.l1c.l1c_lookup_utils import (
18
+ calculate_fwhm_spun_scattering,
19
+ get_spacecraft_pointing_lookup_tables,
20
+ )
21
+ from imap_processing.ultra.l1c.ultra_l1c_culling import compute_culling_mask
7
22
  from imap_processing.ultra.l1c.ultra_l1c_pset_bins import (
8
23
  build_energy_bins,
24
+ get_efficiencies_and_geometric_function,
25
+ get_energy_delta_minus_plus,
9
26
  get_spacecraft_background_rates,
10
27
  get_spacecraft_exposure_times,
11
28
  get_spacecraft_histogram,
12
- interpolate_sensitivity,
13
29
  )
14
30
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
15
31
 
32
+ logger = logging.getLogger(__name__)
33
+
16
34
 
17
35
  def calculate_spacecraft_pset(
18
36
  de_dataset: xr.Dataset,
19
- extendedspin_dataset: xr.Dataset,
20
- cullingmask_dataset: xr.Dataset,
37
+ goodtimes_dataset: xr.Dataset,
21
38
  rates_dataset: xr.Dataset,
22
39
  params_dataset: xr.Dataset,
23
40
  name: str,
24
41
  ancillary_files: dict,
42
+ instrument_id: int,
43
+ species_id: list,
25
44
  ) -> xr.Dataset:
26
45
  """
27
46
  Create dictionary with defined datatype for Pointing Set Grid Data.
@@ -30,10 +49,8 @@ def calculate_spacecraft_pset(
30
49
  ----------
31
50
  de_dataset : xarray.Dataset
32
51
  Dataset containing de data.
33
- extendedspin_dataset : xarray.Dataset
34
- Dataset containing extendedspin data.
35
- cullingmask_dataset : xarray.Dataset
36
- Dataset containing cullingmask data.
52
+ goodtimes_dataset : xarray.Dataset
53
+ Dataset containing goodtimes data.
37
54
  rates_dataset : xarray.Dataset
38
55
  Dataset containing image rates data.
39
56
  params_dataset : xarray.Dataset
@@ -42,6 +59,10 @@ def calculate_spacecraft_pset(
42
59
  Name of the dataset.
43
60
  ancillary_files : dict
44
61
  Ancillary files.
62
+ instrument_id : int
63
+ Instrument ID, either 45 or 90.
64
+ species_id : List
65
+ Species ID.
45
66
 
46
67
  Returns
47
68
  -------
@@ -50,50 +71,140 @@ def calculate_spacecraft_pset(
50
71
  """
51
72
  pset_dict: dict[str, np.ndarray] = {}
52
73
 
53
- v_mag_dps_spacecraft = np.linalg.norm(de_dataset["velocity_dps_sc"].values, axis=1)
74
+ sensor = parse_filename_like(name)["sensor"][0:2]
75
+ indices = np.where(np.isin(de_dataset["e_bin"].values, species_id))[0]
76
+ species_dataset = de_dataset.isel(epoch=indices)
77
+
78
+ # If there are no species return None.
79
+ if indices.size == 0:
80
+ return None
81
+
82
+ # Before we use the de_dataset to calculate the pointing set grid we need to filter.
83
+ rejected = get_de_rejection_mask(
84
+ species_dataset["quality_scattering"].values,
85
+ species_dataset["quality_outliers"].values,
86
+ )
87
+ species_dataset = species_dataset.isel(epoch=~rejected)
88
+
89
+ v_mag_dps_spacecraft = np.linalg.norm(
90
+ species_dataset["velocity_dps_sc"].values, axis=1
91
+ )
54
92
  vhat_dps_spacecraft = (
55
- de_dataset["velocity_dps_sc"].values / v_mag_dps_spacecraft[:, np.newaxis]
93
+ species_dataset["velocity_dps_sc"].values / v_mag_dps_spacecraft[:, np.newaxis]
56
94
  )
57
95
 
58
96
  intervals, _, energy_bin_geometric_means = build_energy_bins()
97
+
98
+ # Get lookup table for FOR indices by spin phase step
99
+ (
100
+ for_indices_by_spin_phase,
101
+ theta_vals,
102
+ phi_vals,
103
+ ra_and_dec,
104
+ boundary_scale_factors,
105
+ ) = get_spacecraft_pointing_lookup_tables(ancillary_files, instrument_id)
106
+
107
+ logger.info("calculating spun FWHM scattering values.")
108
+ pixels_below_scattering, scattering_theta, scattering_phi, scattering_thresholds = (
109
+ calculate_fwhm_spun_scattering(
110
+ for_indices_by_spin_phase,
111
+ theta_vals,
112
+ phi_vals,
113
+ ancillary_files,
114
+ instrument_id,
115
+ )
116
+ )
117
+ # Determine nside from the lookup table
118
+ nside = hp.npix2nside(len(for_indices_by_spin_phase))
59
119
  counts, latitude, longitude, n_pix = get_spacecraft_histogram(
60
120
  vhat_dps_spacecraft,
61
- de_dataset["energy_spacecraft"].values,
121
+ species_dataset["energy_spacecraft"].values,
62
122
  intervals,
63
- nside=128,
123
+ nside=nside,
64
124
  )
65
125
  healpix = np.arange(n_pix)
66
126
 
67
- # calculate background rates
68
- background_rates = get_spacecraft_background_rates()
127
+ logger.info("Calculating spun efficiencies and geometric function.")
128
+ # calculate efficiency and geometric function as a function of energy
129
+ efficiencies, geometric_function = get_efficiencies_and_geometric_function(
130
+ pixels_below_scattering,
131
+ boundary_scale_factors,
132
+ theta_vals,
133
+ phi_vals,
134
+ n_pix,
135
+ ancillary_files,
136
+ )
137
+ sensitivity = efficiencies * geometric_function
69
138
 
70
- efficiencies = ancillary_files["l1c-90sensor-efficiencies"]
71
- geometric_function = ancillary_files["l1c-90sensor-gf"]
139
+ # Calculate exposure times
140
+ logger.info("Calculating spacecraft exposure times with deadtime correction.")
141
+ exposure_pointing, deadtime_ratios = get_spacecraft_exposure_times(
142
+ rates_dataset,
143
+ params_dataset,
144
+ pixels_below_scattering,
145
+ boundary_scale_factors,
146
+ n_pix=n_pix,
147
+ )
148
+ logger.info("Calculating background rates.")
149
+ # Calculate background rates
150
+ background_rates = get_spacecraft_background_rates(
151
+ rates_dataset,
152
+ sensor,
153
+ ancillary_files,
154
+ intervals,
155
+ goodtimes_dataset["spin_number"].values,
156
+ nside=nside,
157
+ )
158
+ spacecraft_pset_quality_flags = np.full(
159
+ n_pix, ImapPSETUltraFlags.NONE.value, dtype=np.uint16
160
+ )
72
161
 
73
- df_efficiencies = pd.read_csv(efficiencies)
74
- df_geometric_function = pd.read_csv(geometric_function)
75
- sensitivity = interpolate_sensitivity(df_efficiencies, df_geometric_function)
162
+ start: float = np.min(species_dataset["event_times"].values)
163
+ end: float = np.max(species_dataset["event_times"].values)
76
164
 
77
- # Calculate exposure
78
- constant_exposure = ancillary_files["l1c-90sensor-dps-exposure"]
79
- df_exposure = pd.read_csv(constant_exposure)
80
- exposure_pointing = get_spacecraft_exposure_times(
81
- df_exposure, rates_dataset, params_dataset
82
- )
165
+ # Time bins in 30 minute intervals
166
+ time_bins = np.arange(start, end + 1800, 1800)
83
167
 
84
- # For ISTP, epoch should be the center of the time bin.
85
- pset_dict["epoch"] = de_dataset.epoch.data[:1].astype(np.int64)
168
+ # Compute mask for culling the Earth
169
+ compute_culling_mask(
170
+ time_bins,
171
+ 6378.1, # Earth radius
172
+ spacecraft_pset_quality_flags,
173
+ nside=nside,
174
+ )
175
+ # Get pointing start and stop times and convert to ttj2000ns
176
+ pointing_start, pointing_stop = get_pointing_times(
177
+ float(et_to_met(species_dataset["event_times"].data[0]))
178
+ )
179
+ pointing_start = met_to_ttj2000ns(pointing_start)
180
+ # Epoch should be the start of the pointing
181
+ pset_dict["epoch"] = np.atleast_1d(pointing_start).astype(np.int64)
86
182
  pset_dict["counts"] = counts[np.newaxis, ...]
87
183
  pset_dict["latitude"] = latitude[np.newaxis, ...]
88
184
  pset_dict["longitude"] = longitude[np.newaxis, ...]
89
185
  pset_dict["energy_bin_geometric_mean"] = energy_bin_geometric_means
90
186
  pset_dict["background_rates"] = background_rates[np.newaxis, ...]
91
- pset_dict["exposure_factor"] = exposure_pointing.to_numpy()[np.newaxis, ...]
187
+ pset_dict["exposure_factor"] = exposure_pointing
92
188
  pset_dict["pixel_index"] = healpix
93
189
  pset_dict["energy_bin_delta"] = np.diff(intervals, axis=1).squeeze()[
94
190
  np.newaxis, ...
95
191
  ]
96
- pset_dict["sensitivity"] = sensitivity[np.newaxis, ...]
192
+ pset_dict["quality_flags"] = spacecraft_pset_quality_flags[np.newaxis, ...]
193
+
194
+ pset_dict["sensitivity"] = sensitivity
195
+ pset_dict["efficiency"] = efficiencies
196
+ pset_dict["geometric_function"] = geometric_function
197
+ pset_dict["dead_time_ratio"] = deadtime_ratios
198
+ pset_dict["spin_phase_step"] = np.arange(len(deadtime_ratios))
199
+
200
+ pset_dict["scatter_theta"] = scattering_theta
201
+ pset_dict["scatter_phi"] = scattering_phi
202
+ pset_dict["scatter_threshold"] = scattering_thresholds
203
+
204
+ # Add the energy delta plus/minus to the dataset
205
+ energy_delta_minus, energy_delta_plus = get_energy_delta_minus_plus()
206
+ pset_dict["energy_delta_minus"] = energy_delta_minus
207
+ pset_dict["energy_delta_plus"] = energy_delta_plus
97
208
 
98
209
  dataset = create_dataset(pset_dict, name, "l1c")
99
210
 
@@ -2,13 +2,13 @@
2
2
 
3
3
  import xarray as xr
4
4
 
5
+ from imap_processing.ultra.constants import UltraConstants
5
6
  from imap_processing.ultra.l1c.helio_pset import calculate_helio_pset
6
- from imap_processing.ultra.l1c.histogram import calculate_histogram
7
7
  from imap_processing.ultra.l1c.spacecraft_pset import calculate_spacecraft_pset
8
8
 
9
9
 
10
10
  def ultra_l1c(
11
- data_dict: dict, ancillary_files: dict, has_spice: bool
11
+ data_dict: dict, ancillary_files: dict, imap_frames: bool
12
12
  ) -> list[xr.Dataset]:
13
13
  """
14
14
  Will process ULTRA L1A and L1B data into L1C CDF files at output_filepath.
@@ -19,8 +19,8 @@ def ultra_l1c(
19
19
  The data itself and its dependent data.
20
20
  ancillary_files : dict
21
21
  Ancillary files.
22
- has_spice : bool
23
- Whether to use SPICE data.
22
+ imap_frames : bool
23
+ Whether to use IMAP frames.
24
24
 
25
25
  Returns
26
26
  -------
@@ -32,43 +32,52 @@ def ultra_l1c(
32
32
  # Account for possibility of having 45 and 90 in dictionary.
33
33
  for instrument_id in [45, 90]:
34
34
  if (
35
- f"imap_ultra_l1a_{instrument_id}sensor-histogram" in data_dict
36
- and f"imap_ultra_l1b_{instrument_id}sensor-cullingmask" in data_dict
37
- ):
38
- histogram_dataset = calculate_histogram(
39
- data_dict[f"imap_ultra_l1a_{instrument_id}sensor-histogram"],
40
- f"imap_ultra_l1c_{instrument_id}sensor-histogram",
41
- )
42
- output_datasets = [histogram_dataset]
43
- elif (
44
- f"imap_ultra_l1b_{instrument_id}sensor-cullingmask" in data_dict
35
+ f"imap_ultra_l1b_{instrument_id}sensor-goodtimes" in data_dict
45
36
  and f"imap_ultra_l1b_{instrument_id}sensor-de" in data_dict
46
- and f"imap_ultra_l1b_{instrument_id}sensor-extendedspin" in data_dict
47
- and has_spice
37
+ and f"imap_ultra_l1a_{instrument_id}sensor-rates" in data_dict
38
+ and f"imap_ultra_l1a_{instrument_id}sensor-params" in data_dict
39
+ and imap_frames
48
40
  ):
49
41
  helio_pset = calculate_helio_pset(
50
42
  data_dict[f"imap_ultra_l1b_{instrument_id}sensor-de"],
51
- data_dict[f"imap_ultra_l1b_{instrument_id}sensor-extendedspin"],
52
- data_dict[f"imap_ultra_l1b_{instrument_id}sensor-cullingmask"],
43
+ data_dict[f"imap_ultra_l1b_{instrument_id}sensor-goodtimes"],
44
+ data_dict[f"imap_ultra_l1a_{instrument_id}sensor-rates"],
45
+ data_dict[f"imap_ultra_l1a_{instrument_id}sensor-params"],
53
46
  f"imap_ultra_l1c_{instrument_id}sensor-heliopset",
54
47
  ancillary_files,
48
+ instrument_id,
49
+ UltraConstants.TOFXPH_SPECIES_GROUPS["proton"],
55
50
  )
56
51
  output_datasets = [helio_pset]
57
52
  elif (
58
- f"imap_ultra_l1b_{instrument_id}sensor-cullingmask" in data_dict
53
+ f"imap_ultra_l1b_{instrument_id}sensor-goodtimes" in data_dict
59
54
  and f"imap_ultra_l1b_{instrument_id}sensor-de" in data_dict
60
- and f"imap_ultra_l1b_{instrument_id}sensor-extendedspin" in data_dict
55
+ and f"imap_ultra_l1a_{instrument_id}sensor-rates" in data_dict
56
+ and f"imap_ultra_l1a_{instrument_id}sensor-params" in data_dict
61
57
  ):
62
58
  spacecraft_pset = calculate_spacecraft_pset(
63
59
  data_dict[f"imap_ultra_l1b_{instrument_id}sensor-de"],
64
- data_dict[f"imap_ultra_l1b_{instrument_id}sensor-extendedspin"],
65
- data_dict[f"imap_ultra_l1b_{instrument_id}sensor-cullingmask"],
66
- data_dict[f"imap_ultra_{instrument_id}sensor-rates"],
67
- data_dict[f"imap_ultra_{instrument_id}sensor-params"],
60
+ data_dict[f"imap_ultra_l1b_{instrument_id}sensor-goodtimes"],
61
+ data_dict[f"imap_ultra_l1a_{instrument_id}sensor-rates"],
62
+ data_dict[f"imap_ultra_l1a_{instrument_id}sensor-params"],
68
63
  f"imap_ultra_l1c_{instrument_id}sensor-spacecraftpset",
69
64
  ancillary_files,
65
+ instrument_id,
66
+ UltraConstants.TOFXPH_SPECIES_GROUPS["proton"],
70
67
  )
71
68
  output_datasets = [spacecraft_pset]
69
+ spacecraft_pset_non_proton = calculate_spacecraft_pset(
70
+ data_dict[f"imap_ultra_l1b_{instrument_id}sensor-de"],
71
+ data_dict[f"imap_ultra_l1b_{instrument_id}sensor-goodtimes"],
72
+ data_dict[f"imap_ultra_l1a_{instrument_id}sensor-rates"],
73
+ data_dict[f"imap_ultra_l1a_{instrument_id}sensor-params"],
74
+ f"imap_ultra_l1c_{instrument_id}sensor-spacecraftpset-nonproton",
75
+ ancillary_files,
76
+ instrument_id,
77
+ UltraConstants.TOFXPH_SPECIES_GROUPS["non_proton"],
78
+ )
79
+ if spacecraft_pset_non_proton is not None:
80
+ output_datasets.append(spacecraft_pset_non_proton)
72
81
  if not output_datasets:
73
82
  raise ValueError("Data dictionary does not contain the expected keys.")
74
83
 
@@ -0,0 +1,92 @@
1
+ """Culling for ULTRA L1c."""
2
+
3
+ import astropy_healpix.healpy as hp
4
+ import numpy as np
5
+ from numpy.typing import NDArray
6
+
7
+ from imap_processing.quality_flags import ImapPSETUltraFlags
8
+ from imap_processing.spice.geometry import (
9
+ SpiceBody,
10
+ SpiceFrame,
11
+ imap_state,
12
+ )
13
+
14
+
15
+ def compute_culling_mask(
16
+ et: NDArray,
17
+ keepout_radius_km: float,
18
+ pset_quality_flags: NDArray,
19
+ observer: SpiceBody = SpiceBody.EARTH,
20
+ nside: int = 128,
21
+ nested: bool = False,
22
+ ) -> tuple[NDArray, NDArray]:
23
+ """
24
+ Compute a mask for HEALPix pixels within a keep-out radius of the target body.
25
+
26
+ Parameters
27
+ ----------
28
+ et : NDArray
29
+ Ephemeris times in TDB seconds past J2000.
30
+ keepout_radius_km : float
31
+ Radius (in km) within which HEALPix pixels will be excluded.
32
+ pset_quality_flags : NDArray,
33
+ Quality flag to set when HEALPIX pixels are within a
34
+ keep-out radius of the target body.
35
+ observer : SpiceBody, optional
36
+ Body from which IMAP is observed.
37
+ nside : int, optional
38
+ HEALPix NSIDE resolution. Default is 128.
39
+ nested : bool, optional
40
+ Whether to use NESTED indexing.
41
+
42
+ Returns
43
+ -------
44
+ mask : tuple[NDArray, NDArray]
45
+ Boolean array of shape (len(et), npix).
46
+ unit_target_vecs : NDArray
47
+ Unit vectors from IMAP to the target body
48
+ (e.g., Earth), shape (len(et), 3).
49
+ """
50
+ # Compute number of HEALPix pixels
51
+ npix = hp.nside2npix(nside)
52
+
53
+ # Compute IMAP to Earth position in the pointing frame.
54
+ state = imap_state(et, ref_frame=SpiceFrame.IMAP_DPS, observer=observer)
55
+ # Flip to get vector from IMAP to Earth
56
+ # position.shape = (len(et), 3)
57
+ position = -state[:, :3]
58
+
59
+ # Distance from IMAP to target (e.g. Earth) (km):
60
+ # distance.shape = (len(et),)
61
+ distance = np.linalg.norm(position, axis=1) # shape (len(et),)
62
+
63
+ # Calculate the keepout angle (radians).
64
+ # keepout_angle.shape = (len(et),)
65
+ keepout_angle = np.arcsin(keepout_radius_km / distance) # radians
66
+
67
+ # Calculate the direction from IMAP to Earth. (shape: [N, 3])
68
+ # unit_target_vecs.shape = (len(et), 3)
69
+ unit_target_vecs = position / distance[:, np.newaxis]
70
+
71
+ # Get pixel unit vectors pointing from the center of the
72
+ # HEALPix sphere to the center of each pixel on the sky.
73
+ pixel_vecs = np.column_stack(
74
+ hp.pix2vec(nside, np.arange(npix), nest=nested)
75
+ ) # shape: (npix, 3)
76
+
77
+ # Returns cos(theta) where theta is the separation angle between:
78
+ # (1) vector from IMAP to Earth
79
+ # (2) vector from IMAP to HEALPix pixel center
80
+ # If theta is within the keepout angle, then the pixel is culled.
81
+ cos_sep = np.dot(unit_target_vecs, pixel_vecs.T) # shape (N, npix)
82
+ cos_sep = np.clip(cos_sep, -1.0, 1.0)
83
+ # Get theta here.
84
+ sep_angle = np.arccos(cos_sep)
85
+
86
+ # Exclude pixels within the keepout angle.
87
+ # mask.shape = (len(et), npix)
88
+ mask = sep_angle > keepout_angle[:, np.newaxis]
89
+ culled_any_time = np.any(~mask, axis=0) # shape: (npix,)
90
+ pset_quality_flags[culled_any_time] |= ImapPSETUltraFlags.EARTH_FOV.value
91
+
92
+ return mask, unit_target_vecs