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
@@ -2,7 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+ import tempfile
5
7
  from copy import deepcopy
8
+ from pathlib import Path
6
9
  from unittest import mock
7
10
 
8
11
  import astropy_healpix.healpy as hp
@@ -10,6 +13,7 @@ import numpy as np
10
13
  import pytest
11
14
  import xarray as xr
12
15
 
16
+ from imap_processing.cdf.utils import load_cdf, write_cdf
13
17
  from imap_processing.ena_maps import ena_maps
14
18
  from imap_processing.ena_maps.utils import spatial_utils
15
19
  from imap_processing.ena_maps.utils.coordinates import CoordNames
@@ -52,8 +56,8 @@ class TestUltraPointingSet:
52
56
  """Test instantiation of UltraPointingSet"""
53
57
  ultra_psets = [
54
58
  ena_maps.UltraPointingSet(
59
+ l1c_product,
55
60
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
56
- l1c_dataset=l1c_product,
57
61
  )
58
62
  for l1c_product in self.l1c_pset_products
59
63
  ]
@@ -78,15 +82,40 @@ class TestUltraPointingSet:
78
82
  # Check that the unwrapped_dims_dict is as expected
79
83
  assert ultra_pset.unwrapped_dims_dict["counts"] == (
80
84
  "epoch",
81
- "energy",
85
+ "energy_bin_geometric_mean",
82
86
  "pixel",
83
87
  )
84
88
  # Check the non_spatial_coords are as expected
85
89
  assert tuple(ultra_pset.non_spatial_coords.keys()) == (
86
90
  "epoch",
87
- "energy",
91
+ "energy_bin_geometric_mean",
88
92
  )
89
93
 
94
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
95
+ def test_init_cdf(
96
+ self,
97
+ ):
98
+ ultra_pset = self.l1c_pset_products[0]
99
+
100
+ cdf_filepath = write_cdf(ultra_pset, istp=False)
101
+
102
+ ultra_pset_from_dataset = ena_maps.UltraPointingSet(ultra_pset)
103
+
104
+ ultra_pset_from_str = ena_maps.UltraPointingSet(cdf_filepath)
105
+ ultra_pset_from_path = ena_maps.UltraPointingSet(Path(cdf_filepath))
106
+
107
+ np.testing.assert_allclose(
108
+ ultra_pset_from_dataset.data["counts"].values,
109
+ ultra_pset_from_str.data["counts"].values,
110
+ rtol=1e-6,
111
+ )
112
+
113
+ np.testing.assert_allclose(
114
+ ultra_pset_from_dataset.data["counts"].values,
115
+ ultra_pset_from_path.data["counts"].values,
116
+ rtol=1e-6,
117
+ )
118
+
90
119
  @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
91
120
  @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
92
121
  def test_different_spacing_raises_error(self):
@@ -100,11 +129,44 @@ class TestUltraPointingSet:
100
129
 
101
130
  with pytest.raises(ValueError, match="do not match"):
102
131
  ena_maps.UltraPointingSet(
132
+ ultra_pset_ds,
103
133
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
104
- l1c_dataset=ultra_pset_ds,
105
134
  )
106
135
 
107
136
 
137
+ @pytest.fixture(scope="module")
138
+ def hi_pset_cdf_path(imap_tests_path):
139
+ return imap_tests_path / "hi/data/l1/imap_hi_l1c_45sensor-pset_20250415_v999.cdf"
140
+
141
+
142
+ @pytest.mark.external_test_data
143
+ class TestHiPointingSet:
144
+ """Test suite for HiPointingSet class."""
145
+
146
+ def test_init(self, hi_pset_cdf_path):
147
+ """Test coverage for __init__ method."""
148
+ pset_ds = load_cdf(hi_pset_cdf_path)
149
+ hi_pset = ena_maps.HiPointingSet(pset_ds)
150
+ assert isinstance(hi_pset, ena_maps.HiPointingSet)
151
+ assert hi_pset.spice_reference_frame == geometry.SpiceFrame.ECLIPJ2000
152
+ assert hi_pset.num_points == 3600
153
+ np.testing.assert_array_equal(hi_pset.az_el_points.shape, (3600, 2))
154
+
155
+ def test_from_cdf(self, hi_pset_cdf_path):
156
+ """Test coverage for from_cdf method."""
157
+ hi_pset = ena_maps.HiPointingSet(hi_pset_cdf_path)
158
+ assert isinstance(hi_pset, ena_maps.HiPointingSet)
159
+
160
+ def test_plays_nice_with_rectangular_sky_map(self, hi_pset_cdf_path):
161
+ """Test that HiPointingSet works with RectangularSkyMap"""
162
+ hi_pset = ena_maps.HiPointingSet(hi_pset_cdf_path)
163
+ rect_map = ena_maps.RectangularSkyMap(
164
+ spacing_deg=2, spice_frame=geometry.SpiceFrame.ECLIPJ2000
165
+ )
166
+ rect_map.project_pset_values_to_map(hi_pset, ["counts", "exposure_times"])
167
+ assert rect_map.data_1d["counts"].max() > 0
168
+
169
+
108
170
  class TestRectangularSkyMap:
109
171
  @pytest.fixture(autouse=True)
110
172
  def _setup_ultra_l1c_pset_products(self, setup_all_pset_products):
@@ -115,8 +177,8 @@ class TestRectangularSkyMap:
115
177
  )
116
178
  self.ultra_psets = [
117
179
  ena_maps.UltraPointingSet(
180
+ l1c_product,
118
181
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
119
- l1c_dataset=l1c_product,
120
182
  )
121
183
  for l1c_product in self.ultra_l1c_pset_products
122
184
  ]
@@ -130,8 +192,8 @@ class TestRectangularSkyMap:
130
192
  )
131
193
  self.rectangular_psets = [
132
194
  ena_maps.RectangularPointingSet(
195
+ l1c_product,
133
196
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
134
- l1c_dataset=l1c_product,
135
197
  )
136
198
  for l1c_product in self.rectangular_l1c_pset_products
137
199
  ]
@@ -188,7 +250,7 @@ class TestRectangularSkyMap:
188
250
  for ultra_pset in self.ultra_psets:
189
251
  rectangular_map.project_pset_values_to_map(
190
252
  ultra_pset,
191
- value_keys=["counts", "exposure_time"],
253
+ value_keys=["counts", "exposure_factor"],
192
254
  index_match_method=index_matching_method,
193
255
  )
194
256
 
@@ -199,20 +261,22 @@ class TestRectangularSkyMap:
199
261
  simple_summed_pset_counts_by_energy = np.zeros(
200
262
  shape=(
201
263
  self.ultra_l1c_pset_products[0]["counts"].sizes[
202
- CoordNames.ENERGY.value
264
+ CoordNames.ENERGY_ULTRA.value
203
265
  ],
204
266
  )
205
267
  )
206
268
  for pset in self.ultra_l1c_pset_products:
207
269
  simple_summed_pset_counts_by_energy += pset["counts"].sum(
208
- dim=[d for d in pset["counts"].dims if d != CoordNames.ENERGY.value]
270
+ dim=[
271
+ d for d in pset["counts"].dims if d != CoordNames.ENERGY_ULTRA.value
272
+ ]
209
273
  )
210
274
 
211
275
  rmap_counts_per_energy_bin = rectangular_map.data_1d["counts"].sum(
212
276
  dim=[
213
277
  d
214
278
  for d in rectangular_map.data_1d["counts"].dims
215
- if d != CoordNames.ENERGY.value
279
+ if d != CoordNames.ENERGY_ULTRA.value
216
280
  ]
217
281
  )
218
282
 
@@ -235,7 +299,7 @@ class TestRectangularSkyMap:
235
299
  """
236
300
  index_matching_method = ena_maps.IndexMatchMethod.PUSH
237
301
 
238
- pset_spacing_deg = self.rectangular_psets[0].spacing_deg
302
+ pset_spacing_deg = self.rectangular_psets[0].sky_grid.spacing_deg
239
303
 
240
304
  # Mock frame_transform to return the az and el unchanged
241
305
  mock_frame_transform_az_el.side_effect = (
@@ -251,7 +315,7 @@ class TestRectangularSkyMap:
251
315
  for rectangular_pset in self.rectangular_psets:
252
316
  rectangular_map.project_pset_values_to_map(
253
317
  rectangular_pset,
254
- value_keys=["counts", "exposure_time"],
318
+ value_keys=["counts", "exposure_factor"],
255
319
  index_match_method=index_matching_method,
256
320
  )
257
321
 
@@ -262,20 +326,22 @@ class TestRectangularSkyMap:
262
326
  simple_summed_pset_counts_by_energy = np.zeros(
263
327
  shape=(
264
328
  self.rectangular_l1c_pset_products[0]["counts"].sizes[
265
- CoordNames.ENERGY.value
329
+ CoordNames.ENERGY_ULTRA.value
266
330
  ],
267
331
  )
268
332
  )
269
333
  for pset in self.rectangular_l1c_pset_products:
270
334
  simple_summed_pset_counts_by_energy += pset["counts"].sum(
271
- dim=[d for d in pset["counts"].dims if d != CoordNames.ENERGY.value]
335
+ dim=[
336
+ d for d in pset["counts"].dims if d != CoordNames.ENERGY_ULTRA.value
337
+ ]
272
338
  )
273
339
 
274
340
  rmap_counts_per_energy_bin = rectangular_map.data_1d["counts"].sum(
275
341
  dim=[
276
342
  d
277
343
  for d in rectangular_map.data_1d["counts"].dims
278
- if d != CoordNames.ENERGY.value
344
+ if d != CoordNames.ENERGY_ULTRA.value
279
345
  ]
280
346
  )
281
347
 
@@ -344,7 +410,7 @@ class TestRectangularSkyMap:
344
410
 
345
411
  rectangular_map.project_pset_values_to_map(
346
412
  rectangular_pset,
347
- value_keys=["counts", "exposure_time"],
413
+ value_keys=["counts", "exposure_factor"],
348
414
  index_match_method=index_matching_method,
349
415
  )
350
416
  expected_value_every_pixel += pset_num
@@ -372,13 +438,13 @@ class TestRectangularSkyMap:
372
438
  assert "counts" in rect_map_ds.data_vars
373
439
  assert rect_map_ds["counts"].shape == (
374
440
  1,
375
- rectangular_pset.data["counts"].sizes[CoordNames.ENERGY.value],
441
+ rectangular_pset.data["counts"].sizes[CoordNames.ENERGY_ULTRA.value],
376
442
  360 / skymap_spacing,
377
443
  180 / skymap_spacing,
378
444
  )
379
445
  assert rect_map_ds["counts"].dims == (
380
446
  CoordNames.TIME.value,
381
- CoordNames.ENERGY.value,
447
+ CoordNames.ENERGY_ULTRA.value,
382
448
  CoordNames.AZIMUTH_L2.value,
383
449
  CoordNames.ELEVATION_L2.value,
384
450
  )
@@ -403,8 +469,8 @@ class TestHealpixSkyMap:
403
469
  )
404
470
  self.ultra_psets = [
405
471
  ena_maps.UltraPointingSet(
472
+ l1c_product,
406
473
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
407
- l1c_dataset=l1c_product,
408
474
  )
409
475
  for l1c_product in self.ultra_l1c_pset_products
410
476
  ]
@@ -418,8 +484,8 @@ class TestHealpixSkyMap:
418
484
  )
419
485
  self.rectangular_psets = [
420
486
  ena_maps.RectangularPointingSet(
487
+ l1c_product,
421
488
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
422
- l1c_dataset=l1c_product,
423
489
  )
424
490
  for l1c_product in self.rectangular_l1c_pset_products
425
491
  ]
@@ -491,7 +557,7 @@ class TestHealpixSkyMap:
491
557
 
492
558
  # Create a PointingSet with a bright spot
493
559
  mock_pset_input_frame = ena_maps.UltraPointingSet(
494
- l1c_dataset=self.ultra_l1c_pset_products[0],
560
+ self.ultra_l1c_pset_products[0],
495
561
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
496
562
  )
497
563
  mock_pset_input_frame.data["counts"].values = np.zeros_like(
@@ -534,7 +600,7 @@ class TestHealpixSkyMap:
534
600
  assert "counts" in hp_map.data_1d.data_vars
535
601
 
536
602
  # Find the maximum value in the spatial pixel dimension of the healpix map
537
- bright_hp_pixel_index = hp_map.data_1d["counts"][0, :].argmax()
603
+ bright_hp_pixel_index = hp_map.data_1d["counts"][0, :].values.argmax()
538
604
  bright_hp_pixel_az_el = hp_map.az_el_points[bright_hp_pixel_index]
539
605
 
540
606
  np.testing.assert_allclose(
@@ -573,7 +639,7 @@ class TestHealpixSkyMap:
573
639
 
574
640
  # Create a PointingSet with a bright spot
575
641
  mock_pset_input_frame = ena_maps.RectangularPointingSet(
576
- l1c_dataset=self.rectangular_l1c_pset_products[0],
642
+ self.rectangular_l1c_pset_products[0],
577
643
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
578
644
  )
579
645
  mock_pset_input_frame.data["counts"].values = np.zeros_like(
@@ -584,10 +650,13 @@ class TestHealpixSkyMap:
584
650
  mock_pset_input_frame.data["counts"].values[
585
651
  :,
586
652
  :,
587
- int(input_bright_pixel_az_el_deg[0] // mock_pset_input_frame.spacing_deg),
653
+ int(
654
+ input_bright_pixel_az_el_deg[0]
655
+ // mock_pset_input_frame.sky_grid.spacing_deg
656
+ ),
588
657
  int(
589
658
  (90 + input_bright_pixel_az_el_deg[1])
590
- // mock_pset_input_frame.spacing_deg
659
+ // mock_pset_input_frame.sky_grid.spacing_deg
591
660
  ),
592
661
  ] = 1
593
662
 
@@ -625,12 +694,12 @@ class TestHealpixSkyMap:
625
694
  assert "counts" in hp_map_ds.data_vars
626
695
  assert hp_map_ds["counts"].shape == (
627
696
  1,
628
- mock_pset_input_frame.data["counts"].sizes[CoordNames.ENERGY.value],
697
+ mock_pset_input_frame.data["counts"].sizes[CoordNames.ENERGY_ULTRA.value],
629
698
  hp_map.num_points,
630
699
  )
631
700
  assert hp_map_ds["counts"].dims == (
632
701
  CoordNames.TIME.value,
633
- CoordNames.ENERGY.value,
702
+ CoordNames.ENERGY_ULTRA.value,
634
703
  CoordNames.HEALPIX_INDEX.value,
635
704
  )
636
705
  np.testing.assert_array_equal(
@@ -638,6 +707,221 @@ class TestHealpixSkyMap:
638
707
  hp_map.data_1d["counts"].values,
639
708
  )
640
709
 
710
+ @mock.patch("astropy_healpix.healpy.ang2pix")
711
+ def test_calculate_rect_pixel_value_from_healpix_map_n_subdivisions(
712
+ self,
713
+ mock_ang2pix,
714
+ ):
715
+ """Test getting rectangular pixel values from HealpixSkyMap via subdivision."""
716
+
717
+ # Mock ang2pix to return fixed values based on a dict
718
+ pixel_dict = {
719
+ # 0 subdiv - just 1 pixel
720
+ (180, 0): 0,
721
+ # 1 subdiv - all subpix have same solid angle because centered on equator
722
+ (179, -1): 1,
723
+ (179, 1): 2,
724
+ (181, -1): 3,
725
+ (181, 1): 4,
726
+ # 2 subdiv - 'Inner' subpix have larger solid angle than 'outer' subpix
727
+ (178.5, -1.5): 5,
728
+ (178.5, -0.5): 6,
729
+ (178.5, 0.5): 7,
730
+ (178.5, 1.5): 8,
731
+ (179.5, -1.5): 9,
732
+ (179.5, -0.5): 10,
733
+ (179.5, 0.5): 11,
734
+ (179.5, 1.5): 12,
735
+ (180.5, -1.5): 12,
736
+ (180.5, -0.5): 14,
737
+ (180.5, 0.5): 15,
738
+ (180.5, 1.5): 16,
739
+ (181.5, -1.5): 17,
740
+ (181.5, -0.5): 18,
741
+ (181.5, 0.5): 19,
742
+ (181.5, 1.5): 20,
743
+ }
744
+ expected_mean_0_subdivisions = 0
745
+ expected_mean_1_subdivisions = 2.5
746
+ expected_mean_2_subdivisions = 12.5
747
+
748
+ def mock_ang2pix_fn(nside, theta, phi, nest=True, lonlat=False):
749
+ vals = []
750
+ for pix_num in range(len(theta)):
751
+ key = (theta[pix_num], phi[pix_num])
752
+ vals.append(pixel_dict.get(key, 0))
753
+ return np.array(vals)
754
+
755
+ hp_map = ena_maps.HealpixSkyMap(
756
+ nside=16,
757
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
758
+ nested=True,
759
+ )
760
+ hp_map.data_1d["counts"] = xr.DataArray(
761
+ data=[
762
+ np.arange(hp_map.num_points),
763
+ ],
764
+ dims=["epoch", "pixel"],
765
+ )
766
+
767
+ for num_subdiv, (expected_value, atol) in enumerate(
768
+ [
769
+ # The first subdivs have all the same solid angle
770
+ (expected_mean_0_subdivisions, 1e-9),
771
+ (expected_mean_1_subdivisions, 1e-9),
772
+ # Slight difference from not taking into account asym solid angle
773
+ (expected_mean_2_subdivisions, 0.1),
774
+ ]
775
+ ):
776
+ mock_ang2pix.reset_mock()
777
+ mock_ang2pix.side_effect = mock_ang2pix_fn
778
+ mean_value = (
779
+ hp_map.calculate_rect_pixel_value_from_healpix_map_n_subdivisions(
780
+ rect_pix_center_lon_lat=(180, 0),
781
+ rect_pix_spacing_deg=4,
782
+ value_array=hp_map.data_1d["counts"],
783
+ num_subdivisions=num_subdiv,
784
+ )
785
+ )
786
+ np.testing.assert_allclose(
787
+ mean_value,
788
+ expected_value,
789
+ atol=atol,
790
+ err_msg=f"Failed for num_subdivisions: {num_subdiv}",
791
+ )
792
+ hp_map.calculate_rect_pixel_value_from_healpix_map_n_subdivisions(
793
+ rect_pix_center_lon_lat=(180, 0),
794
+ rect_pix_spacing_deg=2,
795
+ value_array=hp_map.data_1d["counts"],
796
+ num_subdivisions=0,
797
+ )
798
+
799
+ @mock.patch(
800
+ "imap_processing.ena_maps.ena_maps.HealpixSkyMap.calculate_rect_pixel_value_from_healpix_map_n_subdivisions"
801
+ )
802
+ def test_get_rect_pixel_value_recursive_subdivs(
803
+ self,
804
+ mock_calculate_rect_pixel_value_from_healpix_map_n_subdivisions,
805
+ ):
806
+ """Test that the recursive subdivision works as expected with different rtol."""
807
+
808
+ # Mock the function to return a fixed value for a number of subdivisions
809
+ value_by_subdivisions = {
810
+ 0: 100.0,
811
+ 1: 110.0, # 10/110 = 0.09090909 change
812
+ 2: 105.0, # 5/105 = 0.04761905 change
813
+ 3: 107.0, # 2/107 = 0.01869159 change
814
+ 4: 107.5, # 0.5/107.5 = 0.00465116 change
815
+ 5: 107.51, # 0.01/107.51 = 0.00009301 change
816
+ 6: 107.5099, # 0.0001/107.5099 = 0.00000093 change
817
+ 7: 120, # Big change - but will stop because of MAX SUBDIVS
818
+ }
819
+ required_rtols = [
820
+ 0.1,
821
+ 0.05,
822
+ 0.02,
823
+ 0.005,
824
+ 0.0001,
825
+ 0.000001,
826
+ 1e-12,
827
+ ]
828
+
829
+ mock_calculate_rect_pixel_value_from_healpix_map_n_subdivisions.side_effect = (
830
+ lambda *args, **kwargs: np.array(
831
+ [
832
+ value_by_subdivisions[kwargs["num_subdivisions"]],
833
+ ]
834
+ )
835
+ )
836
+ hp_map = ena_maps.HealpixSkyMap(
837
+ nside=16,
838
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
839
+ )
840
+
841
+ # Test the recursive subdivision by setting different tolerances to get the
842
+ # expected number of subdivisions and resultant mean value.
843
+ for expected_subdiv_level in range(1, len(required_rtols)):
844
+ mean, depth = hp_map.get_rect_pixel_value_recursive_subdivs(
845
+ rect_pix_center_lon_lat=(180, 0),
846
+ rect_pix_spacing_deg=4,
847
+ value_array=[],
848
+ rtol=required_rtols[expected_subdiv_level - 1],
849
+ max_subdivision_depth=7,
850
+ )
851
+ assert depth == expected_subdiv_level
852
+ np.testing.assert_equal(
853
+ mean,
854
+ value_by_subdivisions[expected_subdiv_level],
855
+ err_msg=f"Failed for expected_subdiv_level: {expected_subdiv_level}",
856
+ )
857
+
858
+ def test_to_rectangular_skymap(
859
+ self,
860
+ ):
861
+ hp_map = ena_maps.HealpixSkyMap(
862
+ nside=64,
863
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
864
+ )
865
+
866
+ hp_map.data_1d["counts"] = xr.DataArray(
867
+ data=np.fromfunction(
868
+ lambda time, energy, pixel: 1000 + pixel * (10 * (energy + 1)),
869
+ shape=(1, 10, hp_map.num_points),
870
+ dtype=np.float32,
871
+ ),
872
+ dims=["epoch", "energy", "pixel"],
873
+ )
874
+ hp_map.data_1d["exposure_factor"] = xr.DataArray(
875
+ data=np.ones((10, hp_map.num_points)),
876
+ dims=["energy", "pixel"],
877
+ )
878
+ hp_map.data_1d["observation_date"] = xr.DataArray(
879
+ data=np.ones(hp_map.num_points),
880
+ dims=["pixel"],
881
+ )
882
+
883
+ rect_map, subdiv_depth_dict = hp_map.to_rectangular_skymap(
884
+ rect_spacing_deg=2,
885
+ value_keys=["counts", "exposure_factor", "observation_date"],
886
+ )
887
+
888
+ for value_key, subdiv_depth in subdiv_depth_dict.items():
889
+ # subdiv depth should always be between 1 and
890
+ # ena_maps.MAX_SUBDIV_RECURSION_DEPTH
891
+ np.testing.assert_array_less(
892
+ 0,
893
+ subdiv_depth,
894
+ err_msg=f"subdiv <1 for: {value_key}",
895
+ )
896
+ np.testing.assert_array_less(
897
+ subdiv_depth,
898
+ ena_maps.MAX_SUBDIV_RECURSION_DEPTH + 1,
899
+ err_msg=f"subdiv >MAX for: {value_key}",
900
+ )
901
+
902
+ # The min and max values of the rect and healpix maps should be close
903
+ # The min will have a larger relative tolerance because the variation
904
+ # in the test data is larger in comparison to the min value than to the max
905
+ np.testing.assert_allclose(
906
+ rect_map.data_1d[value_key].min(),
907
+ hp_map.data_1d[value_key].min(),
908
+ rtol=5e-2,
909
+ err_msg=f"Min values of {value_key} do not match",
910
+ )
911
+ np.testing.assert_allclose(
912
+ rect_map.data_1d[value_key].max(),
913
+ hp_map.data_1d[value_key].max(),
914
+ rtol=1e-3,
915
+ err_msg=f"Max values of {value_key} do not match",
916
+ )
917
+
918
+ # The dims of the rect map should be the same as the healpix map,
919
+ # except for the final pixel dimension
920
+ assert (
921
+ rect_map.data_1d[value_key].dims[:-1]
922
+ == hp_map.data_1d[value_key].dims[:-1]
923
+ )
924
+
641
925
 
642
926
  class TestIndexMatching:
643
927
  @pytest.fixture(autouse=True)
@@ -649,8 +933,8 @@ class TestIndexMatching:
649
933
  )
650
934
  self.rectangular_psets = [
651
935
  ena_maps.RectangularPointingSet(
936
+ l1c_product,
652
937
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
653
- l1c_dataset=l1c_product,
654
938
  )
655
939
  for l1c_product in self.rectangular_l1c_pset_products
656
940
  ]
@@ -670,7 +954,7 @@ class TestIndexMatching:
670
954
 
671
955
  # Mock a PSET, overriding the az/el points
672
956
  mock_pset_input_frame = ena_maps.RectangularPointingSet(
673
- l1c_dataset=self.rectangular_l1c_pset_products[0],
957
+ self.rectangular_l1c_pset_products[0],
674
958
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
675
959
  )
676
960
  manual_az_el_coords = np.array(
@@ -748,7 +1032,7 @@ class TestIndexMatching:
748
1032
 
749
1033
  # Make a PointingSet
750
1034
  mock_pset_input_frame = ena_maps.RectangularPointingSet(
751
- l1c_dataset=self.rectangular_l1c_pset_products[0],
1035
+ self.rectangular_l1c_pset_products[0],
752
1036
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
753
1037
  )
754
1038
 
@@ -787,7 +1071,7 @@ class TestIndexMatching:
787
1071
  self,
788
1072
  ):
789
1073
  mock_pset_input_frame = ena_maps.RectangularPointingSet(
790
- l1c_dataset=self.rectangular_l1c_pset_products[0],
1074
+ self.rectangular_l1c_pset_products[0],
791
1075
  spice_reference_frame=geometry.SpiceFrame.ECLIPJ2000,
792
1076
  )
793
1077
  # Until implemented, just change the tiling on a RectangularSkyMap
@@ -803,11 +1087,11 @@ class TestIndexMatching:
803
1087
 
804
1088
  def test_match_coords_to_indices_pset_to_pset_error(self):
805
1089
  mock_pset_input_frame = ena_maps.RectangularPointingSet(
806
- l1c_dataset=self.rectangular_l1c_pset_products[0],
1090
+ self.rectangular_l1c_pset_products[0],
807
1091
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
808
1092
  )
809
1093
  mock_pset_output_frame = ena_maps.RectangularPointingSet(
810
- l1c_dataset=self.rectangular_l1c_pset_products[1],
1094
+ self.rectangular_l1c_pset_products[1],
811
1095
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
812
1096
  )
813
1097
  with pytest.raises(
@@ -836,3 +1120,148 @@ class TestIndexMatching:
836
1120
  _ = ena_maps.match_coords_to_indices(
837
1121
  mock_rect_map_1, mock_rect_map_2, event_et=0
838
1122
  )
1123
+
1124
+
1125
+ class TestAbstractSkyMap:
1126
+ @pytest.mark.parametrize(
1127
+ "skymap_props_dict",
1128
+ [
1129
+ pytest.param(
1130
+ # HealpixSkyMap properties
1131
+ {
1132
+ "sky_tiling_type": "HEALPIX",
1133
+ "nside": 32,
1134
+ "nested": True,
1135
+ "spice_reference_frame": geometry.SpiceFrame.ECLIPJ2000.name,
1136
+ "values_to_push_project": ["foo", "bar"],
1137
+ },
1138
+ id="healpix-skymap",
1139
+ ),
1140
+ pytest.param(
1141
+ {
1142
+ "sky_tiling_type": "RECTANGULAR",
1143
+ "spacing_deg": 2,
1144
+ "spice_reference_frame": geometry.SpiceFrame.ECLIPJ2000.name,
1145
+ "values_to_pull_project": ["potato", "po-tah-to"],
1146
+ },
1147
+ id="rectangular-skymap",
1148
+ ),
1149
+ ],
1150
+ )
1151
+ def test_to_dict_and_from_dict(self, skymap_props_dict):
1152
+ """Test serialization to and from dictionary"""
1153
+ # Make a SkyMap from the original properties dict
1154
+ skymap_from_dict = ena_maps.AbstractSkyMap.from_dict(skymap_props_dict)
1155
+
1156
+ # Use the SkyMap to create a new properties dict
1157
+ dict_from_skymap = skymap_from_dict.to_dict()
1158
+
1159
+ assert (
1160
+ skymap_from_dict.spice_reference_frame
1161
+ == geometry.SpiceFrame[skymap_props_dict["spice_reference_frame"]]
1162
+ )
1163
+
1164
+ if skymap_props_dict["sky_tiling_type"] == "HEALPIX":
1165
+ assert isinstance(skymap_from_dict, ena_maps.HealpixSkyMap), (
1166
+ "from_dict should return a HealpixSkyMap object"
1167
+ )
1168
+ assert skymap_from_dict.nside == skymap_props_dict["nside"]
1169
+ assert skymap_from_dict.nested == skymap_props_dict["nested"]
1170
+ assert (
1171
+ skymap_from_dict.values_to_push_project
1172
+ == skymap_props_dict["values_to_push_project"]
1173
+ )
1174
+ assert skymap_from_dict.values_to_pull_project == []
1175
+
1176
+ elif skymap_props_dict["sky_tiling_type"] == "RECTANGULAR":
1177
+ assert isinstance(skymap_from_dict, ena_maps.RectangularSkyMap), (
1178
+ "from_dict should return a RectangularSkyMap object"
1179
+ )
1180
+ assert skymap_from_dict.spacing_deg == skymap_props_dict["spacing_deg"]
1181
+ assert skymap_from_dict.values_to_push_project == []
1182
+ assert (
1183
+ skymap_from_dict.values_to_pull_project
1184
+ == skymap_props_dict["values_to_pull_project"]
1185
+ )
1186
+
1187
+ for key in [
1188
+ "sky_tiling_type",
1189
+ "spice_reference_frame",
1190
+ "nside",
1191
+ "nested",
1192
+ "spacing_deg",
1193
+ ]:
1194
+ if key in skymap_props_dict:
1195
+ assert dict_from_skymap[key] == skymap_props_dict[key]
1196
+
1197
+ # Check that the dict from the SkyMap matches the original dict ONLY after
1198
+ # adding automatically added "values_to_push_project"/"values_to_pull_project"
1199
+ # key to the original dict
1200
+ assert dict_from_skymap != skymap_props_dict
1201
+
1202
+ # In the dicts passed in above, the HEALPIX one is missing the pull key
1203
+ # and the RECTANGULAR one is missing the push key
1204
+ if skymap_props_dict["sky_tiling_type"] == "HEALPIX":
1205
+ skymap_props_dict["values_to_pull_project"] = []
1206
+ elif skymap_props_dict["sky_tiling_type"] == "RECTANGULAR":
1207
+ skymap_props_dict["values_to_push_project"] = []
1208
+ assert dict_from_skymap == skymap_props_dict
1209
+
1210
+ # Change a value in the new dict and check that it is not equal to the original
1211
+ dict_from_skymap["spice_reference_frame"] = "SPACE!"
1212
+ assert (
1213
+ dict_from_skymap["spice_reference_frame"]
1214
+ != skymap_props_dict["spice_reference_frame"]
1215
+ )
1216
+
1217
+ def test_to_json_and_from_json(self):
1218
+ """Test serialization to and from JSON"""
1219
+ # Make a SkyMap from the original properties dict
1220
+ skymap_props_dict = {
1221
+ "sky_tiling_type": "HEALPIX",
1222
+ "nside": 32,
1223
+ "nested": True,
1224
+ "spice_reference_frame": geometry.SpiceFrame.ECLIPJ2000.name,
1225
+ "values_to_push_project": ["foo", "bar"],
1226
+ }
1227
+
1228
+ # Write a temporary json file with the properties dict
1229
+
1230
+ with tempfile.NamedTemporaryFile(
1231
+ delete=False, suffix=".json", mode="w"
1232
+ ) as temp_file:
1233
+ json.dump(skymap_props_dict, temp_file)
1234
+ temp_file_path_input = temp_file.name
1235
+
1236
+ # Read the json file and create a new SkyMap from it
1237
+ skymap_from_json = ena_maps.AbstractSkyMap.from_json(temp_file_path_input)
1238
+
1239
+ # Create json output from the SkyMap at a separate temporary file path
1240
+ temp_file_path_output = tempfile.NamedTemporaryFile(
1241
+ delete=False, suffix=".json", mode="w"
1242
+ ).name
1243
+ skymap_from_json.to_json(json_path=temp_file_path_output)
1244
+
1245
+ assert skymap_from_json.spice_reference_frame == geometry.SpiceFrame.ECLIPJ2000
1246
+ assert skymap_from_json.tiling_type is ena_maps.SkyTilingType.HEALPIX
1247
+ assert skymap_from_json.nside == 32
1248
+ assert skymap_from_json.nested is True
1249
+ assert skymap_from_json.values_to_push_project == ["foo", "bar"]
1250
+ assert skymap_from_json.values_to_pull_project == []
1251
+
1252
+ # Expect there to be a AttributeError when accessing a non-existent key
1253
+ with pytest.raises(AttributeError):
1254
+ _ = skymap_from_json.spacing_deg
1255
+
1256
+ # Check that the json output is the same as the original input ONLY
1257
+ # after adding automatically added
1258
+ # "values_to_push_project"/"values_to_pull_project" key to the original dict
1259
+ with open(temp_file_path_input) as f:
1260
+ original_json = json.load(f)
1261
+ with open(temp_file_path_output) as f:
1262
+ output_json = json.load(f)
1263
+ # The output json will have added an empty list for values_to_pull_project
1264
+ assert original_json != output_json
1265
+ # add the values_to_pull_project key to the original json
1266
+ original_json["values_to_pull_project"] = []
1267
+ assert original_json == output_json