imap-processing 0.7.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 (172) hide show
  1. imap_processing/__init__.py +1 -1
  2. imap_processing/_version.py +2 -2
  3. imap_processing/ccsds/excel_to_xtce.py +36 -2
  4. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +1 -1
  5. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +145 -30
  6. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +36 -36
  7. imap_processing/cdf/config/imap_hi_variable_attrs.yaml +136 -9
  8. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +14 -0
  9. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +63 -1
  10. imap_processing/cdf/config/imap_hit_l1b_variable_attrs.yaml +9 -0
  11. imap_processing/cdf/config/imap_idex_global_cdf_attrs.yaml +14 -7
  12. imap_processing/cdf/config/imap_idex_l1a_variable_attrs.yaml +577 -235
  13. imap_processing/cdf/config/imap_idex_l1b_variable_attrs.yaml +326 -0
  14. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +33 -23
  15. imap_processing/cdf/config/imap_mag_l1_variable_attrs.yaml +24 -28
  16. imap_processing/cdf/config/imap_ultra_l1a_variable_attrs.yaml +1 -0
  17. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +137 -79
  18. imap_processing/cdf/config/imap_variable_schema.yaml +13 -0
  19. imap_processing/cdf/imap_cdf_manager.py +31 -27
  20. imap_processing/cdf/utils.py +3 -5
  21. imap_processing/cli.py +25 -14
  22. imap_processing/codice/codice_l1a.py +153 -63
  23. imap_processing/codice/constants.py +10 -10
  24. imap_processing/codice/decompress.py +10 -11
  25. imap_processing/codice/utils.py +1 -0
  26. imap_processing/glows/l1a/glows_l1a.py +1 -2
  27. imap_processing/glows/l1b/glows_l1b.py +3 -3
  28. imap_processing/glows/l1b/glows_l1b_data.py +59 -37
  29. imap_processing/glows/l2/glows_l2_data.py +123 -0
  30. imap_processing/hi/l1a/hi_l1a.py +4 -4
  31. imap_processing/hi/l1a/histogram.py +107 -109
  32. imap_processing/hi/l1a/science_direct_event.py +92 -225
  33. imap_processing/hi/l1b/hi_l1b.py +85 -11
  34. imap_processing/hi/l1c/hi_l1c.py +23 -1
  35. imap_processing/hi/packet_definitions/TLM_HI_COMBINED_SCI.xml +3994 -0
  36. imap_processing/hi/utils.py +1 -1
  37. imap_processing/hit/hit_utils.py +221 -0
  38. imap_processing/hit/l0/constants.py +118 -0
  39. imap_processing/hit/l0/decom_hit.py +100 -156
  40. imap_processing/hit/l1a/hit_l1a.py +170 -184
  41. imap_processing/hit/l1b/hit_l1b.py +33 -153
  42. imap_processing/ialirt/l0/process_codicelo.py +153 -0
  43. imap_processing/ialirt/l0/process_hit.py +5 -5
  44. imap_processing/ialirt/packet_definitions/ialirt_codicelo.xml +281 -0
  45. imap_processing/ialirt/process_ephemeris.py +212 -0
  46. imap_processing/idex/idex_l1a.py +65 -84
  47. imap_processing/idex/idex_l1b.py +192 -0
  48. imap_processing/idex/idex_variable_unpacking_and_eu_conversion.csv +33 -0
  49. imap_processing/idex/packet_definitions/idex_packet_definition.xml +97 -595
  50. imap_processing/lo/l0/decompression_tables/decompression_tables.py +17 -1
  51. imap_processing/lo/l0/lo_science.py +45 -13
  52. imap_processing/lo/l1a/lo_l1a.py +76 -8
  53. imap_processing/lo/packet_definitions/lo_xtce.xml +8344 -1849
  54. imap_processing/mag/l0/decom_mag.py +4 -3
  55. imap_processing/mag/l1a/mag_l1a.py +12 -13
  56. imap_processing/mag/l1a/mag_l1a_data.py +1 -2
  57. imap_processing/mag/l1b/mag_l1b.py +90 -7
  58. imap_processing/spice/geometry.py +156 -16
  59. imap_processing/spice/time.py +144 -2
  60. imap_processing/swapi/l1/swapi_l1.py +4 -4
  61. imap_processing/swapi/l2/swapi_l2.py +1 -1
  62. imap_processing/swapi/packet_definitions/swapi_packet_definition.xml +1535 -446
  63. imap_processing/swe/l1b/swe_l1b_science.py +8 -8
  64. imap_processing/swe/l2/swe_l2.py +134 -17
  65. imap_processing/tests/ccsds/test_data/expected_output.xml +2 -1
  66. imap_processing/tests/ccsds/test_excel_to_xtce.py +4 -4
  67. imap_processing/tests/cdf/test_imap_cdf_manager.py +0 -10
  68. imap_processing/tests/codice/conftest.py +1 -17
  69. imap_processing/tests/codice/data/imap_codice_l0_raw_20241110_v001.pkts +0 -0
  70. imap_processing/tests/codice/test_codice_l0.py +8 -2
  71. imap_processing/tests/codice/test_codice_l1a.py +127 -107
  72. imap_processing/tests/codice/test_codice_l1b.py +1 -0
  73. imap_processing/tests/codice/test_decompress.py +7 -7
  74. imap_processing/tests/conftest.py +100 -58
  75. imap_processing/tests/glows/conftest.py +6 -0
  76. imap_processing/tests/glows/test_glows_l1b.py +9 -9
  77. imap_processing/tests/glows/test_glows_l1b_data.py +9 -9
  78. imap_processing/tests/hi/test_data/l0/H90_NHK_20241104.bin +0 -0
  79. imap_processing/tests/hi/test_data/l0/H90_sci_cnt_20241104.bin +0 -0
  80. imap_processing/tests/hi/test_data/l0/H90_sci_de_20241104.bin +0 -0
  81. imap_processing/tests/hi/test_data/l1a/imap_hi_l1a_45sensor-de_20250415_v000.cdf +0 -0
  82. imap_processing/tests/hi/test_hi_l1b.py +73 -3
  83. imap_processing/tests/hi/test_hi_l1c.py +10 -2
  84. imap_processing/tests/hi/test_l1a.py +31 -58
  85. imap_processing/tests/hi/test_science_direct_event.py +58 -0
  86. imap_processing/tests/hi/test_utils.py +4 -3
  87. imap_processing/tests/hit/test_data/sci_sample1.ccsds +0 -0
  88. imap_processing/tests/hit/{test_hit_decom.py → test_decom_hit.py} +95 -36
  89. imap_processing/tests/hit/test_hit_l1a.py +299 -179
  90. imap_processing/tests/hit/test_hit_l1b.py +231 -24
  91. imap_processing/tests/hit/test_hit_utils.py +218 -0
  92. imap_processing/tests/hit/validation_data/hskp_sample_eu.csv +89 -0
  93. imap_processing/tests/hit/validation_data/sci_sample_raw1.csv +29 -0
  94. imap_processing/tests/ialirt/test_data/l0/apid01152.tlm +0 -0
  95. imap_processing/tests/ialirt/test_data/l0/imap_codice_l1a_lo-ialirt_20241110193700_v0.0.0.cdf +0 -0
  96. imap_processing/tests/ialirt/unit/test_process_codicelo.py +106 -0
  97. imap_processing/tests/ialirt/unit/test_process_ephemeris.py +109 -0
  98. imap_processing/tests/ialirt/unit/test_process_hit.py +9 -6
  99. imap_processing/tests/idex/conftest.py +2 -2
  100. imap_processing/tests/idex/imap_idex_l0_raw_20231214_v001.pkts +0 -0
  101. imap_processing/tests/idex/impact_14_tof_high_data.txt +4444 -4444
  102. imap_processing/tests/idex/test_idex_l0.py +4 -4
  103. imap_processing/tests/idex/test_idex_l1a.py +8 -2
  104. imap_processing/tests/idex/test_idex_l1b.py +126 -0
  105. imap_processing/tests/lo/test_lo_l1a.py +7 -16
  106. imap_processing/tests/lo/test_lo_science.py +69 -5
  107. imap_processing/tests/lo/test_pkts/imap_lo_l0_raw_20240803_v002.pkts +0 -0
  108. imap_processing/tests/lo/validation_data/Instrument_FM1_T104_R129_20240803_ILO_SCI_DE_dec_DN_with_fills.csv +1999 -0
  109. imap_processing/tests/mag/imap_mag_l1a_norm-magi_20251017_v001.cdf +0 -0
  110. imap_processing/tests/mag/test_mag_l1b.py +97 -7
  111. imap_processing/tests/spice/test_data/imap_ena_sim_metakernel.template +3 -1
  112. imap_processing/tests/spice/test_geometry.py +115 -9
  113. imap_processing/tests/spice/test_time.py +135 -6
  114. imap_processing/tests/swapi/test_swapi_decom.py +75 -69
  115. imap_processing/tests/swapi/test_swapi_l1.py +4 -4
  116. imap_processing/tests/swe/conftest.py +33 -0
  117. imap_processing/tests/swe/l1_validation/swe_l0_unpacked-data_20240510_v001_VALIDATION_L1B_v3.dat +4332 -0
  118. imap_processing/tests/swe/test_swe_l1b.py +29 -8
  119. imap_processing/tests/swe/test_swe_l2.py +64 -8
  120. imap_processing/tests/test_utils.py +2 -2
  121. imap_processing/tests/ultra/test_data/l0/ultra45_raw_sc_ultrarawimg_withFSWcalcs_FM45_40P_Phi28p5_BeamCal_LinearScan_phi2850_theta-000_20240207T102740.csv +3314 -3314
  122. imap_processing/tests/ultra/test_data/l1/dps_exposure_helio_45_E12.cdf +0 -0
  123. imap_processing/tests/ultra/test_data/l1/dps_exposure_helio_45_E24.cdf +0 -0
  124. imap_processing/tests/ultra/unit/test_de.py +113 -0
  125. imap_processing/tests/ultra/unit/test_spatial_utils.py +125 -0
  126. imap_processing/tests/ultra/unit/test_ultra_l1b.py +27 -3
  127. imap_processing/tests/ultra/unit/test_ultra_l1b_annotated.py +31 -10
  128. imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +55 -35
  129. imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +10 -68
  130. imap_processing/ultra/constants.py +12 -3
  131. imap_processing/ultra/l1b/de.py +168 -30
  132. imap_processing/ultra/l1b/ultra_l1b_annotated.py +24 -10
  133. imap_processing/ultra/l1b/ultra_l1b_extended.py +46 -80
  134. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +60 -144
  135. imap_processing/ultra/utils/spatial_utils.py +221 -0
  136. {imap_processing-0.7.0.dist-info → imap_processing-0.9.0.dist-info}/METADATA +15 -14
  137. {imap_processing-0.7.0.dist-info → imap_processing-0.9.0.dist-info}/RECORD +142 -139
  138. imap_processing/cdf/cdf_attribute_manager.py +0 -322
  139. imap_processing/cdf/config/shared/default_global_cdf_attrs_schema.yaml +0 -246
  140. imap_processing/cdf/config/shared/default_variable_cdf_attrs_schema.yaml +0 -466
  141. imap_processing/hi/l0/decom_hi.py +0 -24
  142. imap_processing/hi/packet_definitions/hi_packet_definition.xml +0 -482
  143. imap_processing/hit/l0/data_classes/housekeeping.py +0 -240
  144. imap_processing/hit/l0/data_classes/science_packet.py +0 -259
  145. imap_processing/hit/l0/utils/hit_base.py +0 -57
  146. imap_processing/tests/cdf/shared/default_global_cdf_attrs_schema.yaml +0 -246
  147. imap_processing/tests/cdf/shared/default_variable_cdf_attrs_schema.yaml +0 -466
  148. imap_processing/tests/cdf/test_cdf_attribute_manager.py +0 -353
  149. imap_processing/tests/codice/data/imap_codice_l0_hi-counters-aggregated_20240429_v001.pkts +0 -0
  150. imap_processing/tests/codice/data/imap_codice_l0_hi-counters-singles_20240429_v001.pkts +0 -0
  151. imap_processing/tests/codice/data/imap_codice_l0_hi-omni_20240429_v001.pkts +0 -0
  152. imap_processing/tests/codice/data/imap_codice_l0_hi-pha_20240429_v001.pkts +0 -0
  153. imap_processing/tests/codice/data/imap_codice_l0_hi-sectored_20240429_v001.pkts +0 -0
  154. imap_processing/tests/codice/data/imap_codice_l0_hskp_20100101_v001.pkts +0 -0
  155. imap_processing/tests/codice/data/imap_codice_l0_lo-counters-aggregated_20240429_v001.pkts +0 -0
  156. imap_processing/tests/codice/data/imap_codice_l0_lo-counters-singles_20240429_v001.pkts +0 -0
  157. imap_processing/tests/codice/data/imap_codice_l0_lo-nsw-angular_20240429_v001.pkts +0 -0
  158. imap_processing/tests/codice/data/imap_codice_l0_lo-nsw-priority_20240429_v001.pkts +0 -0
  159. imap_processing/tests/codice/data/imap_codice_l0_lo-nsw-species_20240429_v001.pkts +0 -0
  160. imap_processing/tests/codice/data/imap_codice_l0_lo-pha_20240429_v001.pkts +0 -0
  161. imap_processing/tests/codice/data/imap_codice_l0_lo-sw-angular_20240429_v001.pkts +0 -0
  162. imap_processing/tests/codice/data/imap_codice_l0_lo-sw-priority_20240429_v001.pkts +0 -0
  163. imap_processing/tests/codice/data/imap_codice_l0_lo-sw-species_20240429_v001.pkts +0 -0
  164. imap_processing/tests/hi/test_decom.py +0 -55
  165. imap_processing/tests/hi/test_l1a_sci_de.py +0 -72
  166. imap_processing/tests/idex/imap_idex_l0_raw_20230725_v001.pkts +0 -0
  167. imap_processing/tests/mag/imap_mag_l1a_burst-magi_20231025_v001.cdf +0 -0
  168. /imap_processing/{hi/l0/__init__.py → tests/glows/test_glows_l2_data.py} +0 -0
  169. /imap_processing/tests/hit/test_data/{imap_hit_l0_hk_20100105_v001.pkts → imap_hit_l0_raw_20100105_v001.pkts} +0 -0
  170. {imap_processing-0.7.0.dist-info → imap_processing-0.9.0.dist-info}/LICENSE +0 -0
  171. {imap_processing-0.7.0.dist-info → imap_processing-0.9.0.dist-info}/WHEEL +0 -0
  172. {imap_processing-0.7.0.dist-info → imap_processing-0.9.0.dist-info}/entry_points.txt +0 -0
@@ -5,25 +5,50 @@ import pytest
5
5
  import xarray as xr
6
6
 
7
7
  from imap_processing.cdf.utils import load_cdf, write_cdf
8
- from imap_processing.mag.l1b.mag_l1b import mag_l1b, mag_l1b_processing
8
+ from imap_processing.mag.l1b.mag_l1b import (
9
+ calibrate_vector,
10
+ mag_l1b,
11
+ mag_l1b_processing,
12
+ )
9
13
 
10
14
 
11
15
  @pytest.fixture(scope="module")
12
16
  def mag_l1a_dataset():
13
17
  epoch = xr.DataArray(np.arange(20), name="epoch", dims=["epoch"])
14
18
  direction = xr.DataArray(np.arange(4), name="direction", dims=["direction"])
19
+ compression = xr.DataArray(np.arange(2), name="compression", dims=["compression"])
20
+
21
+ direction_label = xr.DataArray(
22
+ direction.values.astype(str),
23
+ name="direction_label",
24
+ dims=["direction_label"],
25
+ )
26
+
27
+ compression_label = xr.DataArray(
28
+ compression.values.astype(str),
29
+ name="compression_label",
30
+ dims=["compression_label"],
31
+ )
32
+
15
33
  vectors = xr.DataArray(
16
34
  np.zeros((20, 4)),
17
35
  dims=["epoch", "direction"],
18
36
  coords={"epoch": epoch, "direction": direction},
19
37
  )
38
+ compression_flags = xr.DataArray(
39
+ np.zeros((20, 2), dtype=np.int8), dims=["epoch", "compression"]
40
+ )
20
41
 
21
42
  vectors[0, :] = np.array([1, 1, 1, 0])
22
43
 
23
44
  output_dataset = xr.Dataset(
24
- coords={"epoch": epoch, "direction": direction},
45
+ coords={"epoch": epoch, "direction": direction, "compression": compression},
25
46
  )
26
47
  output_dataset["vectors"] = vectors
48
+ output_dataset["compression_flags"] = compression_flags
49
+ output_dataset["direction_label"] = direction_label
50
+ output_dataset["compression_label"] = compression_label
51
+ output_dataset.attrs["Logical_source"] = ["imap_mag_l1a_norm-mago"]
27
52
 
28
53
  return output_dataset
29
54
 
@@ -32,9 +57,8 @@ def test_mag_processing(mag_l1a_dataset):
32
57
  mag_l1a_dataset.attrs["Logical_source"] = ["imap_mag_l1a_norm-mago"]
33
58
 
34
59
  mag_l1b = mag_l1b_processing(mag_l1a_dataset)
35
-
36
60
  np.testing.assert_allclose(
37
- mag_l1b["vectors"][0].values, [2.29819857, 2.22914442, 2.24950008, 0]
61
+ mag_l1b["vectors"][0].values, [2.2972, 2.2415, 2.2381, 0], atol=1e-4
38
62
  )
39
63
  np.testing.assert_allclose(mag_l1b["vectors"][1].values, [0, 0, 0, 0])
40
64
 
@@ -45,7 +69,7 @@ def test_mag_processing(mag_l1a_dataset):
45
69
  mag_l1b = mag_l1b_processing(mag_l1a_dataset)
46
70
 
47
71
  np.testing.assert_allclose(
48
- mag_l1b["vectors"][0].values, [2.27615106, 2.22638234, 2.24382211, 0]
72
+ mag_l1b["vectors"][0].values, [2.27538, 2.23416, 2.23682, 0], atol=1e-5
49
73
  )
50
74
  np.testing.assert_allclose(mag_l1b["vectors"][1].values, [0, 0, 0, 0])
51
75
 
@@ -66,13 +90,79 @@ def test_mag_attributes(mag_l1a_dataset):
66
90
  assert output.attrs["Data_level"] == "L1B"
67
91
 
68
92
 
69
- @pytest.mark.skip(reason="Epoch variable data need to be monotonically increasing")
70
93
  def test_cdf_output():
71
94
  l1a_cdf = load_cdf(
72
- Path(__file__).parent / "imap_mag_l1a_burst-magi_20231025_v001.cdf"
95
+ Path(__file__).parent / "imap_mag_l1a_norm-magi_20251017_v001.cdf"
73
96
  )
74
97
  l1b_dataset = mag_l1b(l1a_cdf, "v001")
75
98
 
76
99
  output_path = write_cdf(l1b_dataset)
77
100
 
78
101
  assert Path.exists(output_path)
102
+
103
+
104
+ def test_mag_compression_scale(mag_l1a_dataset):
105
+ test_calibration = np.array(
106
+ [
107
+ [2.2972202, 0.0, 0.0],
108
+ [0.00348625, 2.23802879, 0.0],
109
+ [-0.00250788, -0.00888437, 2.24950008],
110
+ ]
111
+ )
112
+ mag_l1a_dataset["vectors"][0, :] = np.array([1, 1, 1, 0])
113
+ mag_l1a_dataset["vectors"][1, :] = np.array([1, 1, 1, 0])
114
+ mag_l1a_dataset["vectors"][2, :] = np.array([1, 1, 1, 0])
115
+ mag_l1a_dataset["vectors"][3, :] = np.array([1, 1, 1, 0])
116
+
117
+ mag_l1a_dataset["compression_flags"][0, :] = np.array([1, 16], dtype=np.int8)
118
+ mag_l1a_dataset["compression_flags"][1, :] = np.array([0, 0], dtype=np.int8)
119
+ mag_l1a_dataset["compression_flags"][2, :] = np.array([1, 18], dtype=np.int8)
120
+ mag_l1a_dataset["compression_flags"][3, :] = np.array([1, 14], dtype=np.int8)
121
+
122
+ mag_l1a_dataset.attrs["Logical_source"] = ["imap_mag_l1a_norm-mago"]
123
+ output = mag_l1b(mag_l1a_dataset, "v001")
124
+
125
+ calibrated_vectors = np.matmul(test_calibration, np.array([1, 1, 1]))
126
+ # 16 bit width is the standard
127
+ assert np.allclose(output["vectors"].data[0][:3], calibrated_vectors)
128
+ # uncompressed data is uncorrected
129
+ assert np.allclose(output["vectors"].data[1][:3], calibrated_vectors)
130
+
131
+ # width of 18 should be multiplied by 1/4
132
+ scaled_vectors = calibrated_vectors * 1 / 4
133
+ # should be corrected
134
+ assert np.allclose(output["vectors"].data[2][:3], scaled_vectors)
135
+
136
+ # width of 14 should be multiplied by 4
137
+ scaled_vectors = calibrated_vectors * 4
138
+ assert np.allclose(output["vectors"].data[3][:3], scaled_vectors)
139
+
140
+
141
+ def test_calibrate_vector():
142
+ # from MFOTOURFO
143
+ cal_array = np.array(
144
+ [
145
+ [
146
+ [2.29722020e00, 7.38200160e-02, 1.88479865e-02, 4.59777333e-03],
147
+ [0.00000000e00, 0.00000000e00, 0.00000000e00, 0.00000000e00],
148
+ [0.00000000e00, 0.00000000e00, 0.00000000e00, 0.00000000e00],
149
+ ],
150
+ [
151
+ [3.48624576e-03, 1.09224000e-04, 3.26118600e-05, 5.02830000e-06],
152
+ [2.23802879e00, 7.23781440e-02, 1.84842873e-02, 4.50744060e-03],
153
+ [0.00000000e00, 0.00000000e00, 0.00000000e00, 0.00000000e00],
154
+ ],
155
+ [
156
+ [-2.50787532e-03, -8.33760000e-05, -2.71240200e-05, 2.50509000e-06],
157
+ [-8.88437262e-03, -2.84256000e-04, -7.41600000e-05, -2.29399200e-05],
158
+ [2.24950008e00, 7.23836160e-02, 1.84847323e-02, 4.50945192e-03],
159
+ ],
160
+ ]
161
+ )
162
+
163
+ calibration_matrix = xr.DataArray(cal_array)
164
+
165
+ cal_vector = calibrate_vector(np.array([1.0, 1.0, 1.0, 0]), calibration_matrix)
166
+ expected_vector = np.array([2.2972, 2.2415, 2.2381, 0])
167
+
168
+ assert np.allclose(cal_vector, expected_vector, atol=1e-4)
@@ -3,4 +3,6 @@
3
3
  {SPICE_TEST_DATA_PATH}/imap_spk_demo.bsp
4
4
  {SPICE_TEST_DATA_PATH}/sim_1yr_imap_attitude.bc
5
5
  {SPICE_TEST_DATA_PATH}/imap_wkcp.tf
6
- {SPICE_TEST_DATA_PATH}/de440s.bsp
6
+ {SPICE_TEST_DATA_PATH}/de440s.bsp
7
+ {SPICE_TEST_DATA_PATH}/imap_science_0001.tf
8
+ {SPICE_TEST_DATA_PATH}/sim_1yr_imap_pointing_frame.bc
@@ -8,6 +8,8 @@ import spiceypy as spice
8
8
  from imap_processing.spice.geometry import (
9
9
  SpiceBody,
10
10
  SpiceFrame,
11
+ basis_vectors,
12
+ cartesian_to_spherical,
11
13
  frame_transform,
12
14
  get_instrument_spin_phase,
13
15
  get_rotation_matrix,
@@ -16,7 +18,9 @@ from imap_processing.spice.geometry import (
16
18
  get_spin_data,
17
19
  imap_state,
18
20
  instrument_pointing,
21
+ spherical_to_cartesian,
19
22
  )
23
+ from imap_processing.spice.kernels import ensure_spice
20
24
 
21
25
 
22
26
  @pytest.mark.parametrize(
@@ -99,10 +103,10 @@ def test_get_spacecraft_spin_phase_value_error(query_met_times, fake_spin_data):
99
103
  _ = get_spacecraft_spin_phase(query_met_times)
100
104
 
101
105
 
102
- @pytest.mark.usefixtures("_set_spin_data_filepath")
103
- def test_get_spin_data():
106
+ @pytest.mark.usefixtures("use_fake_spin_data_for_time")
107
+ def test_get_spin_data(use_fake_spin_data_for_time):
104
108
  """Test get_spin_data() with generated spin data."""
105
-
109
+ use_fake_spin_data_for_time(453051323.0 - 56120)
106
110
  spin_data = get_spin_data()
107
111
 
108
112
  (
@@ -200,6 +204,19 @@ def test_get_spacecraft_to_instrument_spin_phase_offset(instrument, expected_off
200
204
  SpiceFrame.IMAP_SPACECRAFT,
201
205
  SpiceFrame.IMAP_DPS,
202
206
  ),
207
+ # single et, multiple position vectors
208
+ (
209
+ ["2025-04-30T12:00:00.000"],
210
+ np.array(
211
+ [
212
+ [1, 0, 0],
213
+ [0, 1, 0],
214
+ [0, 0, 1],
215
+ ]
216
+ ),
217
+ SpiceFrame.IMAP_SPACECRAFT,
218
+ SpiceFrame.IMAP_DPS,
219
+ ),
203
220
  ],
204
221
  )
205
222
  def test_frame_transform(et_strings, position, from_frame, to_frame, furnish_kernels):
@@ -219,11 +236,24 @@ def test_frame_transform(et_strings, position, from_frame, to_frame, furnish_ker
219
236
  et = np.array([spice.utc2et(et_str) for et_str in et_strings])
220
237
  et_arg = et[0] if len(et) == 1 else et
221
238
  result = frame_transform(et_arg, position, from_frame, to_frame)
222
- # check the result shape before modifying for value checking
223
- assert result.shape == (3,) if len(et) == 1 else (len(et), 3)
224
- # compare against pure SPICE calculation
225
- position = np.broadcast_to(position, (len(et), 3))
226
- result = np.broadcast_to(result, (len(et), 3))
239
+ # check the result shape before modifying for value checking.
240
+ # There are 3 cases to consider:
241
+
242
+ # 1 event time, multiple position vectors:
243
+ if len(et) == 1 and position.ndim > 1:
244
+ assert result.shape == position.shape
245
+ # multiple event times, single position vector:
246
+ elif len(et) > 1 and position.ndim == 1:
247
+ assert result.shape == (len(et), 3)
248
+ # multiple event times, multiple position vectors (same number of each)
249
+ elif len(et) > 1 and position.ndim > 1:
250
+ assert result.shape == (len(et), 3)
251
+
252
+ # compare against pure SPICE calculation.
253
+ # If the result is a single position vector, broadcast it to first.
254
+ if position.ndim == 1:
255
+ position = np.broadcast_to(position, (len(et), 3))
256
+ result = np.broadcast_to(result, (len(et), 3))
227
257
  for spice_et, spice_position, test_result in zip(et, position, result):
228
258
  rotation_matrix = spice.pxform(from_frame.name, to_frame.name, spice_et)
229
259
  spice_result = spice.mxv(rotation_matrix, spice_position)
@@ -250,7 +280,7 @@ def test_frame_transform_exceptions():
250
280
  match="Mismatch in number of position vectors and Ephemeris times provided.",
251
281
  ):
252
282
  frame_transform(
253
- np.arange(2),
283
+ [1, 2],
254
284
  np.arange(9).reshape((3, 3)),
255
285
  SpiceFrame.ECLIPJ2000,
256
286
  SpiceFrame.IMAP_HIT,
@@ -306,3 +336,79 @@ def test_instrument_pointing(furnish_kernels):
306
336
  et, SpiceFrame.IMAP_HI_90, SpiceFrame.ECLIPJ2000, cartesian=True
307
337
  )
308
338
  assert ins_pointing.shape == (3, 3)
339
+
340
+
341
+ @pytest.mark.external_kernel()
342
+ @pytest.mark.use_test_metakernel("imap_ena_sim_metakernel.template")
343
+ def test_basis_vectors():
344
+ """Test coverage for basis_vectors()."""
345
+ # This call to SPICE needs to be wrapped with `ensure_spice` so that kernels
346
+ # get furnished automatically
347
+ et = ensure_spice(spice.utc2et)("2025-09-30T12:00:00.000")
348
+ # test input of float
349
+ sc_axes = basis_vectors(et, SpiceFrame.IMAP_SPACECRAFT, SpiceFrame.IMAP_SPACECRAFT)
350
+ np.testing.assert_array_equal(sc_axes, np.eye(3))
351
+ # test array of et input
352
+ et_array = np.arange(10) + et
353
+ sc_axes = basis_vectors(et_array, SpiceFrame.IMAP_SPACECRAFT, SpiceFrame.ECLIPJ2000)
354
+ assert sc_axes.shape == (10, 3, 3)
355
+ # Verify that for each time, the basis vectors are correct
356
+ for et, basis_matrix in zip(et_array, sc_axes):
357
+ np.testing.assert_array_equal(
358
+ basis_matrix,
359
+ frame_transform(
360
+ et * np.ones(3),
361
+ np.eye(3),
362
+ SpiceFrame.IMAP_SPACECRAFT,
363
+ SpiceFrame.ECLIPJ2000,
364
+ ),
365
+ )
366
+
367
+
368
+ def test_cartesian_to_spherical():
369
+ """Tests cartesian_to_spherical function."""
370
+
371
+ step = 0.05
372
+ x = np.arange(-1, 1 + step, step)
373
+ y = np.arange(-1, 1 + step, step)
374
+ z = np.arange(-1, 1 + step, step)
375
+ x, y, z = np.meshgrid(x, y, z)
376
+
377
+ cartesian_points = np.stack((x.ravel(), y.ravel(), z.ravel()), axis=-1)
378
+
379
+ for point in cartesian_points:
380
+ r, az, el = cartesian_to_spherical(point)
381
+ r_spice, colat_spice, slong_spice = spice.recsph(point)
382
+
383
+ # Convert SPICE co-latitude to elevation
384
+ el_spice = 90 - np.degrees(colat_spice)
385
+ az_spice = np.degrees(slong_spice)
386
+
387
+ # Normalize azimuth to [0, 360]
388
+ az_spice = az_spice % 360
389
+
390
+ np.testing.assert_allclose(r, r_spice, atol=1e-5)
391
+ np.testing.assert_allclose(az, az_spice, atol=1e-5)
392
+ np.testing.assert_allclose(el, el_spice, atol=1e-5)
393
+
394
+
395
+ def test_spherical_to_cartesian():
396
+ """Tests spherical_to_cartesian function."""
397
+
398
+ azimuth = np.linspace(0, 2 * np.pi, 50)
399
+ elevation = np.linspace(-np.pi / 2, np.pi / 2, 50)
400
+ theta, elev = np.meshgrid(azimuth, elevation)
401
+ r = 1.0
402
+
403
+ spherical_points = np.stack(
404
+ (r * np.ones_like(theta).ravel(), theta.ravel(), elev.ravel()), axis=-1
405
+ )
406
+
407
+ # Convert elevation to colatitude for SPICE
408
+ colat = np.pi / 2 - spherical_points[:, 2]
409
+
410
+ for i in range(len(colat)):
411
+ cartesian_coords = spherical_to_cartesian(np.array([spherical_points[i]]))
412
+ spice_coords = spice.sphrec(r, colat[i], spherical_points[i, 1])
413
+
414
+ np.testing.assert_allclose(cartesian_coords[0], spice_coords, atol=1e-5)
@@ -2,22 +2,41 @@
2
2
 
3
3
  import numpy as np
4
4
  import pytest
5
- import spiceypy as spice
5
+ import spiceypy
6
6
 
7
7
  from imap_processing.spice import IMAP_SC_ID
8
- from imap_processing.spice.time import _sct2e_wrapper, j2000ns_to_j2000s, met_to_j2000ns
8
+ from imap_processing.spice.time import (
9
+ TICK_DURATION,
10
+ _sct2e_wrapper,
11
+ et_to_utc,
12
+ j2000ns_to_j2000s,
13
+ met_to_datetime64,
14
+ met_to_j2000ns,
15
+ met_to_sclkticks,
16
+ met_to_utc,
17
+ str_to_et,
18
+ )
19
+
20
+
21
+ @pytest.mark.parametrize("met", [1, np.arange(10)])
22
+ def test_met_to_sclkticks(met):
23
+ """Test coverage for met_to_sclkticks."""
24
+ # Tick duration is 20us as specified in imap_sclk_0000.tsc
25
+ expected = met * 1 / 20e-6
26
+ ticks = met_to_sclkticks(met)
27
+ np.testing.assert_array_equal(ticks, expected)
9
28
 
10
29
 
11
30
  def test_met_to_j2000ns(furnish_time_kernels):
12
31
  """Test coverage for met_to_j2000ns function."""
13
32
  utc = "2026-01-01T00:00:00.125"
14
- et = spice.str2et(utc)
15
- sclk_str = spice.sce2s(IMAP_SC_ID, et)
33
+ et = spiceypy.str2et(utc)
34
+ sclk_str = spiceypy.sce2s(IMAP_SC_ID, et)
16
35
  seconds, ticks = sclk_str.split("/")[1].split(":")
17
36
  # There is some floating point error calculating tick duration from 1 clock
18
37
  # tick so average over many clock ticks for better accuracy
19
38
  spice_tick_duration = (
20
- spice.sct2e(IMAP_SC_ID, 1e12) - spice.sct2e(IMAP_SC_ID, 0)
39
+ spiceypy.sct2e(IMAP_SC_ID, 1e12) - spiceypy.sct2e(IMAP_SC_ID, 0)
21
40
  ) / 1e12
22
41
  met = float(seconds) + float(ticks) * spice_tick_duration
23
42
  j2000ns = met_to_j2000ns(met)
@@ -30,7 +49,7 @@ def test_j2000ns_to_j2000s(furnish_time_kernels):
30
49
  # Use spice to come up with reasonable J2000 values
31
50
  utc = "2025-09-23T00:00:00.000"
32
51
  # Test single value input
33
- et = spice.str2et(utc)
52
+ et = spiceypy.str2et(utc)
34
53
  epoch = int(et * 1e9)
35
54
  j2000s = j2000ns_to_j2000s(epoch)
36
55
  assert j2000s == et
@@ -42,6 +61,61 @@ def test_j2000ns_to_j2000s(furnish_time_kernels):
42
61
  )
43
62
 
44
63
 
64
+ @pytest.mark.parametrize(
65
+ "expected_utc, precision",
66
+ [
67
+ ("2024-01-01T00:00:00.000", 3),
68
+ (
69
+ [
70
+ "2024-01-01T00:00:00.000555",
71
+ "2025-09-23T00:00:00.000111",
72
+ "2040-11-14T10:23:48.156980",
73
+ ],
74
+ 6,
75
+ ),
76
+ ],
77
+ )
78
+ def test_met_to_utc(furnish_time_kernels, expected_utc, precision):
79
+ """Test coverage for met_to_utc function."""
80
+ if isinstance(expected_utc, list):
81
+ et_arr = spiceypy.str2et(expected_utc)
82
+ sclk_ticks = np.array([spiceypy.sce2c(IMAP_SC_ID, et) for et in et_arr])
83
+ else:
84
+ et = spiceypy.str2et(expected_utc)
85
+ sclk_ticks = spiceypy.sce2c(IMAP_SC_ID, et)
86
+ met = sclk_ticks * TICK_DURATION
87
+ utc = met_to_utc(met, precision=precision)
88
+ np.testing.assert_array_equal(utc, expected_utc)
89
+
90
+
91
+ @pytest.mark.parametrize(
92
+ "utc",
93
+ [
94
+ "2024-01-01T00:00:00.000",
95
+ [
96
+ "2024-01-01T00:00:00.000",
97
+ "2025-09-23T00:00:00.000",
98
+ "2040-11-14T10:23:48.15698",
99
+ ],
100
+ ],
101
+ )
102
+ def test_met_to_datetime64(furnish_time_kernels, utc):
103
+ """Test coverage for met_to_datetime64 function."""
104
+ if isinstance(utc, list):
105
+ expected_dt64 = np.array([np.datetime64(utc_str) for utc_str in utc])
106
+ et_arr = spiceypy.str2et(utc)
107
+ sclk_ticks = np.array([spiceypy.sce2c(IMAP_SC_ID, et) for et in et_arr])
108
+ else:
109
+ expected_dt64 = np.asarray(np.datetime64(utc))
110
+ et = spiceypy.str2et(utc)
111
+ sclk_ticks = spiceypy.sce2c(IMAP_SC_ID, et)
112
+ met = sclk_ticks * TICK_DURATION
113
+ dt64 = met_to_datetime64(met)
114
+ np.testing.assert_array_equal(
115
+ dt64.astype("datetime64[us]"), expected_dt64.astype("datetime64[us]")
116
+ )
117
+
118
+
45
119
  @pytest.mark.parametrize("sclk_ticks", [0.0, np.arange(10)])
46
120
  def test_sct2e_wrapper(sclk_ticks):
47
121
  """Test for `_sct2e_wrapper` function."""
@@ -50,3 +124,58 @@ def test_sct2e_wrapper(sclk_ticks):
50
124
  assert isinstance(et, float)
51
125
  else:
52
126
  assert len(et) == len(sclk_ticks)
127
+
128
+
129
+ def test_str_to_et(furnish_time_kernels):
130
+ """Test coverage for string to et conversion function."""
131
+ utc = "2017-07-14T19:46:00"
132
+ # Test single value input
133
+ expected_et = 553333629.1837274
134
+ actual_et = str_to_et(utc)
135
+ assert expected_et == actual_et
136
+
137
+ # Test list input
138
+ list_of_utc = [
139
+ "2017-08-14T19:46:00.000",
140
+ "2017-09-14T19:46:00.000",
141
+ "2017-10-14T19:46:00.000",
142
+ ]
143
+
144
+ expected_et_array = np.array(
145
+ (556012029.1829445, 558690429.1824446, 561282429.1823651)
146
+ )
147
+ actual_et_array = str_to_et(list_of_utc)
148
+ assert np.array_equal(expected_et_array, actual_et_array)
149
+
150
+ # Test array input
151
+ array_of_utc = np.array(
152
+ [
153
+ "2017-08-14T19:46:00.000",
154
+ "2017-09-14T19:46:00.000",
155
+ "2017-10-14T19:46:00.000",
156
+ ]
157
+ )
158
+
159
+ actual_et_array = str_to_et(array_of_utc)
160
+ assert np.array_equal(expected_et_array, actual_et_array)
161
+
162
+
163
+ def test_et_to_utc(furnish_time_kernels):
164
+ """Test coverage for et to utc conversion function."""
165
+ et = 553333629.1837274
166
+ # Test single value input
167
+ expected_utc = "2017-07-14T19:46:00.000"
168
+ actual_utc = et_to_utc(et)
169
+ assert expected_utc == actual_utc
170
+
171
+ # Test array input
172
+ array_of_et = np.array((556012029.1829445, 558690429.1824446, 561282429.1823651))
173
+ expected_utc_array = np.array(
174
+ (
175
+ "2017-08-14T19:46:00.000",
176
+ "2017-09-14T19:46:00.000",
177
+ "2017-10-14T19:46:00.000",
178
+ )
179
+ )
180
+ actual_utc_array = et_to_utc(array_of_et)
181
+ assert np.array_equal(expected_utc_array, actual_utc_array)