imap-processing 0.8.0__py3-none-any.whl → 0.9.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 (99) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ccsds/excel_to_xtce.py +2 -0
  3. imap_processing/cdf/config/imap_hi_variable_attrs.yaml +100 -1
  4. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +14 -0
  5. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +63 -1
  6. imap_processing/cdf/config/imap_idex_global_cdf_attrs.yaml +7 -0
  7. imap_processing/cdf/config/imap_idex_l1a_variable_attrs.yaml +574 -231
  8. imap_processing/cdf/config/imap_idex_l1b_variable_attrs.yaml +326 -0
  9. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +33 -23
  10. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +7 -4
  11. imap_processing/cdf/utils.py +3 -5
  12. imap_processing/cli.py +13 -4
  13. imap_processing/codice/codice_l1a.py +5 -5
  14. imap_processing/codice/constants.py +9 -9
  15. imap_processing/codice/decompress.py +6 -2
  16. imap_processing/glows/l1a/glows_l1a.py +1 -2
  17. imap_processing/hi/l1a/hi_l1a.py +4 -4
  18. imap_processing/hi/l1a/histogram.py +106 -108
  19. imap_processing/hi/l1a/science_direct_event.py +91 -224
  20. imap_processing/hi/packet_definitions/TLM_HI_COMBINED_SCI.xml +3994 -0
  21. imap_processing/hit/l0/constants.py +2 -2
  22. imap_processing/hit/l0/decom_hit.py +12 -101
  23. imap_processing/hit/l1a/hit_l1a.py +164 -23
  24. imap_processing/ialirt/l0/process_codicelo.py +153 -0
  25. imap_processing/ialirt/l0/process_hit.py +5 -5
  26. imap_processing/ialirt/packet_definitions/ialirt_codicelo.xml +281 -0
  27. imap_processing/ialirt/process_ephemeris.py +212 -0
  28. imap_processing/idex/idex_l1a.py +55 -75
  29. imap_processing/idex/idex_l1b.py +192 -0
  30. imap_processing/idex/idex_variable_unpacking_and_eu_conversion.csv +33 -0
  31. imap_processing/idex/packet_definitions/idex_packet_definition.xml +97 -595
  32. imap_processing/lo/l0/decompression_tables/decompression_tables.py +16 -0
  33. imap_processing/lo/l0/lo_science.py +44 -12
  34. imap_processing/lo/l1a/lo_l1a.py +76 -8
  35. imap_processing/lo/packet_definitions/lo_xtce.xml +9877 -87
  36. imap_processing/mag/l1a/mag_l1a.py +1 -2
  37. imap_processing/mag/l1a/mag_l1a_data.py +1 -2
  38. imap_processing/mag/l1b/mag_l1b.py +2 -1
  39. imap_processing/spice/geometry.py +37 -19
  40. imap_processing/spice/time.py +144 -2
  41. imap_processing/swapi/l1/swapi_l1.py +3 -3
  42. imap_processing/swapi/packet_definitions/swapi_packet_definition.xml +1535 -446
  43. imap_processing/swe/l2/swe_l2.py +134 -17
  44. imap_processing/tests/ccsds/test_data/expected_output.xml +1 -1
  45. imap_processing/tests/codice/test_codice_l1a.py +8 -8
  46. imap_processing/tests/codice/test_decompress.py +4 -4
  47. imap_processing/tests/conftest.py +46 -43
  48. imap_processing/tests/hi/test_data/l0/H90_NHK_20241104.bin +0 -0
  49. imap_processing/tests/hi/test_data/l0/H90_sci_cnt_20241104.bin +0 -0
  50. imap_processing/tests/hi/test_data/l0/H90_sci_de_20241104.bin +0 -0
  51. imap_processing/tests/hi/test_hi_l1b.py +2 -2
  52. imap_processing/tests/hi/test_l1a.py +31 -58
  53. imap_processing/tests/hi/test_science_direct_event.py +58 -0
  54. imap_processing/tests/hit/test_data/sci_sample1.ccsds +0 -0
  55. imap_processing/tests/hit/test_decom_hit.py +60 -50
  56. imap_processing/tests/hit/test_hit_l1a.py +327 -12
  57. imap_processing/tests/hit/test_hit_l1b.py +76 -0
  58. imap_processing/tests/hit/validation_data/hskp_sample_eu.csv +89 -0
  59. imap_processing/tests/hit/validation_data/sci_sample_raw1.csv +29 -0
  60. imap_processing/tests/ialirt/test_data/l0/apid01152.tlm +0 -0
  61. imap_processing/tests/ialirt/test_data/l0/imap_codice_l1a_lo-ialirt_20241110193700_v0.0.0.cdf +0 -0
  62. imap_processing/tests/ialirt/unit/test_process_codicelo.py +106 -0
  63. imap_processing/tests/ialirt/unit/test_process_ephemeris.py +109 -0
  64. imap_processing/tests/ialirt/unit/test_process_hit.py +9 -6
  65. imap_processing/tests/idex/conftest.py +1 -1
  66. imap_processing/tests/idex/test_idex_l0.py +1 -1
  67. imap_processing/tests/idex/test_idex_l1a.py +7 -1
  68. imap_processing/tests/idex/test_idex_l1b.py +126 -0
  69. imap_processing/tests/lo/test_lo_l1a.py +7 -16
  70. imap_processing/tests/lo/test_lo_science.py +67 -3
  71. imap_processing/tests/lo/test_pkts/imap_lo_l0_raw_20240803_v002.pkts +0 -0
  72. imap_processing/tests/lo/validation_data/Instrument_FM1_T104_R129_20240803_ILO_SCI_DE_dec_DN_with_fills.csv +1999 -0
  73. imap_processing/tests/mag/test_mag_l1b.py +39 -5
  74. imap_processing/tests/spice/test_geometry.py +32 -6
  75. imap_processing/tests/spice/test_time.py +135 -6
  76. imap_processing/tests/swapi/test_swapi_decom.py +75 -69
  77. imap_processing/tests/swapi/test_swapi_l1.py +4 -4
  78. imap_processing/tests/swe/test_swe_l2.py +64 -8
  79. imap_processing/tests/test_utils.py +1 -1
  80. imap_processing/tests/ultra/test_data/l0/ultra45_raw_sc_ultrarawimg_withFSWcalcs_FM45_40P_Phi28p5_BeamCal_LinearScan_phi2850_theta-000_20240207T102740.csv +3314 -3314
  81. imap_processing/tests/ultra/unit/test_de.py +8 -3
  82. imap_processing/tests/ultra/unit/test_spatial_utils.py +125 -0
  83. imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +39 -29
  84. imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +2 -25
  85. imap_processing/ultra/constants.py +4 -0
  86. imap_processing/ultra/l1b/de.py +8 -14
  87. imap_processing/ultra/l1b/ultra_l1b_extended.py +29 -70
  88. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +1 -36
  89. imap_processing/ultra/utils/spatial_utils.py +221 -0
  90. {imap_processing-0.8.0.dist-info → imap_processing-0.9.0.dist-info}/METADATA +1 -1
  91. {imap_processing-0.8.0.dist-info → imap_processing-0.9.0.dist-info}/RECORD +94 -76
  92. imap_processing/hi/l0/__init__.py +0 -0
  93. imap_processing/hi/l0/decom_hi.py +0 -24
  94. imap_processing/hi/packet_definitions/hi_packet_definition.xml +0 -482
  95. imap_processing/tests/hi/test_decom.py +0 -55
  96. imap_processing/tests/hi/test_l1a_sci_de.py +0 -72
  97. {imap_processing-0.8.0.dist-info → imap_processing-0.9.0.dist-info}/LICENSE +0 -0
  98. {imap_processing-0.8.0.dist-info → imap_processing-0.9.0.dist-info}/WHEEL +0 -0
  99. {imap_processing-0.8.0.dist-info → imap_processing-0.9.0.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,7 @@ import numpy as np
6
6
  import pandas as pd
7
7
  import pytest
8
8
 
9
+ from imap_processing.ultra.constants import UltraConstants
9
10
  from imap_processing.ultra.l1b.de import calculate_de
10
11
 
11
12
 
@@ -66,9 +67,13 @@ def test_calculate_de(mock_get_annotated_particle_velocity, de_dataset, df_filt)
66
67
 
67
68
  # Energies and species
68
69
  assert np.allclose(dataset["energy"], df_filt["Energy"].astype("float"))
69
- assert np.allclose(
70
- dataset["species"], np.full(len(de_dataset["epoch"]), np.nan, dtype=np.uint8)
71
- )
70
+ species_array = dataset["species"][
71
+ np.where(
72
+ (dataset["tof_corrected"] > UltraConstants.CTOF_SPECIES_MIN)
73
+ & (dataset["tof_corrected"] < UltraConstants.CTOF_SPECIES_MAX)
74
+ )[0]
75
+ ]
76
+ assert np.all(species_array == "H")
72
77
 
73
78
  # Velocities in various frames
74
79
  test_tof = dataset["tof_start_stop"]
@@ -0,0 +1,125 @@
1
+ """Test creation of solid angle map."""
2
+
3
+ import numpy as np
4
+ import numpy.testing as npt
5
+ import pytest
6
+
7
+ from imap_processing.ultra.utils import spatial_utils
8
+
9
+ # Parameterize with spacings (degrees here):
10
+ valid_spacings = [0.1, 0.25, 0.5, 1, 5, 10, 20]
11
+ invalid_spacings = [0, -1, 11]
12
+ invalid_spacings_match_str = [
13
+ "Spacing must be positive valued, non-zero.",
14
+ "Spacing must be positive valued, non-zero.",
15
+ "Spacing must divide evenly into pi radians.",
16
+ ]
17
+
18
+
19
+ def test_build_spatial_bins():
20
+ """Tests build_spatial_bins function."""
21
+ az_bin_edges, el_bin_edges, az_bin_midpoints, el_bin_midpoints = (
22
+ spatial_utils.build_spatial_bins()
23
+ )
24
+
25
+ assert az_bin_edges[0] == 0
26
+ assert az_bin_edges[-1] == 360
27
+ assert len(az_bin_edges) == 721
28
+
29
+ assert el_bin_edges[0] == -90
30
+ assert el_bin_edges[-1] == 90
31
+ assert len(el_bin_edges) == 361
32
+
33
+ assert len(az_bin_midpoints) == 720
34
+ np.testing.assert_allclose(az_bin_midpoints[0], 0.25, atol=1e-4)
35
+ np.testing.assert_allclose(az_bin_midpoints[-1], 359.75, atol=1e-4)
36
+
37
+ assert len(el_bin_midpoints) == 360
38
+ np.testing.assert_allclose(el_bin_midpoints[0], -89.75, atol=1e-4)
39
+ np.testing.assert_allclose(el_bin_midpoints[-1], 89.75, atol=1e-4)
40
+
41
+
42
+ @pytest.mark.parametrize("spacing", valid_spacings)
43
+ def test_build_solid_angle_map(spacing):
44
+ """Test build_solid_angle_map function."""
45
+ solid_angle_map_steradians = spatial_utils.build_solid_angle_map(
46
+ spacing, input_degrees=True, output_degrees=False
47
+ )
48
+ assert np.isclose(np.sum(solid_angle_map_steradians), 4 * np.pi, atol=0, rtol=1e-9)
49
+
50
+ solid_angle_map_sqdeg = spatial_utils.build_solid_angle_map(
51
+ np.deg2rad(spacing), input_degrees=False, output_degrees=True
52
+ )
53
+ assert np.isclose(
54
+ np.sum(solid_angle_map_sqdeg), 4 * np.pi * (180 / np.pi) ** 2, atol=0, rtol=1e-9
55
+ )
56
+
57
+
58
+ @pytest.mark.parametrize(
59
+ "spacing, match_str", zip(invalid_spacings, invalid_spacings_match_str)
60
+ )
61
+ def test_build_solid_angle_map_invalid_spacing(spacing, match_str):
62
+ """Test build_solid_angle_map function raises error for invalid spacing."""
63
+ with pytest.raises(ValueError, match=match_str):
64
+ _ = spatial_utils.build_solid_angle_map(
65
+ spacing, input_degrees=True, output_degrees=False
66
+ )
67
+
68
+
69
+ @pytest.mark.parametrize("spacing", valid_spacings)
70
+ def test_build_az_el_grid(spacing):
71
+ """Test build_az_el_grid function."""
72
+ az_range, el_range, az_grid, el_grid = spatial_utils.build_az_el_grid(
73
+ spacing=spacing,
74
+ input_degrees=True,
75
+ output_degrees=True,
76
+ centered_azimuth=False,
77
+ centered_elevation=True,
78
+ )
79
+
80
+ # Size checks
81
+ assert az_range.size == int(360 / spacing)
82
+ assert el_range.size == int(180 / spacing)
83
+ assert az_range.size == az_grid.shape[1]
84
+ assert el_range.size == el_grid.shape[0]
85
+
86
+ # Check grid values
87
+ expected_az_range = np.arange((spacing / 2), 360 + (spacing / 2), spacing)
88
+ expected_el_range = np.arange(-90 + (spacing / 2), 90 + (spacing / 2), spacing)[
89
+ ::-1
90
+ ] # Note el order is reversed
91
+
92
+ npt.assert_allclose(az_range, expected_az_range, atol=1e-12)
93
+ npt.assert_allclose(el_range, expected_el_range, atol=1e-12)
94
+
95
+
96
+ def test_rewrap_even_spaced_el_az_grid_1d():
97
+ """Test rewrap_even_spaced_el_az_grid function, without extra axis."""
98
+ orig_shape = (180 * 12, 360 * 12)
99
+ orig_grid = np.fromfunction(lambda i, j: i**2 + j, orig_shape, dtype=int)
100
+ raveled_values = orig_grid.ravel(order="F")
101
+ rewrapped_grid_infer_shape = spatial_utils.rewrap_even_spaced_el_az_grid(
102
+ raveled_values
103
+ )
104
+ rewrapped_grid_known_shape = spatial_utils.rewrap_even_spaced_el_az_grid(
105
+ raveled_values, shape=orig_shape
106
+ )
107
+
108
+ assert np.array_equal(rewrapped_grid_infer_shape, orig_grid)
109
+ assert np.array_equal(rewrapped_grid_known_shape, orig_grid)
110
+
111
+
112
+ def test_rewrap_even_spaced_el_az_grid_2d():
113
+ """Test rewrap_even_spaced_el_az_grid function, with extra axis."""
114
+ orig_shape = (180 * 12, 360 * 12, 5)
115
+ orig_grid = np.fromfunction(lambda i, j, k: i**2 + j + k, orig_shape, dtype=int)
116
+ raveled_values = orig_grid.reshape(-1, 5, order="F")
117
+ rewrapped_grid_infer_shape = spatial_utils.rewrap_even_spaced_el_az_grid(
118
+ raveled_values, extra_axis=True
119
+ )
120
+ rewrapped_grid_known_shape = spatial_utils.rewrap_even_spaced_el_az_grid(
121
+ raveled_values, shape=orig_shape, extra_axis=True
122
+ )
123
+ assert raveled_values.shape == (180 * 12 * 360 * 12, 5)
124
+ assert np.array_equal(rewrapped_grid_infer_shape, orig_grid)
125
+ assert np.array_equal(rewrapped_grid_known_shape, orig_grid)
@@ -4,13 +4,13 @@ import numpy as np
4
4
  import pandas as pd
5
5
  import pytest
6
6
 
7
+ from imap_processing.ultra.constants import UltraConstants
7
8
  from imap_processing.ultra.l1b.ultra_l1b_extended import (
8
9
  CoinType,
9
10
  StartType,
10
11
  StopType,
11
12
  calculate_etof_xc,
12
- determine_species_pulse_height,
13
- determine_species_ssd,
13
+ determine_species,
14
14
  get_coincidence_positions,
15
15
  get_ctof,
16
16
  get_energy_pulse_height,
@@ -254,7 +254,7 @@ def test_get_unit_vector(de_dataset, yf_fixture):
254
254
  def test_get_ssd_tof(de_dataset, yf_fixture):
255
255
  """Tests get_ssd_tof function."""
256
256
  df_filt, _, _ = yf_fixture
257
- df_ssd = df_filt[df_filt["StopType"].isin(StopType.SSD.value)]
257
+ df_ssd = df_filt[np.isin(df_filt["StopType"], [StopType.SSD.value])]
258
258
  test_xf = df_filt["Xf"].astype("float").values
259
259
 
260
260
  ssd_tof = get_ssd_tof(de_dataset, test_xf)
@@ -267,7 +267,7 @@ def test_get_ssd_tof(de_dataset, yf_fixture):
267
267
  def test_get_energy_ssd(de_dataset, yf_fixture):
268
268
  """Tests get_energy_ssd function."""
269
269
  df_filt, _, _ = yf_fixture
270
- df_ssd = df_filt[df_filt["StopType"].isin(StopType.SSD.value)]
270
+ df_ssd = df_filt[np.isin(df_filt["StopType"], [StopType.SSD.value])]
271
271
  _, _, ssd_number = get_ssd_back_position_and_tof_offset(de_dataset)
272
272
  energy = get_energy_ssd(de_dataset, ssd_number)
273
273
  test_energy = df_ssd["Energy"].astype("float")
@@ -278,7 +278,7 @@ def test_get_energy_ssd(de_dataset, yf_fixture):
278
278
  def test_get_energy_pulse_height(de_dataset, yf_fixture):
279
279
  """Tests get_energy_ssd function."""
280
280
  df_filt, _, _ = yf_fixture
281
- df_ph = df_filt[df_filt["StopType"].isin(StopType.PH.value)]
281
+ df_ph = df_filt[np.isin(df_filt["StopType"], [StopType.PH.value])]
282
282
  ph_indices = np.nonzero(
283
283
  np.isin(de_dataset["STOP_TYPE"], [StopType.Top.value, StopType.Bottom.value])
284
284
  )[0]
@@ -297,18 +297,19 @@ def test_get_energy_pulse_height(de_dataset, yf_fixture):
297
297
  def test_get_ctof(yf_fixture):
298
298
  """Tests get_ctof function."""
299
299
  df_filt, _, _ = yf_fixture
300
+ df_filt = df_filt[df_filt["eTOF"].astype("str") != "FILL"]
301
+ df_filt = df_filt[df_filt["cTOF"].astype("float") > 0]
300
302
 
301
- df_ph = df_filt[df_filt["StopType"].isin([StopType.PH.value])]
303
+ df_ph = df_filt[np.isin(df_filt["StopType"], [StopType.PH.value])]
304
+ df_ssd = df_filt[np.isin(df_filt["StopType"], [StopType.SSD.value])]
302
305
 
303
- df_ssd = df_filt[df_filt["StopType"].isin([StopType.SSD.value])]
304
-
305
- ph_ctof = get_ctof(
306
+ ph_ctof, ph_magnitude_v = get_ctof(
306
307
  df_ph["TOF"].astype("float").to_numpy(),
307
308
  df_ph["r"].astype("float").to_numpy(),
308
309
  "PH",
309
310
  )
310
311
 
311
- ssd_ctof = get_ctof(
312
+ ssd_ctof, ssd_magnitude_v = get_ctof(
312
313
  df_ssd["TOF"].astype("float").to_numpy(),
313
314
  df_ssd["r"].astype("float").to_numpy(),
314
315
  "SSD",
@@ -320,33 +321,42 @@ def test_get_ctof(yf_fixture):
320
321
  np.testing.assert_allclose(
321
322
  ssd_ctof, df_ssd["cTOF"].astype("float"), atol=1e-05, rtol=0
322
323
  )
324
+ np.testing.assert_allclose(
325
+ ph_magnitude_v, df_ph["vmag"].astype("float"), atol=1e-01, rtol=0
326
+ )
327
+ np.testing.assert_allclose(
328
+ ssd_magnitude_v, df_ssd["vmag"].astype("float"), atol=1e-01, rtol=0
329
+ )
323
330
 
324
331
 
325
- def test_determine_species_ph(yf_fixture):
326
- """Tests determine_species_ph function."""
332
+ def test_determine_species(yf_fixture):
333
+ """Tests determine_species function."""
327
334
  df_filt, _, _ = yf_fixture
328
- df_ph = df_filt[df_filt["StopType"].isin(StopType.PH.value)]
335
+ df_ph = df_filt[np.isin(df_filt["StopType"], [StopType.PH.value])]
336
+ df_ssd = df_filt[np.isin(df_filt["StopType"], [StopType.SSD.value])]
329
337
 
330
- bin = determine_species_pulse_height(
331
- df_ph["Energy"].astype("float").to_numpy(),
338
+ species_bin_ph = determine_species(
332
339
  df_ph["TOF"].astype("float").to_numpy(),
333
340
  df_ph["r"].astype("float").to_numpy(),
341
+ "PH",
334
342
  )
335
-
336
- # TODO: add in bin values.
337
- np.testing.assert_allclose(bin, np.zeros(len(bin)), atol=1e-05, rtol=0)
338
-
339
-
340
- def test_determine_species_ssd(yf_fixture):
341
- """Tests determine_species_ssd function."""
342
- df_filt, _, _ = yf_fixture
343
- df_ssd = df_filt[df_filt["StopType"].isin(StopType.SSD.value)]
344
-
345
- bin = determine_species_ssd(
346
- df_ssd["Energy"].astype("float").to_numpy(),
343
+ species_bin_ssd = determine_species(
347
344
  df_ssd["TOF"].astype("float").to_numpy(),
348
345
  df_ssd["r"].astype("float").to_numpy(),
346
+ "SSD",
349
347
  )
350
348
 
351
- # TODO: add in bin values.
352
- np.testing.assert_allclose(bin, np.zeros(len(bin)), atol=1e-05, rtol=0)
349
+ h_indices_ph = np.where(species_bin_ph == "H")[0]
350
+ ctof_indices_ph = np.where(
351
+ (df_ph["cTOF"].astype("float") > UltraConstants.CTOF_SPECIES_MIN)
352
+ & (df_ph["cTOF"].astype("float") < UltraConstants.CTOF_SPECIES_MAX)
353
+ )[0]
354
+
355
+ h_indices_ssd = np.where(species_bin_ssd == "H")[0]
356
+ ctof_indices_ssd = np.where(
357
+ (df_ssd["cTOF"].astype("float") > UltraConstants.CTOF_SPECIES_MIN)
358
+ & (df_ssd["cTOF"].astype("float") < UltraConstants.CTOF_SPECIES_MAX)
359
+ )[0]
360
+
361
+ np.testing.assert_array_equal(h_indices_ph, ctof_indices_ph)
362
+ np.testing.assert_array_equal(h_indices_ssd, ctof_indices_ssd)
@@ -8,12 +8,12 @@ from cdflib import CDF
8
8
  from imap_processing import imap_module_directory
9
9
  from imap_processing.ultra.l1c.ultra_l1c_pset_bins import (
10
10
  build_energy_bins,
11
- build_spatial_bins,
12
11
  get_helio_exposure_times,
13
12
  get_histogram,
14
13
  get_pointing_frame_exposure_times,
15
14
  get_pointing_frame_sensitivity,
16
15
  )
16
+ from imap_processing.ultra.utils.spatial_utils import build_spatial_bins
17
17
 
18
18
  BASE_PATH = imap_module_directory / "ultra" / "lookup_tables"
19
19
 
@@ -47,29 +47,6 @@ def test_build_energy_bins():
47
47
  np.testing.assert_allclose(energy_bin_end[-1], 341.989, atol=1e-4)
48
48
 
49
49
 
50
- def test_build_spatial_bins():
51
- """Tests build_spatial_bins function."""
52
- az_bin_edges, el_bin_edges, az_bin_midpoints, el_bin_midpoints = (
53
- build_spatial_bins()
54
- )
55
-
56
- assert az_bin_edges[0] == 0
57
- assert az_bin_edges[-1] == 360
58
- assert len(az_bin_edges) == 721
59
-
60
- assert el_bin_edges[0] == -90
61
- assert el_bin_edges[-1] == 90
62
- assert len(el_bin_edges) == 361
63
-
64
- assert len(az_bin_midpoints) == 720
65
- np.testing.assert_allclose(az_bin_midpoints[0], 0.25, atol=1e-4)
66
- np.testing.assert_allclose(az_bin_midpoints[-1], 359.75, atol=1e-4)
67
-
68
- assert len(el_bin_midpoints) == 360
69
- np.testing.assert_allclose(el_bin_midpoints[0], -89.75, atol=1e-4)
70
- np.testing.assert_allclose(el_bin_midpoints[-1], 89.75, atol=1e-4)
71
-
72
-
73
50
  def test_get_histogram(test_data):
74
51
  """Tests get_histogram function."""
75
52
  v, energy = test_data
@@ -109,7 +86,7 @@ def test_get_pointing_frame_exposure_times():
109
86
 
110
87
  @pytest.mark.external_kernel()
111
88
  @pytest.mark.use_test_metakernel("imap_ena_sim_metakernel.template")
112
- def test_et_helio_exposure_times():
89
+ def test_get_helio_exposure_times():
113
90
  """Tests get_helio_exposure_times function."""
114
91
 
115
92
  constant_exposure = BASE_PATH / "dps_grid45_compressed.cdf"
@@ -67,3 +67,7 @@ class UltraConstants:
67
67
  ALPHA = 0.2 # deltaE/E
68
68
  ENERGY_START = 3.385 # energy start for the Ultra grids
69
69
  N_BINS = 23 # number of energy bins
70
+
71
+ # Constants for species determination based on ctof range.
72
+ CTOF_SPECIES_MIN = 50
73
+ CTOF_SPECIES_MAX = 200
@@ -10,8 +10,7 @@ from imap_processing.ultra.l1b.ultra_l1b_annotated import (
10
10
  )
11
11
  from imap_processing.ultra.l1b.ultra_l1b_extended import (
12
12
  StopType,
13
- determine_species_pulse_height,
14
- determine_species_ssd,
13
+ determine_species,
15
14
  get_coincidence_positions,
16
15
  get_ctof,
17
16
  get_energy_pulse_height,
@@ -57,9 +56,8 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
57
56
  etof = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
58
57
  ctof = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
59
58
  energy = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
60
- # TODO: uint8 fills with zeros instead of nans.
61
- # Confirm with Ultra team what fill values and dtype we want.
62
- species_bin = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.uint8)
59
+ # TODO: Confirm with Ultra team what fill values and dtype we want.
60
+ species_bin = np.full(len(de_dataset["epoch"]), "UNKNOWN", dtype="U10")
63
61
  t2 = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
64
62
 
65
63
  # Drop events with invalid start type.
@@ -95,13 +93,11 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
95
93
  (xb[ph_indices], yb[ph_indices]),
96
94
  d[ph_indices],
97
95
  )
98
- species_bin[ph_indices] = determine_species_pulse_height(
99
- energy[ph_indices], tof[ph_indices], r[ph_indices]
100
- )
96
+ species_bin[ph_indices] = determine_species(tof[ph_indices], r[ph_indices], "PH")
101
97
  etof[ph_indices], xc[ph_indices] = get_coincidence_positions(
102
98
  de_dataset.isel(epoch=ph_indices), t2[ph_indices], f"ultra{sensor}"
103
99
  )
104
- ctof[ph_indices] = get_ctof(tof[ph_indices], r[ph_indices], "PH")
100
+ ctof[ph_indices], _ = get_ctof(tof[ph_indices], r[ph_indices], "PH")
105
101
 
106
102
  # SSD
107
103
  ssd_indices = np.nonzero(np.isin(de_dataset["STOP_TYPE"], StopType.SSD.value))[0]
@@ -119,12 +115,10 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
119
115
  (xb[ssd_indices], yb[ssd_indices]),
120
116
  d[ssd_indices],
121
117
  )
122
- species_bin[ssd_indices] = determine_species_ssd(
123
- energy[ssd_indices],
124
- tof[ssd_indices],
125
- r[ssd_indices],
118
+ species_bin[ssd_indices] = determine_species(
119
+ tof[ssd_indices], r[ssd_indices], "SSD"
126
120
  )
127
- ctof[ssd_indices] = get_ctof(tof[ssd_indices], r[ssd_indices], "SSD")
121
+ ctof[ssd_indices], _ = get_ctof(tof[ssd_indices], r[ssd_indices], "SSD")
128
122
 
129
123
  # Combine ph_yb and ssd_yb along with their indices
130
124
  de_dict["x_front"] = xf.astype(np.float32)
@@ -648,9 +648,11 @@ def get_energy_ssd(de_dataset: xarray.Dataset, ssd: np.ndarray) -> NDArray[np.fl
648
648
  return energy_norm
649
649
 
650
650
 
651
- def get_ctof(tof: np.ndarray, path_length: np.ndarray, type: str) -> NDArray:
651
+ def get_ctof(
652
+ tof: np.ndarray, path_length: np.ndarray, type: str
653
+ ) -> tuple[NDArray, NDArray]:
652
654
  """
653
- Calculate the corrected TOF.
655
+ Calculate the corrected TOF and the magnitude of the particle velocity.
654
656
 
655
657
  The corrected TOF (ctof) is the TOF normalized with respect
656
658
  to a fixed distance dmin between the front and back detectors.
@@ -666,33 +668,35 @@ def get_ctof(tof: np.ndarray, path_length: np.ndarray, type: str) -> NDArray:
666
668
  path_length : np.ndarray
667
669
  Path length (r) (hundredths of a millimeter).
668
670
  type : str
669
- Type of event, either "ph" or "ssd".
671
+ Type of event, either "PH" or "SSD".
670
672
 
671
673
  Returns
672
674
  -------
673
675
  ctof : np.ndarray
674
676
  Corrected TOF (tenths of a ns).
677
+ magnitude_v : np.ndarray
678
+ Magnitude of the particle velocity (km/s).
675
679
  """
676
680
  dmin_ctof = getattr(UltraConstants, f"DMIN_{type}_CTOF")
677
681
 
678
682
  # Multiply times 100 to convert to hundredths of a millimeter.
679
683
  ctof = tof * dmin_ctof * 100 / path_length
680
684
 
681
- return ctof
685
+ # Convert from mm/0.1ns to km/s.
686
+ magnitude_v = dmin_ctof / ctof * 1e4
682
687
 
688
+ return ctof, magnitude_v
683
689
 
684
- def determine_species_pulse_height(
685
- energy: np.ndarray, tof: np.ndarray, path_length: np.ndarray
686
- ) -> NDArray:
690
+
691
+ def determine_species(tof: np.ndarray, path_length: np.ndarray, type: str) -> NDArray:
687
692
  """
688
693
  Determine the species for pulse-height events.
689
694
 
690
- Species is determined from the particle energy and velocity.
695
+ Species is determined from the particle velocity.
691
696
  For velocity, the particle TOF is normalized with respect
692
697
  to a fixed distance dmin between the front and back detectors.
693
698
  The normalized TOF is termed the corrected TOF (ctof).
694
- Particle species are determined from
695
- the energy and ctof using a lookup table.
699
+ Particle species are determined from ctof using thresholds.
696
700
 
697
701
  Further description is available on pages 42-44 of
698
702
  IMAP-Ultra Flight Software Specification document
@@ -700,72 +704,27 @@ def determine_species_pulse_height(
700
704
 
701
705
  Parameters
702
706
  ----------
703
- energy : np.ndarray
704
- Energy from the SSD event (keV).
705
- tof : np.ndarray
706
- Time of flight of the SSD event (tenths of a nanosecond).
707
- path_length : np.ndarray
708
- Path length (r) (hundredths of a millimeter).
709
-
710
- Returns
711
- -------
712
- bin : np.array
713
- Species bin.
714
- """
715
- # PH event TOF normalization to Z axis
716
- ctof = get_ctof(tof, path_length, "PH")
717
- # TODO: need lookup tables
718
- # placeholder
719
- bin = np.zeros(len(ctof))
720
- # bin = PHxTOFSpecies[ctof, energy]
721
-
722
- return bin
723
-
724
-
725
- def determine_species_ssd(
726
- energy: np.ndarray, tof: np.ndarray, path_length: np.ndarray
727
- ) -> NDArray:
728
- """
729
- Determine the species for SSD events.
730
-
731
- Species is determined from the particle's energy and velocity.
732
- For velocity, the particle's TOF is normalized with respect
733
- to a fixed distance dmin between the front and back detectors.
734
- For SSD events, an adjustment is also made to the path length
735
- to account for the shorter distances that such events
736
- travel to reach the detector. The normalized TOF is termed
737
- the corrected tof (ctof). Particle species are determined from
738
- the energy and cTOF using a lookup table.
739
-
740
- Further description is available on pages 42-44 of
741
- IMAP-Ultra Flight Software Specification document
742
- (7523-9009_Rev_-.pdf).
743
-
744
- Parameters
745
- ----------
746
- energy : np.ndarray
747
- Energy from the SSD event (keV).
748
707
  tof : np.ndarray
749
708
  Time of flight of the SSD event (tenths of a nanosecond).
750
709
  path_length : np.ndarray
751
710
  Path length (r) (hundredths of a millimeter).
711
+ type : str
712
+ Type of data (PH or SSD).
752
713
 
753
714
  Returns
754
715
  -------
755
- bin : np.ndarray
716
+ species_bin : np.array
756
717
  Species bin.
757
718
  """
758
- # SSD event TOF normalization to Z axis
759
- ctof = get_ctof(tof, path_length, "SSD")
760
-
761
- bin = np.zeros(len(ctof)) # placeholder
762
-
763
- # TODO: get these lookup tables
764
- # if r < get_image_params("PathSteepThresh"):
765
- # # bin = ExTOFSpeciesSteep[energy, ctof]
766
- # elif r < get_image_params("PathMediumThresh"):
767
- # # bin = ExTOFSpeciesMedium[energy, ctof]
768
- # else:
769
- # # bin = ExTOFSpeciesFlat[energy, ctof]
770
-
771
- return bin
719
+ # Event TOF normalization to Z axis
720
+ ctof, _ = get_ctof(tof, path_length, type)
721
+ # Initialize bin array
722
+ species_bin = np.full(len(ctof), "UNKNOWN", dtype="U10")
723
+
724
+ # Assign "H" to bins where cTOF is within the specified range
725
+ species_bin[
726
+ (ctof > UltraConstants.CTOF_SPECIES_MIN)
727
+ & (ctof < UltraConstants.CTOF_SPECIES_MAX)
728
+ ] = "H"
729
+
730
+ return species_bin
@@ -13,6 +13,7 @@ from imap_processing.spice.geometry import (
13
13
  spherical_to_cartesian,
14
14
  )
15
15
  from imap_processing.ultra.constants import UltraConstants
16
+ from imap_processing.ultra.utils.spatial_utils import build_spatial_bins
16
17
 
17
18
  # TODO: add species binning.
18
19
 
@@ -47,42 +48,6 @@ def build_energy_bins() -> tuple[list[tuple[float, float]], np.ndarray]:
47
48
  return intervals, energy_midpoints
48
49
 
49
50
 
50
- def build_spatial_bins(
51
- az_spacing: float = 0.5,
52
- el_spacing: float = 0.5,
53
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
54
- """
55
- Build spatial bin boundaries for azimuth and elevation.
56
-
57
- Parameters
58
- ----------
59
- az_spacing : float, optional
60
- The azimuth bin spacing in degrees (default is 0.5 degrees).
61
- el_spacing : float, optional
62
- The elevation bin spacing in degrees (default is 0.5 degrees).
63
-
64
- Returns
65
- -------
66
- az_bin_edges : np.ndarray
67
- Array of azimuth bin boundary values.
68
- el_bin_edges : np.ndarray
69
- Array of elevation bin boundary values.
70
- az_bin_midpoints : np.ndarray
71
- Array of azimuth bin midpoint values.
72
- el_bin_midpoints : np.ndarray
73
- Array of elevation bin midpoint values.
74
- """
75
- # Azimuth bins from 0 to 360 degrees.
76
- az_bin_edges = np.arange(0, 360 + az_spacing, az_spacing)
77
- az_bin_midpoints = az_bin_edges[:-1] + az_spacing / 2 # Midpoints between edges
78
-
79
- # Elevation bins from -90 to 90 degrees.
80
- el_bin_edges = np.arange(-90, 90 + el_spacing, el_spacing)
81
- el_bin_midpoints = el_bin_edges[:-1] + el_spacing / 2 # Midpoints between edges
82
-
83
- return az_bin_edges, el_bin_edges, az_bin_midpoints, el_bin_midpoints
84
-
85
-
86
51
  def get_histogram(
87
52
  vhat: tuple[np.ndarray, np.ndarray, np.ndarray],
88
53
  energy: np.ndarray,