imap-processing 0.12.0__py3-none-any.whl → 0.13.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 (272) hide show
  1. imap_processing/__init__.py +1 -0
  2. imap_processing/_version.py +2 -2
  3. imap_processing/ccsds/ccsds_data.py +1 -2
  4. imap_processing/ccsds/excel_to_xtce.py +1 -2
  5. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +18 -12
  6. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +569 -0
  7. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +1846 -128
  8. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +5 -5
  9. imap_processing/cdf/config/imap_idex_global_cdf_attrs.yaml +20 -1
  10. imap_processing/cdf/config/imap_idex_l1a_variable_attrs.yaml +6 -4
  11. imap_processing/cdf/config/imap_idex_l1b_variable_attrs.yaml +3 -3
  12. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +15 -0
  13. imap_processing/cdf/config/imap_swapi_variable_attrs.yaml +22 -0
  14. imap_processing/cdf/config/imap_swe_l1b_variable_attrs.yaml +16 -0
  15. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +178 -5
  16. imap_processing/cdf/config/imap_ultra_l1a_variable_attrs.yaml +5045 -41
  17. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +33 -19
  18. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +8 -48
  19. imap_processing/cdf/utils.py +41 -33
  20. imap_processing/cli.py +463 -234
  21. imap_processing/codice/codice_l1a.py +260 -47
  22. imap_processing/codice/codice_l1b.py +51 -152
  23. imap_processing/codice/constants.py +38 -1
  24. imap_processing/ena_maps/ena_maps.py +658 -65
  25. imap_processing/ena_maps/utils/coordinates.py +1 -1
  26. imap_processing/ena_maps/utils/spatial_utils.py +10 -5
  27. imap_processing/glows/l1a/glows_l1a.py +28 -99
  28. imap_processing/glows/l1a/glows_l1a_data.py +2 -2
  29. imap_processing/glows/l1b/glows_l1b.py +1 -4
  30. imap_processing/glows/l1b/glows_l1b_data.py +1 -3
  31. imap_processing/glows/l2/glows_l2.py +2 -5
  32. imap_processing/hi/l1a/hi_l1a.py +31 -12
  33. imap_processing/hi/l1b/hi_l1b.py +80 -43
  34. imap_processing/hi/l1c/hi_l1c.py +12 -16
  35. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-sector-dt0-factors_20250219_v002.csv +81 -0
  36. imap_processing/hit/hit_utils.py +93 -35
  37. imap_processing/hit/l0/decom_hit.py +3 -1
  38. imap_processing/hit/l1a/hit_l1a.py +30 -25
  39. imap_processing/hit/l1b/constants.py +6 -2
  40. imap_processing/hit/l1b/hit_l1b.py +279 -318
  41. imap_processing/hit/l2/constants.py +37 -0
  42. imap_processing/hit/l2/hit_l2.py +373 -264
  43. imap_processing/ialirt/l0/parse_mag.py +138 -10
  44. imap_processing/ialirt/l0/process_swapi.py +69 -0
  45. imap_processing/ialirt/l0/process_swe.py +318 -22
  46. imap_processing/ialirt/packet_definitions/ialirt.xml +216 -212
  47. imap_processing/ialirt/packet_definitions/ialirt_codicehi.xml +1 -1
  48. imap_processing/ialirt/packet_definitions/ialirt_codicelo.xml +1 -1
  49. imap_processing/ialirt/packet_definitions/ialirt_swapi.xml +14 -14
  50. imap_processing/ialirt/utils/grouping.py +1 -1
  51. imap_processing/idex/idex_constants.py +9 -1
  52. imap_processing/idex/idex_l0.py +22 -8
  53. imap_processing/idex/idex_l1a.py +75 -44
  54. imap_processing/idex/idex_l1b.py +9 -8
  55. imap_processing/idex/idex_l2a.py +79 -45
  56. imap_processing/idex/idex_l2b.py +120 -0
  57. imap_processing/idex/idex_variable_unpacking_and_eu_conversion.csv +33 -39
  58. imap_processing/idex/packet_definitions/idex_housekeeping_packet_definition.xml +9130 -0
  59. imap_processing/lo/l0/lo_science.py +1 -2
  60. imap_processing/lo/l1a/lo_l1a.py +1 -4
  61. imap_processing/lo/l1b/lo_l1b.py +527 -6
  62. imap_processing/lo/l1b/tof_conversions.py +11 -0
  63. imap_processing/lo/l1c/lo_l1c.py +1 -4
  64. imap_processing/mag/constants.py +43 -0
  65. imap_processing/mag/imap_mag_sdc_configuration_v001.py +8 -0
  66. imap_processing/mag/l1a/mag_l1a.py +2 -9
  67. imap_processing/mag/l1a/mag_l1a_data.py +10 -10
  68. imap_processing/mag/l1b/mag_l1b.py +84 -17
  69. imap_processing/mag/l1c/interpolation_methods.py +180 -3
  70. imap_processing/mag/l1c/mag_l1c.py +236 -70
  71. imap_processing/mag/l2/mag_l2.py +140 -0
  72. imap_processing/mag/l2/mag_l2_data.py +288 -0
  73. imap_processing/spacecraft/quaternions.py +1 -3
  74. imap_processing/spice/geometry.py +3 -3
  75. imap_processing/spice/kernels.py +0 -276
  76. imap_processing/spice/pointing_frame.py +257 -0
  77. imap_processing/spice/repoint.py +48 -19
  78. imap_processing/spice/spin.py +38 -33
  79. imap_processing/spice/time.py +24 -0
  80. imap_processing/swapi/l1/swapi_l1.py +16 -12
  81. imap_processing/swapi/l2/swapi_l2.py +116 -4
  82. imap_processing/swapi/swapi_utils.py +32 -0
  83. imap_processing/swe/l1a/swe_l1a.py +2 -9
  84. imap_processing/swe/l1a/swe_science.py +8 -11
  85. imap_processing/swe/l1b/swe_l1b.py +898 -23
  86. imap_processing/swe/l2/swe_l2.py +21 -77
  87. imap_processing/swe/utils/swe_constants.py +1 -0
  88. imap_processing/tests/ccsds/test_excel_to_xtce.py +1 -1
  89. imap_processing/tests/cdf/test_utils.py +14 -16
  90. imap_processing/tests/codice/conftest.py +44 -33
  91. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-pha_20241110193700_v0.0.0.cdf +0 -0
  92. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-pha_20241110193700_v0.0.0.cdf +0 -0
  93. imap_processing/tests/codice/test_codice_l1a.py +20 -11
  94. imap_processing/tests/codice/test_codice_l1b.py +6 -7
  95. imap_processing/tests/conftest.py +78 -22
  96. imap_processing/tests/ena_maps/test_ena_maps.py +462 -33
  97. imap_processing/tests/ena_maps/test_spatial_utils.py +1 -1
  98. imap_processing/tests/glows/conftest.py +10 -14
  99. imap_processing/tests/glows/test_glows_decom.py +4 -4
  100. imap_processing/tests/glows/test_glows_l1a_cdf.py +6 -27
  101. imap_processing/tests/glows/test_glows_l1a_data.py +6 -8
  102. imap_processing/tests/glows/test_glows_l1b.py +11 -11
  103. imap_processing/tests/glows/test_glows_l1b_data.py +5 -5
  104. imap_processing/tests/glows/test_glows_l2.py +2 -8
  105. imap_processing/tests/hi/conftest.py +1 -1
  106. imap_processing/tests/hi/test_hi_l1b.py +10 -12
  107. imap_processing/tests/hi/test_hi_l1c.py +27 -24
  108. imap_processing/tests/hi/test_l1a.py +7 -9
  109. imap_processing/tests/hi/test_science_direct_event.py +2 -2
  110. imap_processing/tests/hit/helpers/l1_validation.py +44 -43
  111. imap_processing/tests/hit/test_decom_hit.py +1 -1
  112. imap_processing/tests/hit/test_hit_l1a.py +9 -9
  113. imap_processing/tests/hit/test_hit_l1b.py +172 -217
  114. imap_processing/tests/hit/test_hit_l2.py +380 -118
  115. imap_processing/tests/hit/test_hit_utils.py +122 -55
  116. imap_processing/tests/hit/validation_data/hit_l1b_standard_sample2_nsrl_v4_3decimals.csv +62 -62
  117. imap_processing/tests/hit/validation_data/sci_sample_raw.csv +1 -1
  118. imap_processing/tests/ialirt/unit/test_decom_ialirt.py +16 -81
  119. imap_processing/tests/ialirt/unit/test_grouping.py +2 -2
  120. imap_processing/tests/ialirt/unit/test_parse_mag.py +71 -16
  121. imap_processing/tests/ialirt/unit/test_process_codicehi.py +3 -3
  122. imap_processing/tests/ialirt/unit/test_process_codicelo.py +3 -10
  123. imap_processing/tests/ialirt/unit/test_process_ephemeris.py +4 -4
  124. imap_processing/tests/ialirt/unit/test_process_hit.py +3 -3
  125. imap_processing/tests/ialirt/unit/test_process_swapi.py +24 -16
  126. imap_processing/tests/ialirt/unit/test_process_swe.py +115 -7
  127. imap_processing/tests/idex/conftest.py +72 -7
  128. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20241206_v001.pkts +0 -0
  129. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20250108_v001.pkts +0 -0
  130. imap_processing/tests/idex/test_idex_l0.py +33 -11
  131. imap_processing/tests/idex/test_idex_l1a.py +50 -23
  132. imap_processing/tests/idex/test_idex_l1b.py +104 -25
  133. imap_processing/tests/idex/test_idex_l2a.py +48 -32
  134. imap_processing/tests/idex/test_idex_l2b.py +93 -0
  135. imap_processing/tests/lo/test_lo_l1a.py +3 -3
  136. imap_processing/tests/lo/test_lo_l1b.py +371 -6
  137. imap_processing/tests/lo/test_lo_l1c.py +1 -1
  138. imap_processing/tests/lo/test_lo_science.py +6 -7
  139. imap_processing/tests/lo/test_star_sensor.py +1 -1
  140. imap_processing/tests/mag/conftest.py +58 -9
  141. imap_processing/tests/mag/test_mag_decom.py +4 -3
  142. imap_processing/tests/mag/test_mag_l1a.py +13 -7
  143. imap_processing/tests/mag/test_mag_l1b.py +9 -9
  144. imap_processing/tests/mag/test_mag_l1c.py +151 -47
  145. imap_processing/tests/mag/test_mag_l2.py +130 -0
  146. imap_processing/tests/mag/test_mag_validation.py +144 -7
  147. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-magi-normal-in.csv +1217 -0
  148. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-magi-normal-out.csv +1857 -0
  149. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-mago-normal-in.csv +1217 -0
  150. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-mago-normal-out.csv +1857 -0
  151. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-magi-normal-in.csv +1217 -0
  152. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-magi-normal-out.csv +1793 -0
  153. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-mago-normal-in.csv +1217 -0
  154. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-mago-normal-out.csv +1793 -0
  155. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-burst-in.csv +2561 -0
  156. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-normal-in.csv +961 -0
  157. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-normal-out.csv +1539 -0
  158. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-mago-normal-in.csv +1921 -0
  159. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-mago-normal-out.csv +2499 -0
  160. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-magi-normal-in.csv +865 -0
  161. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-magi-normal-out.csv +1196 -0
  162. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-mago-normal-in.csv +1729 -0
  163. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-mago-normal-out.csv +3053 -0
  164. imap_processing/tests/mag/validation/L2/imap_mag_l1b_norm-mago_20251017_v002.cdf +0 -0
  165. imap_processing/tests/mag/validation/calibration/imap_mag_l2-calibration-matrices_20251017_v004.cdf +0 -0
  166. imap_processing/tests/mag/validation/calibration/imap_mag_l2-offsets-norm_20251017_20251017_v001.cdf +0 -0
  167. imap_processing/tests/spacecraft/test_quaternions.py +1 -1
  168. imap_processing/tests/spice/test_data/fake_repoint_data.csv +4 -4
  169. imap_processing/tests/spice/test_data/fake_spin_data.csv +11 -11
  170. imap_processing/tests/spice/test_geometry.py +3 -3
  171. imap_processing/tests/spice/test_kernels.py +1 -200
  172. imap_processing/tests/spice/test_pointing_frame.py +185 -0
  173. imap_processing/tests/spice/test_repoint.py +20 -10
  174. imap_processing/tests/spice/test_spin.py +50 -9
  175. imap_processing/tests/spice/test_time.py +14 -0
  176. imap_processing/tests/swapi/lut/imap_swapi_esa-unit-conversion_20250211_v000.csv +73 -0
  177. imap_processing/tests/swapi/lut/imap_swapi_lut-notes_20250211_v000.csv +1025 -0
  178. imap_processing/tests/swapi/test_swapi_l1.py +7 -9
  179. imap_processing/tests/swapi/test_swapi_l2.py +180 -8
  180. imap_processing/tests/swe/lut/checker-board-indices.csv +24 -0
  181. imap_processing/tests/swe/lut/imap_swe_esa-lut_20250301_v000.csv +385 -0
  182. imap_processing/tests/swe/lut/imap_swe_l1b-in-flight-cal_20240510_20260716_v000.csv +3 -0
  183. imap_processing/tests/swe/test_swe_l1a.py +6 -6
  184. imap_processing/tests/swe/test_swe_l1a_science.py +3 -3
  185. imap_processing/tests/swe/test_swe_l1b.py +162 -24
  186. imap_processing/tests/swe/test_swe_l2.py +82 -102
  187. imap_processing/tests/test_cli.py +171 -88
  188. imap_processing/tests/test_utils.py +2 -1
  189. imap_processing/tests/ultra/data/mock_data.py +49 -21
  190. imap_processing/tests/ultra/unit/conftest.py +53 -70
  191. imap_processing/tests/ultra/unit/test_badtimes.py +2 -4
  192. imap_processing/tests/ultra/unit/test_cullingmask.py +4 -6
  193. imap_processing/tests/ultra/unit/test_de.py +3 -10
  194. imap_processing/tests/ultra/unit/test_decom_apid_880.py +27 -76
  195. imap_processing/tests/ultra/unit/test_decom_apid_881.py +15 -16
  196. imap_processing/tests/ultra/unit/test_decom_apid_883.py +12 -10
  197. imap_processing/tests/ultra/unit/test_decom_apid_896.py +202 -55
  198. imap_processing/tests/ultra/unit/test_lookup_utils.py +23 -1
  199. imap_processing/tests/ultra/unit/test_spacecraft_pset.py +3 -4
  200. imap_processing/tests/ultra/unit/test_ultra_l1a.py +84 -307
  201. imap_processing/tests/ultra/unit/test_ultra_l1b.py +30 -12
  202. imap_processing/tests/ultra/unit/test_ultra_l1b_annotated.py +2 -2
  203. imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py +4 -1
  204. imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +163 -29
  205. imap_processing/tests/ultra/unit/test_ultra_l1c.py +5 -5
  206. imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +32 -43
  207. imap_processing/tests/ultra/unit/test_ultra_l2.py +230 -0
  208. imap_processing/ultra/constants.py +1 -1
  209. imap_processing/ultra/l0/decom_tools.py +21 -34
  210. imap_processing/ultra/l0/decom_ultra.py +168 -204
  211. imap_processing/ultra/l0/ultra_utils.py +152 -136
  212. imap_processing/ultra/l1a/ultra_l1a.py +55 -243
  213. imap_processing/ultra/l1b/badtimes.py +1 -4
  214. imap_processing/ultra/l1b/cullingmask.py +2 -6
  215. imap_processing/ultra/l1b/de.py +62 -47
  216. imap_processing/ultra/l1b/extendedspin.py +8 -4
  217. imap_processing/ultra/l1b/lookup_utils.py +72 -9
  218. imap_processing/ultra/l1b/ultra_l1b.py +3 -8
  219. imap_processing/ultra/l1b/ultra_l1b_culling.py +4 -4
  220. imap_processing/ultra/l1b/ultra_l1b_extended.py +236 -78
  221. imap_processing/ultra/l1c/histogram.py +2 -6
  222. imap_processing/ultra/l1c/spacecraft_pset.py +2 -4
  223. imap_processing/ultra/l1c/ultra_l1c.py +1 -5
  224. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +107 -60
  225. imap_processing/ultra/l2/ultra_l2.py +299 -0
  226. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +526 -0
  227. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +526 -0
  228. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +526 -0
  229. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +526 -0
  230. imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +2 -2
  231. imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +2 -0
  232. imap_processing/ultra/packet_definitions/README.md +38 -0
  233. imap_processing/ultra/packet_definitions/ULTRA_SCI_COMBINED.xml +15302 -482
  234. imap_processing/ultra/utils/ultra_l1_utils.py +13 -12
  235. imap_processing/utils.py +1 -1
  236. {imap_processing-0.12.0.dist-info → imap_processing-0.13.0.dist-info}/METADATA +3 -2
  237. {imap_processing-0.12.0.dist-info → imap_processing-0.13.0.dist-info}/RECORD +264 -225
  238. imap_processing/hi/l1b/hi_eng_unit_convert_table.csv +0 -154
  239. imap_processing/mag/imap_mag_sdc-configuration_v001.yaml +0 -6
  240. imap_processing/mag/l1b/__init__.py +0 -0
  241. imap_processing/swe/l1b/swe_esa_lookup_table.csv +0 -1441
  242. imap_processing/swe/l1b/swe_l1b_science.py +0 -699
  243. imap_processing/tests/swe/test_swe_l1b_science.py +0 -103
  244. imap_processing/ultra/lookup_tables/dps_sensitivity45.cdf +0 -0
  245. imap_processing/ultra/lookup_tables/ultra_90_dps_exposure_compressed.cdf +0 -0
  246. /imap_processing/idex/packet_definitions/{idex_packet_definition.xml → idex_science_packet_definition.xml} +0 -0
  247. /imap_processing/tests/ialirt/{test_data → data}/l0/20240827095047_SWE_IALIRT_packet.bin +0 -0
  248. /imap_processing/tests/ialirt/{test_data → data}/l0/461971383-404.bin +0 -0
  249. /imap_processing/tests/ialirt/{test_data → data}/l0/461971384-405.bin +0 -0
  250. /imap_processing/tests/ialirt/{test_data → data}/l0/461971385-406.bin +0 -0
  251. /imap_processing/tests/ialirt/{test_data → data}/l0/461971386-407.bin +0 -0
  252. /imap_processing/tests/ialirt/{test_data → data}/l0/461971387-408.bin +0 -0
  253. /imap_processing/tests/ialirt/{test_data → data}/l0/461971388-409.bin +0 -0
  254. /imap_processing/tests/ialirt/{test_data → data}/l0/461971389-410.bin +0 -0
  255. /imap_processing/tests/ialirt/{test_data → data}/l0/461971390-411.bin +0 -0
  256. /imap_processing/tests/ialirt/{test_data → data}/l0/461971391-412.bin +0 -0
  257. /imap_processing/tests/ialirt/{test_data → data}/l0/BinLog CCSDS_FRAG_TLM_20240826_152323Z_IALIRT_data_for_SDC.bin +0 -0
  258. /imap_processing/tests/ialirt/{test_data → data}/l0/IALiRT Raw Packet Telemetry.txt +0 -0
  259. /imap_processing/tests/ialirt/{test_data → data}/l0/apid01152.tlm +0 -0
  260. /imap_processing/tests/ialirt/{test_data → data}/l0/eu_SWP_IAL_20240826_152033.csv +0 -0
  261. /imap_processing/tests/ialirt/{test_data → data}/l0/hi_fsw_view_1_ccsds.bin +0 -0
  262. /imap_processing/tests/ialirt/{test_data → data}/l0/hit_ialirt_sample.ccsds +0 -0
  263. /imap_processing/tests/ialirt/{test_data → data}/l0/hit_ialirt_sample.csv +0 -0
  264. /imap_processing/tests/ialirt/{test_data → data}/l0/idle_export_eu.SWE_IALIRT_20240827_093852.csv +0 -0
  265. /imap_processing/tests/ialirt/{test_data → data}/l0/imap_codice_l1a_hi-ialirt_20240523200000_v0.0.0.cdf +0 -0
  266. /imap_processing/tests/ialirt/{test_data → data}/l0/imap_codice_l1a_lo-ialirt_20241110193700_v0.0.0.cdf +0 -0
  267. /imap_processing/tests/ialirt/{test_data → data}/l0/sample_decoded_i-alirt_data.csv +0 -0
  268. /imap_processing/tests/mag/validation/{imap_calibration_mag_20240229_v01.cdf → calibration/imap_mag_l1b-calibration_20240229_v001.cdf} +0 -0
  269. /imap_processing/{swe/l1b/engineering_unit_convert_table.csv → tests/swe/lut/imap_swe_eu-conversion_20240510_v000.csv} +0 -0
  270. {imap_processing-0.12.0.dist-info → imap_processing-0.13.0.dist-info}/LICENSE +0 -0
  271. {imap_processing-0.12.0.dist-info → imap_processing-0.13.0.dist-info}/WHEEL +0 -0
  272. {imap_processing-0.12.0.dist-info → imap_processing-0.13.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,288 @@
1
+ """Data structures for MAG L2 and L1D processing."""
2
+
3
+ from dataclasses import InitVar, dataclass, field
4
+ from enum import Enum
5
+
6
+ import numpy as np
7
+ import xarray as xr
8
+
9
+ from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
10
+ from imap_processing.mag.constants import DataMode
11
+
12
+
13
+ class ValidFrames(Enum):
14
+ """SPICE reference frames for output."""
15
+
16
+ dsrf = "dsrf"
17
+ srf = "srf"
18
+ rtn = "rtn"
19
+ gse = "gse"
20
+
21
+
22
+ @dataclass
23
+ class MagL2:
24
+ """
25
+ Dataclass for MAG L2 data.
26
+
27
+ Since L2 and L1D should have the same structure, this can be used for either level.
28
+
29
+ Some of the methods are also static, so they can be used in i-ALiRT processing.
30
+
31
+ Attributes
32
+ ----------
33
+ vectors: np.ndarray
34
+ Magnetic field vectors of size (n, 3) where n is the number of vectors.
35
+ Describes (x, y, z) components of the magnetic field.
36
+ epoch: np.ndarray
37
+ Time of each vector in J2000 seconds. Should be of length n.
38
+ range: np.ndarray
39
+ Range of each vector. Should be of length n.
40
+ global_attributes: dict
41
+ Any global attributes we want to carry forward into the output CDF file.
42
+ quality_flags: np.ndarray
43
+ Quality flags for each vector. Should be of length n.
44
+ quality_bitmask: np.ndarray
45
+ Quality bitmask for each vector. Should be of length n. Copied from offset
46
+ file in L2, marked as good always in L1D.
47
+ magnitude: np.ndarray
48
+ Magnitude of each vector. Should be of length n. Calculated from L2 vectors.
49
+ is_l1d: bool
50
+ Flag to indicate if the data is L1D. Defaults to False.
51
+ """
52
+
53
+ vectors: np.ndarray
54
+ epoch: np.ndarray
55
+ range: np.ndarray
56
+ global_attributes: dict
57
+ quality_flags: np.ndarray
58
+ quality_bitmask: np.ndarray
59
+ data_mode: DataMode
60
+ magnitude: np.ndarray = field(init=False)
61
+ is_l1d: bool = False
62
+ offsets: InitVar[np.ndarray] = None
63
+ timedelta: InitVar[np.ndarray] = None
64
+
65
+ def __post_init__(self, offsets: np.ndarray, timedelta: np.ndarray) -> None:
66
+ """
67
+ Calculate the magnitude of the vectors after initialization.
68
+
69
+ Parameters
70
+ ----------
71
+ offsets : np.ndarray
72
+ Offsets to apply to the vectors. Should be of shape (n, 3) where n is the
73
+ number of vectors.
74
+ timedelta : np.ndarray
75
+ Time deltas to shift the timestamps by. Should be of length n.
76
+ Given in seconds.
77
+ """
78
+ if offsets is not None:
79
+ self.vectors = self.apply_offsets(self.vectors, offsets)
80
+ if timedelta is not None:
81
+ self.epoch = self.shift_timestamps(self.epoch, timedelta)
82
+
83
+ self.magnitude = self.calculate_magnitude(self.vectors)
84
+
85
+ @staticmethod
86
+ def calculate_magnitude(
87
+ vectors: np.ndarray,
88
+ ) -> np.ndarray:
89
+ """
90
+ Given a list of vectors (x, y, z), calculate the magnitude of each vector.
91
+
92
+ For an input list of vectors of size (n, 3) returns a list of magnitudes of
93
+ size (n,).
94
+
95
+ Parameters
96
+ ----------
97
+ vectors : np.ndarray
98
+ Array of vectors to calculate the magnitude of.
99
+
100
+ Returns
101
+ -------
102
+ np.ndarray
103
+ Array of magnitudes of the input vectors.
104
+ """
105
+ return np.zeros(vectors.shape[0]) # type: ignore
106
+
107
+ @staticmethod
108
+ def apply_offsets(vectors: np.ndarray, offsets: np.ndarray) -> np.ndarray:
109
+ """
110
+ Apply the offsets to the vectors by adding them together.
111
+
112
+ These offsets are used to shift the vectors in the x, y, and z directions.
113
+ They can either be provided through a custom offsets datafile, or calculated
114
+ using a gradiometry algorithm.
115
+
116
+ Parameters
117
+ ----------
118
+ vectors : np.ndarray
119
+ Array of vectors to apply the offsets to. Should be of shape (n, 3) where n
120
+ is the number of vectors.
121
+ offsets : np.ndarray
122
+ Array of offsets to apply to the vectors. Should be of shape (n, 3) where n
123
+ is the number of vectors.
124
+
125
+ Returns
126
+ -------
127
+ np.ndarray
128
+ Array of vectors with offsets applied. Should be of shape (n, 3).
129
+ """
130
+ if vectors.shape[0] != offsets.shape[0]:
131
+ raise ValueError("Vectors and offsets must have the same length.")
132
+
133
+ offset_vectors: np.ndarray = vectors[:, :3] + offsets
134
+
135
+ # TODO: CDF files don't have NaNs. Emailed MAG to ask what this will look like.
136
+ # Any values where offsets is nan must also be nan
137
+ offset_vectors[np.isnan(offsets).any(axis=1)] = np.nan
138
+
139
+ return offset_vectors
140
+
141
+ @staticmethod
142
+ def shift_timestamps(epoch: np.ndarray, timedelta: np.ndarray) -> np.ndarray:
143
+ """
144
+ Shift the timestamps by the given timedelta.
145
+
146
+ If timedelta is positive, the epochs are shifted forward in time.
147
+
148
+ Parameters
149
+ ----------
150
+ epoch : np.ndarray
151
+ Array of timestamps to shift. Should be of length n.
152
+ timedelta : np.ndarray
153
+ Array of time deltas to shift the timestamps by. Should be the same length
154
+ as epoch. Given in seconds.
155
+
156
+ Returns
157
+ -------
158
+ np.ndarray
159
+ Shifted timestamps.
160
+ """
161
+ if epoch.shape[0] != timedelta.shape[0]:
162
+ raise ValueError(
163
+ "Input Epoch and offsets timedeltas must be the same length."
164
+ )
165
+
166
+ timedelta_ns = timedelta * 1e9
167
+ shifted_timestamps = epoch + timedelta_ns
168
+ return shifted_timestamps
169
+
170
+ def truncate_to_24h(self, timestamp: str) -> None:
171
+ """
172
+ Truncate all data to a 24 hour period.
173
+
174
+ 24 hours is given by timestamp in the format YYYYmmdd.
175
+
176
+ Parameters
177
+ ----------
178
+ timestamp : str
179
+ Timestamp in the format YYYYMMDD.
180
+ """
181
+ pass
182
+
183
+ def generate_dataset(
184
+ self,
185
+ attribute_manager: ImapCdfAttributes,
186
+ frame: ValidFrames = ValidFrames.dsrf,
187
+ ) -> xr.Dataset:
188
+ """
189
+ Generate an xarray dataset from the dataclass.
190
+
191
+ This method can be used for L2 and L1D, since they have extremely similar
192
+ output.
193
+
194
+ Parameters
195
+ ----------
196
+ attribute_manager : ImapCdfAttributes
197
+ CDF attributes object for the correct level.
198
+ frame : ValidFrames
199
+ SPICE reference frame to rotate the data into.
200
+
201
+ Returns
202
+ -------
203
+ xr.Dataset
204
+ Complete dataset ready to write to CDF file.
205
+ """
206
+ logical_source_id = f"imap_mag_l2_{self.data_mode.value.lower()}-{frame.name}"
207
+ direction = xr.DataArray(
208
+ np.arange(3),
209
+ name="direction",
210
+ dims=["direction"],
211
+ attrs=attribute_manager.get_variable_attributes(
212
+ "direction_attrs", check_schema=False
213
+ ),
214
+ )
215
+
216
+ direction_label = xr.DataArray(
217
+ direction.values.astype(str),
218
+ name="direction_label",
219
+ dims=["direction_label"],
220
+ attrs=attribute_manager.get_variable_attributes(
221
+ "direction_label", check_schema=False
222
+ ),
223
+ )
224
+
225
+ epoch_time = xr.DataArray(
226
+ self.epoch,
227
+ name="epoch",
228
+ dims=["epoch"],
229
+ attrs=attribute_manager.get_variable_attributes("epoch"),
230
+ )
231
+
232
+ vectors = xr.DataArray(
233
+ self.vectors,
234
+ name="vectors",
235
+ dims=["epoch", "direction"],
236
+ attrs=attribute_manager.get_variable_attributes("vector_attrs"),
237
+ )
238
+
239
+ quality_flags = xr.DataArray(
240
+ self.quality_flags,
241
+ name="quality_flags",
242
+ dims=["epoch"],
243
+ attrs=attribute_manager.get_variable_attributes("compression"),
244
+ )
245
+
246
+ quality_bitmask = xr.DataArray(
247
+ self.quality_flags,
248
+ name="quality_flags",
249
+ dims=["epoch"],
250
+ attrs=attribute_manager.get_variable_attributes("compression"),
251
+ )
252
+
253
+ rng = xr.DataArray(
254
+ self.range,
255
+ name="range",
256
+ dims=["epoch"],
257
+ # TODO temp attrs
258
+ attrs=attribute_manager.get_variable_attributes("compression_width"),
259
+ )
260
+
261
+ magnitude = xr.DataArray(
262
+ self.magnitude,
263
+ name="magnitude",
264
+ dims=["epoch"],
265
+ attrs=attribute_manager.get_variable_attributes("compression_width"),
266
+ )
267
+
268
+ global_attributes = (
269
+ attribute_manager.get_global_attributes(logical_source_id)
270
+ | self.global_attributes
271
+ )
272
+
273
+ output = xr.Dataset(
274
+ coords={
275
+ "epoch": epoch_time,
276
+ "direction": direction,
277
+ "direction_label": direction_label,
278
+ },
279
+ attrs=global_attributes,
280
+ )
281
+
282
+ output["vectors"] = vectors
283
+ output["quality_flags"] = quality_flags
284
+ output["quality_bitmask"] = quality_bitmask
285
+ output["range"] = rng
286
+ output["magnitude"] = magnitude
287
+
288
+ return output
@@ -66,7 +66,7 @@ def assemble_quaternions(ds: xr.Dataset) -> xr.Dataset:
66
66
  base_name = "FSW_ACS_QUAT_10_HZ_BUFFERED".lower()
67
67
  for quat_i, label in enumerate(["x", "y", "z", "s"]):
68
68
  # 0, 1, 2, .. 9 // 10, 11, 12, .. 19 // 20, 21, 22, .. 29 // 30, 31, 32, .. 39
69
- names = [f"{base_name}_{i + quat_i*10}" for i in range(10)]
69
+ names = [f"{base_name}_{i + quat_i * 10}" for i in range(10)]
70
70
  quat = np.stack([ds[name] for name in names], axis=1).ravel()
71
71
  output_ds[f"quat_{label}"] = ("epoch", quat)
72
72
  return output_ds
@@ -100,8 +100,6 @@ def process_quaternions(packet_file: Path | str) -> tuple[xr.Dataset, xr.Dataset
100
100
  # Update dataset global attributes
101
101
  attr_mgr = ImapCdfAttributes()
102
102
  attr_mgr.add_instrument_global_attrs("spacecraft")
103
- # TODO: Allow version to be passed in
104
- attr_mgr.add_global_attribute("Data_version", 1)
105
103
  attr_mgr.add_instrument_variable_attrs(instrument="spacecraft", level=None)
106
104
 
107
105
  l1a_ds.attrs.update(
@@ -63,7 +63,7 @@ class SpiceFrame(IntEnum):
63
63
 
64
64
 
65
65
  BORESIGHT_LOOKUP = {
66
- SpiceFrame.IMAP_LO: np.array([0, -1, 0]),
66
+ SpiceFrame.IMAP_LO_BASE: np.array([0, -1, 0]),
67
67
  SpiceFrame.IMAP_HI_45: np.array([0, 1, 0]),
68
68
  SpiceFrame.IMAP_HI_90: np.array([0, 1, 0]),
69
69
  SpiceFrame.IMAP_ULTRA_45: np.array([0, 0, 1]),
@@ -136,7 +136,7 @@ def get_spacecraft_to_instrument_spin_phase_offset(instrument: SpiceFrame) -> fl
136
136
  """
137
137
  # TODO: Implement retrieval from SPICE?
138
138
  offset_lookup = {
139
- SpiceFrame.IMAP_LO: 330 / 360,
139
+ SpiceFrame.IMAP_LO_BASE: 330 / 360,
140
140
  SpiceFrame.IMAP_HI_45: 255 / 360,
141
141
  SpiceFrame.IMAP_HI_90: 285 / 360,
142
142
  SpiceFrame.IMAP_ULTRA_45: 33 / 360,
@@ -325,7 +325,7 @@ def instrument_pointing(
325
325
  """
326
326
  Compute the instrument pointing at the specified times.
327
327
 
328
- By default, the coordinates returned are Latitude/Longitude coordinates in
328
+ By default, the coordinates returned are (Longitude, Latitude) coordinates in
329
329
  the reference frame `to_frame`. Cartesian coordinates can be returned if
330
330
  desired by setting `cartesian=True`.
331
331
 
@@ -3,14 +3,9 @@
3
3
  import functools
4
4
  import logging
5
5
  import os
6
- from collections.abc import Generator
7
- from contextlib import contextmanager
8
- from pathlib import Path
9
6
  from typing import Any, Callable, Optional, Union, overload
10
7
 
11
- import numpy as np
12
8
  import spiceypy
13
- from numpy.typing import NDArray
14
9
  from spiceypy.utils.exceptions import SpiceyError
15
10
 
16
11
  from imap_processing import imap_module_directory
@@ -181,277 +176,6 @@ def ensure_spice(
181
176
  return _decorator
182
177
 
183
178
 
184
- @contextmanager
185
- def open_spice_ck_file(pointing_frame_path: Path) -> Generator[int, None, None]:
186
- """
187
- Context manager for handling SPICE CK files.
188
-
189
- Parameters
190
- ----------
191
- pointing_frame_path : str
192
- Path to the CK file.
193
-
194
- Yields
195
- ------
196
- handle : int
197
- Handle to the opened CK file.
198
- """
199
- # TODO: We will need to figure out if ck kernel changes
200
- # and how that will affect appending to the pointing
201
- # frame kernel.
202
- if pointing_frame_path.exists():
203
- handle = spiceypy.dafopw(str(pointing_frame_path))
204
- else:
205
- handle = spiceypy.ckopn(str(pointing_frame_path), "CK", 0)
206
- try:
207
- yield handle
208
- finally:
209
- spiceypy.ckcls(handle)
210
-
211
-
212
- @ensure_spice
213
- def create_pointing_frame(pointing_frame_path: Path, ck_path: Path) -> None:
214
- """
215
- Create the pointing frame.
216
-
217
- Parameters
218
- ----------
219
- pointing_frame_path : pathlib.Path
220
- Location of pointing frame kernel.
221
- ck_path : pathlib.Path
222
- Location of the CK kernel.
223
-
224
- Notes
225
- -----
226
- Kernels required to be furnished:
227
- "imap_science_0001.tf",
228
- "imap_sclk_0000.tsc",
229
- "imap_sim_ck_2hr_2secsampling_with_nutation.bc" or
230
- "sim_1yr_imap_attitude.bc",
231
- "imap_wkcp.tf",
232
- "naif0012.tls"
233
-
234
- Assumptions:
235
- - The MOC has removed timeframe in which nutation/procession are present.
236
- TODO: We may come back and have a check for this.
237
- - We will continue to append to the pointing frame kernel.
238
- TODO: Figure out how we want to handle the file size becoming too large.
239
- - For now we can only furnish a single ck kernel.
240
- TODO: This will not be the case once we add the ability to query the .csv.
241
-
242
- References
243
- ----------
244
- https://numpydoc.readthedocs.io/en/latest/format.html#references
245
- """
246
- # Get IDs.
247
- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.gipool
248
- id_imap_dps = spiceypy.gipool("FRAME_IMAP_DPS", 0, 1)
249
- id_imap_sclk = spiceypy.gipool("CK_-43000_SCLK", 0, 1)
250
-
251
- # Verify that only ck_path kernel is loaded.
252
- count = spiceypy.ktotal("ck")
253
- loaded_ck_kernel, _, _, _ = spiceypy.kdata(count - 1, "ck")
254
-
255
- if count != 1 or str(ck_path) != loaded_ck_kernel:
256
- raise ValueError(f"Error: Expected CK kernel {ck_path}")
257
-
258
- # If the pointing frame kernel already exists, find the last time.
259
- if pointing_frame_path.exists():
260
- # Get the last time in the pointing frame kernel.
261
- pointing_cover = spiceypy.ckcov(
262
- str(pointing_frame_path), int(id_imap_dps), True, "SEGMENT", 0, "TDB"
263
- )
264
- num_segments = spiceypy.wncard(pointing_cover)
265
- _, et_end_pointing_frame = spiceypy.wnfetd(pointing_cover, num_segments - 1)
266
- else:
267
- et_end_pointing_frame = None
268
-
269
- # TODO: Query for .csv file to get the pointing start and end times.
270
- # TODO: Remove next four lines once query is added.
271
- id_imap_spacecraft = spiceypy.gipool("FRAME_IMAP_SPACECRAFT", 0, 1)
272
- ck_cover = spiceypy.ckcov(
273
- str(ck_path), int(id_imap_spacecraft), True, "INTERVAL", 0, "TDB"
274
- )
275
- num_intervals = spiceypy.wncard(ck_cover)
276
-
277
- with open_spice_ck_file(pointing_frame_path) as handle:
278
- # TODO: this will change to the number of pointings.
279
- for i in range(num_intervals):
280
- # Get the coverage window
281
- # TODO: this will change to pointing start and end time.
282
- et_start, et_end = spiceypy.wnfetd(ck_cover, i)
283
- et_times = _get_et_times(et_start, et_end)
284
-
285
- # TODO: remove after query is added.
286
- if (
287
- et_end_pointing_frame is not None
288
- and et_times[0] < et_end_pointing_frame
289
- ):
290
- break
291
-
292
- # Create a rotation matrix
293
- rotation_matrix = _create_rotation_matrix(et_times)
294
-
295
- # Convert the rotation matrix to a quaternion.
296
- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.m2q
297
- q_avg = spiceypy.m2q(rotation_matrix)
298
-
299
- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.sce2c
300
- # Convert start and end times to SCLK.
301
- sclk_begtim = spiceypy.sce2c(int(id_imap_sclk), et_times[0])
302
- sclk_endtim = spiceypy.sce2c(int(id_imap_sclk), et_times[-1])
303
-
304
- # Create the pointing frame kernel.
305
- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.ckw02
306
- spiceypy.ckw02(
307
- # Handle of an open CK file.
308
- handle,
309
- # Start time of the segment.
310
- sclk_begtim,
311
- # End time of the segment.
312
- sclk_endtim,
313
- # Pointing frame ID.
314
- int(id_imap_dps),
315
- # Reference frame.
316
- "ECLIPJ2000", # Reference frame
317
- # Identifier.
318
- "IMAP_DPS",
319
- # Number of pointing intervals.
320
- 1,
321
- # Start times of individual pointing records within segment.
322
- # Since there is only a single record this is equal to sclk_begtim.
323
- np.array([sclk_begtim]),
324
- # End times of individual pointing records within segment.
325
- # Since there is only a single record this is equal to sclk_endtim.
326
- np.array([sclk_endtim]), # Single stop time
327
- # Average quaternion.
328
- q_avg,
329
- # 0.0 Angular rotation terms.
330
- np.array([0.0, 0.0, 0.0]),
331
- # Rates (seconds per tick) at which the quaternion and
332
- # angular velocity change.
333
- np.array([1.0]),
334
- )
335
-
336
-
337
- def _get_et_times(et_start: float, et_end: float) -> NDArray[np.float64]:
338
- """
339
- Get times for pointing start and stop.
340
-
341
- Parameters
342
- ----------
343
- et_start : float
344
- Pointing start time.
345
- et_end : float
346
- Pointing end time.
347
-
348
- Returns
349
- -------
350
- et_times : numpy.ndarray
351
- Array of times between et_start and et_end.
352
- """
353
- # TODO: Queried pointing start and stop times here.
354
- # TODO removing the @ensure_spice decorator when using the repointing table.
355
-
356
- # 1 spin/15 seconds; 10 quaternions / spin.
357
- num_samples = (et_end - et_start) / 15 * 10
358
- # There were rounding errors when using spiceypy.pxform so np.ceil and np.floor
359
- # were used to ensure the start and end times were included in the array.
360
- et_times = np.linspace(
361
- np.ceil(et_start * 1e6) / 1e6, np.floor(et_end * 1e6) / 1e6, int(num_samples)
362
- )
363
-
364
- return et_times
365
-
366
-
367
- @ensure_spice
368
- def _average_quaternions(et_times: np.ndarray) -> NDArray:
369
- """
370
- Average the quaternions.
371
-
372
- Parameters
373
- ----------
374
- et_times : numpy.ndarray
375
- Array of times between et_start and et_end.
376
-
377
- Returns
378
- -------
379
- q_avg : np.ndarray
380
- Average quaternion.
381
- """
382
- aggregate = np.zeros((4, 4))
383
- for tdb in et_times:
384
- # we use a quick and dirty method here for grabbing the quaternions
385
- # from the attitude kernel. Depending on how well the kernel input
386
- # data is built and sampled, there may or may not be aliasing with this
387
- # approach. If it turns out that we need to pull the quaternions
388
- # directly from the CK there are several routines that exist to do this
389
- # but it's not straight forward. We'll revisit this if needed.
390
-
391
- # Rotation matrix from IMAP spacecraft frame to ECLIPJ2000.
392
- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.pxform
393
- body_rots = spiceypy.pxform("IMAP_SPACECRAFT", "ECLIPJ2000", tdb)
394
- # Convert rotation matrix to quaternion.
395
- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.m2q
396
- body_quat = spiceypy.m2q(body_rots)
397
-
398
- # Standardize the quaternion so that they may be compared.
399
- body_quat = body_quat * np.sign(body_quat[0])
400
- # Aggregate quaternions into a single matrix.
401
- aggregate += np.outer(body_quat, body_quat)
402
-
403
- # Reference: "On Averaging Rotations".
404
- # Link: https://link.springer.com/content/pdf/10.1023/A:1011129215388.pdf
405
- aggregate /= len(et_times)
406
-
407
- # Compute eigen values and vectors of the matrix A
408
- # Eigenvalues tell you how much "influence" each
409
- # direction (eigenvector) has.
410
- # The largest eigenvalue corresponds to the direction
411
- # that has the most influence.
412
- # The eigenvector corresponding to the largest
413
- # eigenvalue points in the direction that has the most
414
- # combined rotation influence.
415
- eigvals, eigvecs = np.linalg.eig(aggregate)
416
- # q0: The scalar part of the quaternion.
417
- # q1, q2, q3: The vector part of the quaternion.
418
- q_avg = eigvecs[:, np.argmax(eigvals)]
419
-
420
- return q_avg
421
-
422
-
423
- def _create_rotation_matrix(et_times: np.ndarray) -> NDArray:
424
- """
425
- Create a rotation matrix.
426
-
427
- Parameters
428
- ----------
429
- et_times : numpy.ndarray
430
- Array of times between et_start and et_end.
431
-
432
- Returns
433
- -------
434
- rotation_matrix : np.ndarray
435
- Rotation matrix.
436
- """
437
- # Averaged quaternions.
438
- q_avg = _average_quaternions(et_times)
439
-
440
- # Converts the averaged quaternion (q_avg) into a rotation matrix
441
- # and get inertial z axis.
442
- # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.q2m
443
- z_avg = spiceypy.q2m(list(q_avg))[:, 2]
444
- # y_avg is perpendicular to both z_avg and the standard Z-axis.
445
- y_avg = np.cross(z_avg, [0, 0, 1])
446
- # x_avg is perpendicular to y_avg and z_avg.
447
- x_avg = np.cross(y_avg, z_avg)
448
-
449
- # Construct the rotation matrix from x_avg, y_avg, z_avg
450
- rotation_matrix = np.asarray([x_avg, y_avg, z_avg])
451
-
452
- return rotation_matrix
453
-
454
-
455
179
  def furnish_time_kernel() -> None:
456
180
  """Furnish the time kernels."""
457
181
  spice_test_data_path = imap_module_directory / "tests/spice/test_data"