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,257 @@
1
+ """Functions for retrieving repointing table data."""
2
+
3
+ import logging
4
+ import typing
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import spiceypy
11
+ from numpy.typing import NDArray
12
+
13
+ from imap_processing.spice.kernels import ensure_spice
14
+ from imap_processing.spice.time import met_to_sclkticks, sct_to_et
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @contextmanager
20
+ def open_spice_ck_file(pointing_frame_path: Path) -> Generator[int, None, None]:
21
+ """
22
+ Context manager for handling SPICE CK files.
23
+
24
+ Parameters
25
+ ----------
26
+ pointing_frame_path : str
27
+ Path to the CK file.
28
+
29
+ Yields
30
+ ------
31
+ handle : int
32
+ Handle to the opened CK file.
33
+ """
34
+ if pointing_frame_path.exists():
35
+ handle = spiceypy.dafopw(str(pointing_frame_path))
36
+ else:
37
+ handle = spiceypy.ckopn(str(pointing_frame_path), "CK", 0)
38
+ try:
39
+ yield handle
40
+ finally:
41
+ spiceypy.ckcls(handle)
42
+
43
+
44
+ @typing.no_type_check
45
+ @ensure_spice
46
+ def create_pointing_frame(
47
+ pointing_frame_path: Path,
48
+ ck_path: Path,
49
+ repoint_start_met: NDArray,
50
+ repoint_end_met: NDArray,
51
+ ) -> None:
52
+ """
53
+ Create the pointing frame.
54
+
55
+ Parameters
56
+ ----------
57
+ pointing_frame_path : pathlib.Path
58
+ Location of pointing frame kernel.
59
+ ck_path : pathlib.Path
60
+ Location of the CK kernel.
61
+ repoint_start_met : numpy.ndarray
62
+ Start time of the repointing in MET.
63
+ repoint_end_met : numpy.ndarray
64
+ End time of the repointing in MET.
65
+
66
+ Notes
67
+ -----
68
+ Kernels required to be furnished:
69
+ "imap_science_0001.tf",
70
+ "imap_sclk_0000.tsc",
71
+ "imap_sim_ck_2hr_2secsampling_with_nutation.bc" or
72
+ "sim_1yr_imap_attitude.bc",
73
+ "imap_wkcp.tf",
74
+ "naif0012.tls"
75
+
76
+ Assumptions:
77
+ - The MOC has removed timeframe in which nutation/procession are present.
78
+ TODO: We may come back and have a check for this.
79
+ - The pointing frame kernel is made based on the most recent ck kernel.
80
+ In other words 1:1 ratio.
81
+ """
82
+ # Get IDs.
83
+ # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.gipool
84
+ id_imap_dps = spiceypy.gipool("FRAME_IMAP_DPS", 0, 1)
85
+ id_imap_sclk = spiceypy.gipool("CK_-43000_SCLK", 0, 1)
86
+
87
+ # Verify that only ck_path kernel is loaded.
88
+ count = spiceypy.ktotal("ck")
89
+ loaded_ck_kernel, _, _, _ = spiceypy.kdata(count - 1, "ck")
90
+
91
+ if count != 1 or str(ck_path) != loaded_ck_kernel:
92
+ raise ValueError(f"Error: Expected CK kernel {ck_path}")
93
+
94
+ id_imap_spacecraft = spiceypy.gipool("FRAME_IMAP_SPACECRAFT", 0, 1)
95
+
96
+ # Select only the pointings within the attitude coverage.
97
+ ck_cover = spiceypy.ckcov(
98
+ str(ck_path), int(id_imap_spacecraft), True, "INTERVAL", 0, "TDB"
99
+ )
100
+ num_intervals = spiceypy.wncard(ck_cover)
101
+ et_start, _ = spiceypy.wnfetd(ck_cover, 0)
102
+ _, et_end = spiceypy.wnfetd(ck_cover, num_intervals - 1)
103
+
104
+ sclk_ticks_start = met_to_sclkticks(repoint_start_met)
105
+ et_start_repoint = sct_to_et(sclk_ticks_start)
106
+ sclk_ticks_end = met_to_sclkticks(repoint_end_met)
107
+ et_end_repoint = sct_to_et(sclk_ticks_end)
108
+
109
+ valid_mask = (et_start_repoint >= et_start) & (et_end_repoint <= et_end)
110
+ et_start_repoint = et_start_repoint[valid_mask]
111
+ et_end_repoint = et_end_repoint[valid_mask]
112
+
113
+ with open_spice_ck_file(pointing_frame_path) as handle:
114
+ for i in range(len(repoint_start_met)):
115
+ # 1 spin/15 seconds; 10 quaternions / spin.
116
+ num_samples = (et_end_repoint[i] - et_start_repoint[i]) / 15 * 10
117
+ # There were rounding errors when using spiceypy.pxform
118
+ # so np.ceil and np.floor were used to ensure the start
119
+ # and end times were within the ck range.
120
+ et_times = np.linspace(
121
+ np.ceil(et_start_repoint[i] * 1e6) / 1e6,
122
+ np.floor(et_end_repoint[i] * 1e6) / 1e6,
123
+ int(num_samples),
124
+ )
125
+
126
+ # Create a rotation matrix
127
+ rotation_matrix = _create_rotation_matrix(et_times)
128
+
129
+ # Convert the rotation matrix to a quaternion.
130
+ # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.m2q
131
+ q_avg = spiceypy.m2q(rotation_matrix)
132
+
133
+ # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.sce2c
134
+ # Convert start and end times to SCLK.
135
+ sclk_begtim = spiceypy.sce2c(int(id_imap_sclk), et_times[0])
136
+ sclk_endtim = spiceypy.sce2c(int(id_imap_sclk), et_times[-1])
137
+
138
+ # Create the pointing frame kernel.
139
+ # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.ckw02
140
+ spiceypy.ckw02(
141
+ # Handle of an open CK file.
142
+ handle,
143
+ # Start time of the segment.
144
+ sclk_begtim,
145
+ # End time of the segment.
146
+ sclk_endtim,
147
+ # Pointing frame ID.
148
+ int(id_imap_dps),
149
+ # Reference frame.
150
+ "ECLIPJ2000", # Reference frame
151
+ # Identifier.
152
+ "IMAP_DPS",
153
+ # Number of pointing intervals.
154
+ 1,
155
+ # Start times of individual pointing records within segment.
156
+ # Since there is only a single record this is equal to sclk_begtim.
157
+ np.array([sclk_begtim]),
158
+ # End times of individual pointing records within segment.
159
+ # Since there is only a single record this is equal to sclk_endtim.
160
+ np.array([sclk_endtim]), # Single stop time
161
+ # Average quaternion.
162
+ q_avg,
163
+ # 0.0 Angular rotation terms.
164
+ np.array([0.0, 0.0, 0.0]),
165
+ # Rates (seconds per tick) at which the quaternion and
166
+ # angular velocity change.
167
+ np.array([1.0]),
168
+ )
169
+
170
+
171
+ @typing.no_type_check
172
+ @ensure_spice
173
+ def _average_quaternions(et_times: np.ndarray) -> NDArray:
174
+ """
175
+ Average the quaternions.
176
+
177
+ Parameters
178
+ ----------
179
+ et_times : numpy.ndarray
180
+ Array of times between et_start and et_end.
181
+
182
+ Returns
183
+ -------
184
+ q_avg : np.ndarray
185
+ Average quaternion.
186
+ """
187
+ aggregate = np.zeros((4, 4))
188
+ for tdb in et_times:
189
+ # we use a quick and dirty method here for grabbing the quaternions
190
+ # from the attitude kernel. Depending on how well the kernel input
191
+ # data is built and sampled, there may or may not be aliasing with this
192
+ # approach. If it turns out that we need to pull the quaternions
193
+ # directly from the CK there are several routines that exist to do this
194
+ # but it's not straight forward. We'll revisit this if needed.
195
+
196
+ # Rotation matrix from IMAP spacecraft frame to ECLIPJ2000.
197
+ # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.pxform
198
+ body_rots = spiceypy.pxform("IMAP_SPACECRAFT", "ECLIPJ2000", tdb)
199
+ # Convert rotation matrix to quaternion.
200
+ # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.m2q
201
+ body_quat = spiceypy.m2q(body_rots)
202
+
203
+ # Standardize the quaternion so that they may be compared.
204
+ body_quat = body_quat * np.sign(body_quat[0])
205
+ # Aggregate quaternions into a single matrix.
206
+ aggregate += np.outer(body_quat, body_quat)
207
+
208
+ # Reference: "On Averaging Rotations".
209
+ # Link: https://link.springer.com/content/pdf/10.1023/A:1011129215388.pdf
210
+ aggregate /= len(et_times)
211
+
212
+ # Compute eigen values and vectors of the matrix A
213
+ # Eigenvalues tell you how much "influence" each
214
+ # direction (eigenvector) has.
215
+ # The largest eigenvalue corresponds to the direction
216
+ # that has the most influence.
217
+ # The eigenvector corresponding to the largest
218
+ # eigenvalue points in the direction that has the most
219
+ # combined rotation influence.
220
+ eigvals, eigvecs = np.linalg.eig(aggregate)
221
+ # q0: The scalar part of the quaternion.
222
+ # q1, q2, q3: The vector part of the quaternion.
223
+ q_avg = eigvecs[:, np.argmax(eigvals)]
224
+
225
+ return q_avg
226
+
227
+
228
+ def _create_rotation_matrix(et_times: np.ndarray) -> NDArray:
229
+ """
230
+ Create a rotation matrix.
231
+
232
+ Parameters
233
+ ----------
234
+ et_times : numpy.ndarray
235
+ Array of times between et_start and et_end.
236
+
237
+ Returns
238
+ -------
239
+ rotation_matrix : np.ndarray
240
+ Rotation matrix.
241
+ """
242
+ # Averaged quaternions.
243
+ q_avg = _average_quaternions(et_times)
244
+
245
+ # Converts the averaged quaternion (q_avg) into a rotation matrix
246
+ # and get inertial z axis.
247
+ # https://spiceypy.readthedocs.io/en/main/documentation.html#spiceypy.spiceypy.q2m
248
+ z_avg = spiceypy.q2m(list(q_avg))[:, 2]
249
+ # y_avg is perpendicular to both z_avg and the standard Z-axis.
250
+ y_avg = np.cross(z_avg, [0, 0, 1])
251
+ # x_avg is perpendicular to y_avg and z_avg.
252
+ x_avg = np.cross(y_avg, z_avg)
253
+
254
+ # Construct the rotation matrix from x_avg, y_avg, z_avg
255
+ rotation_matrix = np.asarray([x_avg, y_avg, z_avg])
256
+
257
+ return rotation_matrix
@@ -16,17 +16,32 @@ def get_repoint_data() -> pd.DataFrame:
16
16
  """
17
17
  Read repointing file using environment variable and return as dataframe.
18
18
 
19
+ Pointing and repointing nomenclature can be confusing. In this case,
20
+ repoint is taken to mean a repoint maneuver. Thus, repoint_start and repoint_end
21
+ are the times that bound when the spacecraft is performing a repointing maneuver.
22
+ This is different from a pointing which is the time between repointing maneuvers.
23
+
19
24
  REPOINT_DATA_FILEPATH environment variable should point to a local
20
25
  file where the repointing csv file is located.
21
26
 
22
27
  Returns
23
28
  -------
24
- repoint_df : pd.DataFrame
29
+ repoint_df : pandas.DataFrame
25
30
  The repointing csv loaded into a pandas dataframe. The dataframe will
26
31
  contain the following columns:
27
- - `repoint_start_time`: Starting MET time of each repoint maneuver.
28
- - `repoint_end_time`: Ending MET time of each repoint maneuver.
29
- - `repoint_id`: Unique ID number of each repoint maneuver.
32
+
33
+ * `repoint_start_sec_sclk`: Starting MET seconds of repoint maneuver.
34
+ * `repoint_start_subsec_sclk`: Starting MET microseconds of repoint
35
+ maneuver.
36
+ * `repoint_start_met`: Floating point MET of repoint maneuver start time.
37
+ Derived from `repoint_start_sec_sclk` and `repoint_start_subsec_sclk`.
38
+ * `repoint_start_utc`: UTC time of repoint maneuver start time.
39
+ * `repoint_end_sec_sclk`: Ending MET seconds of repoint maneuver.
40
+ * `repoint_end_subsec_sclk`: Ending MET microseconds of repoint maneuver.
41
+ * `repoint_end_met`: Floating point MET of repoint maneuver end time.
42
+ Derived from `repoint_end_sec_sclk` and `repoint_end_subsec_sclk`.
43
+ * `repoint_end_utc`: UTC time of repoint maneuver end time.
44
+ * `repoint_id`: Unique ID number of each repoint maneuver.
30
45
  """
31
46
  repoint_data_filepath = os.getenv("REPOINT_DATA_FILEPATH")
32
47
  if repoint_data_filepath is not None:
@@ -38,6 +53,15 @@ def get_repoint_data() -> pd.DataFrame:
38
53
  logger.info(f"Reading repointing data from {path_to_spin_file}")
39
54
  repoint_df = pd.read_csv(path_to_spin_file, comment="#")
40
55
 
56
+ # Compute times by combining seconds and subseconds fields
57
+ repoint_df["repoint_start_met"] = (
58
+ repoint_df["repoint_start_sec_sclk"]
59
+ + repoint_df["repoint_start_subsec_sclk"] / 1e6
60
+ )
61
+ repoint_df["repoint_end_met"] = (
62
+ repoint_df["repoint_end_sec_sclk"] + repoint_df["repoint_end_subsec_sclk"] / 1e6
63
+ )
64
+
41
65
  return repoint_df
42
66
 
43
67
 
@@ -68,15 +92,20 @@ def interpolate_repoint_data(
68
92
  repoint_df : pandas.DataFrame
69
93
  Repoint table data interpolated such that there is one row
70
94
  for each of the queried MET times. Output columns are:
71
- - `repoint_start_time`
72
- - `repoint_end_time`
73
- - `repoint_id`
74
- - `repoint_in_progress`
95
+
96
+ * `repoint_start_sec_sclk`
97
+ * `repoint_start_subsec_sclk`
98
+ * `repoint_start_met`
99
+ * `repoint_end_sec_sclk`
100
+ * `repoint_end_subsec_sclk`
101
+ * `repoint_end_met`
102
+ * `repoint_id`
103
+ * `repoint_in_progress`
75
104
 
76
105
  Raises
77
106
  ------
78
107
  ValueError : If any of the query_met_times are before the first repoint
79
- start time or after the last repoint start time plus 24-hours.
108
+ start time or after the last repoint start time plus 24-hours.
80
109
  """
81
110
  repoint_df = get_repoint_data()
82
111
 
@@ -84,29 +113,29 @@ def interpolate_repoint_data(
84
113
  query_met_times = np.atleast_1d(query_met_times)
85
114
 
86
115
  # Make sure no query times are before the first repoint in the dataframe.
87
- repoint_df_start_time = repoint_df["repoint_start_time"].values[0]
88
- if np.any(query_met_times < repoint_df_start_time):
89
- bad_times = query_met_times[query_met_times < repoint_df_start_time]
116
+ repoint_df_start_met = repoint_df["repoint_start_met"].values[0]
117
+ if np.any(query_met_times < repoint_df_start_met):
118
+ bad_times = query_met_times[query_met_times < repoint_df_start_met]
90
119
  raise ValueError(
91
120
  f"{bad_times.size} query times are before the first repoint start "
92
- f" time in the repoint table. {bad_times=}, {repoint_df_start_time=}"
121
+ f" time in the repoint table. {bad_times=}, {repoint_df_start_met=}"
93
122
  )
94
123
  # Make sure that no query times are after the valid range of the dataframe.
95
124
  # We approximate the end time of the table by adding 24 hours to the last
96
125
  # known repoint start time.
97
- repoint_df_end_time = repoint_df["repoint_start_time"].values[-1] + 24 * 60 * 60
98
- if np.any(query_met_times >= repoint_df_end_time):
99
- bad_times = query_met_times[query_met_times >= repoint_df_end_time]
126
+ repoint_df_end_met = repoint_df["repoint_start_met"].values[-1] + 24 * 60 * 60
127
+ if np.any(query_met_times >= repoint_df_end_met):
128
+ bad_times = query_met_times[query_met_times >= repoint_df_end_met]
100
129
  raise ValueError(
101
130
  f"{bad_times.size} query times are after the valid time of the "
102
131
  f"pointing table. The valid end time is 24-hours after the last "
103
- f"repoint_start_time. {bad_times=}, {repoint_df_end_time=}"
132
+ f"repoint_start_time. {bad_times=}, {repoint_df_end_met=}"
104
133
  )
105
134
 
106
135
  # Find the row index for each queried MET time such that:
107
136
  # repoint_start_time[i] <= MET < repoint_start_time[i+1]
108
137
  row_indices = (
109
- np.searchsorted(repoint_df["repoint_start_time"], query_met_times, side="right")
138
+ np.searchsorted(repoint_df["repoint_start_met"], query_met_times, side="right")
110
139
  - 1
111
140
  )
112
141
  out_df = repoint_df.iloc[row_indices]
@@ -115,6 +144,6 @@ def interpolate_repoint_data(
115
144
  # The table already has the correct row for each query time, so we
116
145
  # only need to check if the query time is less than the repoint end time to
117
146
  # get the same result as `repoint_start_time <= query_met_times < repoint_end_time`.
118
- out_df["repoint_in_progress"] = query_met_times < out_df["repoint_end_time"].values
147
+ out_df["repoint_in_progress"] = query_met_times < out_df["repoint_end_met"].values
119
148
 
120
149
  return out_df
@@ -22,20 +22,21 @@ def get_spin_data() -> pd.DataFrame:
22
22
  It could be s3 filepath that can be used to download the data
23
23
  through API or it could be path EFS or Batch volume mount path.
24
24
 
25
- Spin data should contain the following fields:
26
- * spin_number
27
- * spin_start_sec
28
- * spin_start_subsec
29
- * spin_period_sec
30
- * spin_period_valid
31
- * spin_phase_valid
32
- * spin_period_source
33
- * thruster_firing
34
-
35
25
  Returns
36
26
  -------
37
27
  spin_data : pandas.DataFrame
38
- Spin data.
28
+ Spin data. The DataFrame will have the following columns:
29
+
30
+ * `spin_number`: Unique integer spin number.
31
+ * `spin_start_sec_sclk`: MET seconds of spin start time.
32
+ * `spin_start_subsec_sclk`: MET microseconds of spin start time.
33
+ * `spin_start_met`: Floating point MET seconds of spin start.
34
+ * `spin_start_utc`: UTC string of spin start time.
35
+ * `spin_period_sec`: Floating point spin period in seconds.
36
+ * `spin_period_valid`: Boolean indicating whether spin period is valid.
37
+ * `spin_phase_valid`: Boolean indicating whether spin phase is valid.
38
+ * `spin_period_source`: Source used for determining spin period.
39
+ * `thruster_firing`: Boolean indicating whether thruster is firing.
39
40
  """
40
41
  spin_data_filepath = os.getenv("SPIN_DATA_FILEPATH")
41
42
  if spin_data_filepath is not None:
@@ -44,11 +45,24 @@ def get_spin_data() -> pd.DataFrame:
44
45
  # Handle the case where the environment variable is not set
45
46
  raise ValueError("SPIN_DATA_FILEPATH environment variable is not set.")
46
47
 
47
- spin_df = pd.read_csv(path_to_spin_file, comment="#")
48
- # Combine spin_start_sec and spin_start_subsec to get the spin start
49
- # time in seconds. The spin start subseconds are in milliseconds.
50
- spin_df["spin_start_time"] = (
51
- spin_df["spin_start_sec"] + spin_df["spin_start_subsec"] / 1e3
48
+ spin_df = pd.read_csv(
49
+ path_to_spin_file,
50
+ comment="#",
51
+ dtype={
52
+ "spin_number": int,
53
+ "spin_start_sec_sclk": int,
54
+ "spin_start_subsec_sclk": int,
55
+ "spin_start_utc": str,
56
+ "spin_period_sec": float,
57
+ "spin_period_valid": bool,
58
+ "spin_period_source": int,
59
+ "thruster_firing": bool,
60
+ },
61
+ )
62
+ # Combine spin_start_sec_sclk and spin_start_subsec_sclk to get the spin start
63
+ # time in seconds. The spin start subseconds are in microseconds.
64
+ spin_df["spin_start_met"] = (
65
+ spin_df["spin_start_sec_sclk"] + spin_df["spin_start_subsec_sclk"] / 1e6
52
66
  )
53
67
 
54
68
  return spin_df
@@ -71,18 +85,9 @@ def interpolate_spin_data(query_met_times: Union[float, npt.NDArray]) -> pd.Data
71
85
  Returns
72
86
  -------
73
87
  spin_df : pandas.DataFrame
74
- Spin table data with the spin-phase column added and one row
75
- interpolated for each queried MET time. Output columns are:
76
- * spin_number
77
- * spin_start_sec
78
- * spin_start_subsec
79
- * spin_period_sec
80
- * spin_period_valid
81
- * spin_phase_valid
82
- * spin_period_source
83
- * thruster_firing
84
- * spin_start_met
85
- * sc_spin_phase
88
+ Spin table data interpolated for each queried MET time. In addition to
89
+ the columns output from :py:func:`get_spin_data`, the `sc_spin_phase`
90
+ column is added and is uniquely computed for each queried MET time.
86
91
  """
87
92
  spin_df = get_spin_data()
88
93
 
@@ -95,9 +100,9 @@ def interpolate_spin_data(query_met_times: Union[float, npt.NDArray]) -> pd.Data
95
100
  query_met_times = np.atleast_1d(query_met_times)
96
101
 
97
102
  # Make sure input times are within the bounds of spin data
98
- spin_df_start_time = spin_df["spin_start_time"].values[0]
103
+ spin_df_start_time = spin_df["spin_start_met"].values[0]
99
104
  spin_df_end_time = (
100
- spin_df["spin_start_time"].values[-1] + spin_df["spin_period_sec"].values[-1]
105
+ spin_df["spin_start_met"].values[-1] + spin_df["spin_period_sec"].values[-1]
101
106
  )
102
107
  input_start_time = query_met_times.min()
103
108
  input_end_time = query_met_times.max()
@@ -115,13 +120,13 @@ def interpolate_spin_data(query_met_times: Union[float, npt.NDArray]) -> pd.Data
115
120
  # >>> np.searchsorted(df['a'], [0, 13, 15, 32, 70], side='right')
116
121
  # array([1, 1, 2, 3, 5])
117
122
  last_spin_indices = (
118
- np.searchsorted(spin_df["spin_start_time"], query_met_times, side="right") - 1
123
+ np.searchsorted(spin_df["spin_start_met"], query_met_times, side="right") - 1
119
124
  )
120
125
  # Generate a dataframe with one row per query time
121
126
  out_df = spin_df.iloc[last_spin_indices]
122
127
 
123
128
  # Calculate spin phase
124
- spin_phases = (query_met_times - out_df["spin_start_time"].values) / out_df[
129
+ spin_phases = (query_met_times - out_df["spin_start_met"].values) / out_df[
125
130
  "spin_period_sec"
126
131
  ].values
127
132
 
@@ -185,7 +190,7 @@ def get_spacecraft_spin_phase(
185
190
  Get the spacecraft spin phase for the input query times.
186
191
 
187
192
  Formula to calculate spin phase:
188
- spin_phase = (query_met_times - spin_start_time) / spin_period_sec
193
+ spin_phase = (query_met_times - spin_start_met) / spin_period_sec
189
194
 
190
195
  Parameters
191
196
  ----------
@@ -2,6 +2,7 @@
2
2
 
3
3
  import typing
4
4
  from collections.abc import Collection, Iterable
5
+ from datetime import datetime
5
6
  from typing import Union
6
7
 
7
8
  import numpy as np
@@ -297,3 +298,26 @@ def et_to_utc(
297
298
  UTC time(s).
298
299
  """
299
300
  return spiceypy.et2utc(et, format_str, precision, utclen)
301
+
302
+
303
+ def epoch_to_doy(epoch: np.ndarray) -> npt.NDArray:
304
+ """
305
+ Convert epoch times to day of year (1-365/366).
306
+
307
+ Parameters
308
+ ----------
309
+ epoch : xarray.DataArray
310
+ Time, number of nanoseconds since J2000 with leap seconds included.
311
+
312
+ Returns
313
+ -------
314
+ day_of_year : numpy.ndarray
315
+ Day of year (1-365/366) for each epoch value.
316
+ """
317
+ et = ttj2000ns_to_et(epoch.data)
318
+ # Get UTC time strings in ISO calendar format
319
+ time_strings = et_to_utc(et, "ISOC")
320
+ # Extract DOY from datetime
321
+ return np.array(
322
+ [datetime.fromisoformat(date).timetuple().tm_yday for date in time_strings]
323
+ )
@@ -72,8 +72,7 @@ def filter_good_data(full_sweep_sci: xr.Dataset) -> npt.NDArray:
72
72
  f"{full_sweep_sci['sweep_table'].data[bad_cycle_indices]}"
73
73
  )
74
74
  logger.debug(
75
- "Plan ID should be same: "
76
- f"{full_sweep_sci['plan_id'].data[bad_cycle_indices]}"
75
+ f"Plan ID should be same: {full_sweep_sci['plan_id'].data[bad_cycle_indices]}"
77
76
  )
78
77
  logger.debug(
79
78
  f"Mode Id should be 3(HVSCI): {full_sweep_sci['mode'].data[bad_cycle_indices]}"
@@ -426,7 +425,7 @@ def process_sweep_data(full_sweep_sci: xr.Dataset, cem_prefix: str) -> xr.Datase
426
425
 
427
426
 
428
427
  def process_swapi_science(
429
- sci_dataset: xr.Dataset, hk_dataset: xr.Dataset, data_version: str
428
+ sci_dataset: xr.Dataset, hk_dataset: xr.Dataset
430
429
  ) -> xr.Dataset:
431
430
  """
432
431
  Will process SWAPI science data and create CDF file.
@@ -437,8 +436,6 @@ def process_swapi_science(
437
436
  L0 data.
438
437
  hk_dataset : xarray.Dataset
439
438
  Housekeeping data.
440
- data_version : str
441
- Version of the data product being created.
442
439
 
443
440
  Returns
444
441
  -------
@@ -586,8 +583,6 @@ def process_swapi_science(
586
583
  )
587
584
 
588
585
  # Add other global attributes
589
- # TODO: add others like below once add_global_attribute is fixed
590
- cdf_manager.add_global_attribute("Data_version", data_version)
591
586
  l1_global_attrs = cdf_manager.get_global_attributes("imap_swapi_l1_sci")
592
587
  l1_global_attrs["Apid"] = f"{sci_dataset['pkt_apid'].data[0]}"
593
588
 
@@ -632,6 +627,18 @@ def process_swapi_science(
632
627
  dims=["epoch"],
633
628
  attrs=cdf_manager.get_variable_attributes("plan_id"),
634
629
  )
630
+ # Add ESA_LVL5 for L2 and L3 purposes.
631
+ # We need to store ESA_LVL5 at SEQ_NUMBER==11
632
+ # which is 71 energy step's ESA_LVL5 value. ESA_LVL5 gets
633
+ # updated every 6th step. This is used in L2 to calculate last 9 fine
634
+ # energy steps.
635
+ dataset["esa_lvl5"] = xr.DataArray(
636
+ good_sweep_sci["esa_lvl5"].data.reshape(total_full_sweeps, 12)[:, 11],
637
+ name="esa_lvl5",
638
+ dims=["epoch"],
639
+ attrs=cdf_manager.get_variable_attributes("esa_lvl5"),
640
+ )
641
+
635
642
  # Add these additional housekeeping support data
636
643
  # SWP_HK.LUT_CHOICE - Which LUT is in use
637
644
  # SWP_HK.FPGA_TYPE - Type number of the FPGA
@@ -703,7 +710,7 @@ def process_swapi_science(
703
710
  return dataset
704
711
 
705
712
 
706
- def swapi_l1(dependencies: list, data_version: str) -> xr.Dataset:
713
+ def swapi_l1(dependencies: list) -> xr.Dataset:
707
714
  """
708
715
  Will process SWAPI level 0 data to level 1.
709
716
 
@@ -711,8 +718,6 @@ def swapi_l1(dependencies: list, data_version: str) -> xr.Dataset:
711
718
  ----------
712
719
  dependencies : list
713
720
  Input dependencies needed for L1 processing.
714
- data_version : str
715
- Version of the data product being created.
716
721
 
717
722
  Returns
718
723
  -------
@@ -744,7 +749,7 @@ def swapi_l1(dependencies: list, data_version: str) -> xr.Dataset:
744
749
  ):
745
750
  # process science data
746
751
  sci_dataset = process_swapi_science(
747
- l0_unpacked_dict[SWAPIAPID.SWP_SCI], l1_hk_ds, data_version
752
+ l0_unpacked_dict[SWAPIAPID.SWP_SCI], l1_hk_ds
748
753
  )
749
754
  processed_data.append(sci_dataset)
750
755
 
@@ -753,7 +758,6 @@ def swapi_l1(dependencies: list, data_version: str) -> xr.Dataset:
753
758
  # Add HK datalevel attrs
754
759
  imap_attrs = ImapCdfAttributes()
755
760
  imap_attrs.add_instrument_global_attrs("swapi")
756
- imap_attrs.add_global_attribute("Data_version", data_version)
757
761
  imap_attrs.add_instrument_variable_attrs(instrument="swapi", level=None)
758
762
  hk_ds.attrs.update(imap_attrs.get_global_attributes("imap_swapi_l1_hk"))
759
763
  hk_common_attrs = imap_attrs.get_variable_attributes("hk_attrs")