imap-processing 0.11.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 (415) hide show
  1. imap_processing/__init__.py +11 -11
  2. imap_processing/_version.py +2 -2
  3. imap_processing/ccsds/ccsds_data.py +1 -2
  4. imap_processing/ccsds/excel_to_xtce.py +66 -18
  5. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +24 -40
  6. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +934 -42
  7. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +1846 -128
  8. imap_processing/cdf/config/imap_glows_global_cdf_attrs.yaml +0 -5
  9. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +10 -11
  10. imap_processing/cdf/config/imap_hi_variable_attrs.yaml +17 -19
  11. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +27 -14
  12. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +106 -116
  13. imap_processing/cdf/config/imap_hit_l1b_variable_attrs.yaml +120 -145
  14. imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml +14 -0
  15. imap_processing/cdf/config/imap_idex_global_cdf_attrs.yaml +25 -9
  16. imap_processing/cdf/config/imap_idex_l1a_variable_attrs.yaml +6 -4
  17. imap_processing/cdf/config/imap_idex_l1b_variable_attrs.yaml +3 -3
  18. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +0 -12
  19. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +1 -1
  20. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +23 -20
  21. imap_processing/cdf/config/imap_mag_l1a_variable_attrs.yaml +361 -0
  22. imap_processing/cdf/config/imap_mag_l1b_variable_attrs.yaml +160 -0
  23. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +160 -0
  24. imap_processing/cdf/config/imap_spacecraft_global_cdf_attrs.yaml +18 -0
  25. imap_processing/cdf/config/imap_spacecraft_variable_attrs.yaml +40 -0
  26. imap_processing/cdf/config/imap_swapi_global_cdf_attrs.yaml +1 -5
  27. imap_processing/cdf/config/imap_swapi_variable_attrs.yaml +22 -0
  28. imap_processing/cdf/config/imap_swe_global_cdf_attrs.yaml +12 -4
  29. imap_processing/cdf/config/imap_swe_l1a_variable_attrs.yaml +16 -2
  30. imap_processing/cdf/config/imap_swe_l1b_variable_attrs.yaml +64 -52
  31. imap_processing/cdf/config/imap_swe_l2_variable_attrs.yaml +71 -47
  32. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +180 -19
  33. imap_processing/cdf/config/imap_ultra_l1a_variable_attrs.yaml +5045 -41
  34. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +80 -17
  35. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +32 -57
  36. imap_processing/cdf/utils.py +52 -38
  37. imap_processing/cli.py +477 -233
  38. imap_processing/codice/codice_l1a.py +466 -131
  39. imap_processing/codice/codice_l1b.py +51 -152
  40. imap_processing/codice/constants.py +1360 -569
  41. imap_processing/codice/decompress.py +2 -6
  42. imap_processing/ena_maps/ena_maps.py +1103 -146
  43. imap_processing/ena_maps/utils/coordinates.py +19 -0
  44. imap_processing/ena_maps/utils/map_utils.py +14 -17
  45. imap_processing/ena_maps/utils/spatial_utils.py +55 -52
  46. imap_processing/glows/l1a/glows_l1a.py +28 -99
  47. imap_processing/glows/l1a/glows_l1a_data.py +2 -2
  48. imap_processing/glows/l1b/glows_l1b.py +1 -4
  49. imap_processing/glows/l1b/glows_l1b_data.py +1 -3
  50. imap_processing/glows/l2/glows_l2.py +2 -5
  51. imap_processing/hi/l1a/hi_l1a.py +54 -29
  52. imap_processing/hi/l1a/histogram.py +0 -1
  53. imap_processing/hi/l1a/science_direct_event.py +6 -8
  54. imap_processing/hi/l1b/hi_l1b.py +111 -82
  55. imap_processing/hi/l1c/hi_l1c.py +416 -32
  56. imap_processing/hi/utils.py +58 -12
  57. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-sector-dt0-factors_20250219_v002.csv +81 -0
  58. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt0-factors_20250219_v002.csv +205 -0
  59. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt1-factors_20250219_v002.csv +205 -0
  60. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt2-factors_20250219_v002.csv +205 -0
  61. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt3-factors_20250219_v002.csv +205 -0
  62. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-summed-dt0-factors_20250219_v002.csv +68 -0
  63. imap_processing/hit/hit_utils.py +235 -5
  64. imap_processing/hit/l0/constants.py +20 -11
  65. imap_processing/hit/l0/decom_hit.py +21 -5
  66. imap_processing/hit/l1a/hit_l1a.py +71 -75
  67. imap_processing/hit/l1b/constants.py +321 -0
  68. imap_processing/hit/l1b/hit_l1b.py +377 -67
  69. imap_processing/hit/l2/constants.py +318 -0
  70. imap_processing/hit/l2/hit_l2.py +723 -0
  71. imap_processing/hit/packet_definitions/hit_packet_definitions.xml +1323 -71
  72. imap_processing/ialirt/l0/mag_l0_ialirt_data.py +155 -0
  73. imap_processing/ialirt/l0/parse_mag.py +374 -0
  74. imap_processing/ialirt/l0/process_swapi.py +69 -0
  75. imap_processing/ialirt/l0/process_swe.py +548 -0
  76. imap_processing/ialirt/packet_definitions/ialirt.xml +216 -208
  77. imap_processing/ialirt/packet_definitions/ialirt_codicehi.xml +1 -1
  78. imap_processing/ialirt/packet_definitions/ialirt_codicelo.xml +1 -1
  79. imap_processing/ialirt/packet_definitions/ialirt_mag.xml +115 -0
  80. imap_processing/ialirt/packet_definitions/ialirt_swapi.xml +14 -14
  81. imap_processing/ialirt/utils/grouping.py +114 -0
  82. imap_processing/ialirt/utils/time.py +29 -0
  83. imap_processing/idex/atomic_masses.csv +22 -0
  84. imap_processing/idex/decode.py +2 -2
  85. imap_processing/idex/idex_constants.py +33 -0
  86. imap_processing/idex/idex_l0.py +22 -8
  87. imap_processing/idex/idex_l1a.py +81 -51
  88. imap_processing/idex/idex_l1b.py +13 -39
  89. imap_processing/idex/idex_l2a.py +823 -0
  90. imap_processing/idex/idex_l2b.py +120 -0
  91. imap_processing/idex/idex_variable_unpacking_and_eu_conversion.csv +11 -11
  92. imap_processing/idex/packet_definitions/idex_housekeeping_packet_definition.xml +9130 -0
  93. imap_processing/lo/l0/lo_science.py +7 -2
  94. imap_processing/lo/l1a/lo_l1a.py +1 -5
  95. imap_processing/lo/l1b/lo_l1b.py +702 -29
  96. imap_processing/lo/l1b/tof_conversions.py +11 -0
  97. imap_processing/lo/l1c/lo_l1c.py +1 -4
  98. imap_processing/mag/constants.py +51 -0
  99. imap_processing/mag/imap_mag_sdc_configuration_v001.py +8 -0
  100. imap_processing/mag/l0/decom_mag.py +10 -3
  101. imap_processing/mag/l1a/mag_l1a.py +23 -19
  102. imap_processing/mag/l1a/mag_l1a_data.py +35 -10
  103. imap_processing/mag/l1b/mag_l1b.py +259 -50
  104. imap_processing/mag/l1c/interpolation_methods.py +388 -0
  105. imap_processing/mag/l1c/mag_l1c.py +621 -17
  106. imap_processing/mag/l2/mag_l2.py +140 -0
  107. imap_processing/mag/l2/mag_l2_data.py +288 -0
  108. imap_processing/quality_flags.py +1 -0
  109. imap_processing/spacecraft/packet_definitions/scid_x252.xml +538 -0
  110. imap_processing/spacecraft/quaternions.py +121 -0
  111. imap_processing/spice/geometry.py +19 -22
  112. imap_processing/spice/kernels.py +0 -276
  113. imap_processing/spice/pointing_frame.py +257 -0
  114. imap_processing/spice/repoint.py +149 -0
  115. imap_processing/spice/spin.py +38 -33
  116. imap_processing/spice/time.py +24 -0
  117. imap_processing/swapi/l1/swapi_l1.py +20 -12
  118. imap_processing/swapi/l2/swapi_l2.py +116 -5
  119. imap_processing/swapi/swapi_utils.py +32 -0
  120. imap_processing/swe/l1a/swe_l1a.py +44 -12
  121. imap_processing/swe/l1a/swe_science.py +13 -13
  122. imap_processing/swe/l1b/swe_l1b.py +898 -23
  123. imap_processing/swe/l2/swe_l2.py +75 -136
  124. imap_processing/swe/packet_definitions/swe_packet_definition.xml +1121 -1
  125. imap_processing/swe/utils/swe_constants.py +64 -0
  126. imap_processing/swe/utils/swe_utils.py +85 -28
  127. imap_processing/tests/ccsds/test_data/expected_output.xml +40 -1
  128. imap_processing/tests/ccsds/test_excel_to_xtce.py +24 -21
  129. imap_processing/tests/cdf/test_data/imap_instrument2_global_cdf_attrs.yaml +0 -2
  130. imap_processing/tests/cdf/test_utils.py +14 -16
  131. imap_processing/tests/codice/conftest.py +44 -33
  132. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-counters-aggregated_20241110193700_v0.0.0.cdf +0 -0
  133. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-counters-singles_20241110193700_v0.0.0.cdf +0 -0
  134. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-ialirt_20241110193700_v0.0.0.cdf +0 -0
  135. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-omni_20241110193700_v0.0.0.cdf +0 -0
  136. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-pha_20241110193700_v0.0.0.cdf +0 -0
  137. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-priorities_20241110193700_v0.0.0.cdf +0 -0
  138. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-sectored_20241110193700_v0.0.0.cdf +0 -0
  139. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-counters-aggregated_20241110193700_v0.0.0.cdf +0 -0
  140. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-counters-singles_20241110193700_v0.0.0.cdf +0 -0
  141. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-ialirt_20241110193700_v0.0.0.cdf +0 -0
  142. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-angular_20241110193700_v0.0.0.cdf +0 -0
  143. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-priority_20241110193700_v0.0.0.cdf +0 -0
  144. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-species_20241110193700_v0.0.0.cdf +0 -0
  145. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-pha_20241110193700_v0.0.0.cdf +0 -0
  146. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-angular_20241110193700_v0.0.0.cdf +0 -0
  147. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-priority_20241110193700_v0.0.0.cdf +0 -0
  148. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-species_20241110193700_v0.0.0.cdf +0 -0
  149. imap_processing/tests/codice/test_codice_l1a.py +126 -53
  150. imap_processing/tests/codice/test_codice_l1b.py +6 -7
  151. imap_processing/tests/codice/test_decompress.py +4 -4
  152. imap_processing/tests/conftest.py +239 -27
  153. imap_processing/tests/ena_maps/conftest.py +51 -0
  154. imap_processing/tests/ena_maps/test_ena_maps.py +1068 -110
  155. imap_processing/tests/ena_maps/test_map_utils.py +66 -43
  156. imap_processing/tests/ena_maps/test_spatial_utils.py +17 -21
  157. imap_processing/tests/glows/conftest.py +10 -14
  158. imap_processing/tests/glows/test_glows_decom.py +4 -4
  159. imap_processing/tests/glows/test_glows_l1a_cdf.py +6 -27
  160. imap_processing/tests/glows/test_glows_l1a_data.py +6 -8
  161. imap_processing/tests/glows/test_glows_l1b.py +11 -11
  162. imap_processing/tests/glows/test_glows_l1b_data.py +5 -5
  163. imap_processing/tests/glows/test_glows_l2.py +2 -8
  164. imap_processing/tests/hi/conftest.py +1 -1
  165. imap_processing/tests/hi/data/l0/H45_diag_fee_20250208.bin +0 -0
  166. imap_processing/tests/hi/data/l0/H45_diag_fee_20250208_verify.csv +205 -0
  167. imap_processing/tests/hi/test_hi_l1b.py +22 -27
  168. imap_processing/tests/hi/test_hi_l1c.py +249 -18
  169. imap_processing/tests/hi/test_l1a.py +35 -7
  170. imap_processing/tests/hi/test_science_direct_event.py +3 -3
  171. imap_processing/tests/hi/test_utils.py +24 -2
  172. imap_processing/tests/hit/helpers/l1_validation.py +74 -73
  173. imap_processing/tests/hit/test_data/hskp_sample.ccsds +0 -0
  174. imap_processing/tests/hit/test_data/imap_hit_l0_raw_20100105_v001.pkts +0 -0
  175. imap_processing/tests/hit/test_decom_hit.py +5 -1
  176. imap_processing/tests/hit/test_hit_l1a.py +32 -36
  177. imap_processing/tests/hit/test_hit_l1b.py +300 -81
  178. imap_processing/tests/hit/test_hit_l2.py +716 -0
  179. imap_processing/tests/hit/test_hit_utils.py +184 -7
  180. imap_processing/tests/hit/validation_data/hit_l1b_standard_sample2_nsrl_v4_3decimals.csv +62 -62
  181. imap_processing/tests/hit/validation_data/hskp_sample_eu_3_6_2025.csv +89 -0
  182. imap_processing/tests/hit/validation_data/hskp_sample_raw.csv +89 -88
  183. imap_processing/tests/hit/validation_data/sci_sample_raw.csv +1 -1
  184. imap_processing/tests/ialirt/data/l0/461971383-404.bin +0 -0
  185. imap_processing/tests/ialirt/data/l0/461971384-405.bin +0 -0
  186. imap_processing/tests/ialirt/data/l0/461971385-406.bin +0 -0
  187. imap_processing/tests/ialirt/data/l0/461971386-407.bin +0 -0
  188. imap_processing/tests/ialirt/data/l0/461971387-408.bin +0 -0
  189. imap_processing/tests/ialirt/data/l0/461971388-409.bin +0 -0
  190. imap_processing/tests/ialirt/data/l0/461971389-410.bin +0 -0
  191. imap_processing/tests/ialirt/data/l0/461971390-411.bin +0 -0
  192. imap_processing/tests/ialirt/data/l0/461971391-412.bin +0 -0
  193. imap_processing/tests/ialirt/data/l0/sample_decoded_i-alirt_data.csv +383 -0
  194. imap_processing/tests/ialirt/unit/test_decom_ialirt.py +16 -81
  195. imap_processing/tests/ialirt/unit/test_grouping.py +81 -0
  196. imap_processing/tests/ialirt/unit/test_parse_mag.py +223 -0
  197. imap_processing/tests/ialirt/unit/test_process_codicehi.py +3 -3
  198. imap_processing/tests/ialirt/unit/test_process_codicelo.py +3 -10
  199. imap_processing/tests/ialirt/unit/test_process_ephemeris.py +4 -4
  200. imap_processing/tests/ialirt/unit/test_process_hit.py +3 -3
  201. imap_processing/tests/ialirt/unit/test_process_swapi.py +24 -16
  202. imap_processing/tests/ialirt/unit/test_process_swe.py +319 -6
  203. imap_processing/tests/ialirt/unit/test_time.py +16 -0
  204. imap_processing/tests/idex/conftest.py +127 -6
  205. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20231218_v001.pkts +0 -0
  206. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20241206_v001.pkts +0 -0
  207. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20250108_v001.pkts +0 -0
  208. imap_processing/tests/idex/test_data/impact_14_tof_high_data.txt +4508 -4508
  209. imap_processing/tests/idex/test_idex_l0.py +33 -11
  210. imap_processing/tests/idex/test_idex_l1a.py +92 -21
  211. imap_processing/tests/idex/test_idex_l1b.py +106 -27
  212. imap_processing/tests/idex/test_idex_l2a.py +399 -0
  213. imap_processing/tests/idex/test_idex_l2b.py +93 -0
  214. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_de_20241022_v002.cdf +0 -0
  215. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_spin_20241022_v002.cdf +0 -0
  216. imap_processing/tests/lo/test_lo_l1a.py +3 -3
  217. imap_processing/tests/lo/test_lo_l1b.py +515 -6
  218. imap_processing/tests/lo/test_lo_l1c.py +1 -1
  219. imap_processing/tests/lo/test_lo_science.py +7 -7
  220. imap_processing/tests/lo/test_star_sensor.py +1 -1
  221. imap_processing/tests/mag/conftest.py +120 -2
  222. imap_processing/tests/mag/test_mag_decom.py +5 -4
  223. imap_processing/tests/mag/test_mag_l1a.py +51 -7
  224. imap_processing/tests/mag/test_mag_l1b.py +40 -59
  225. imap_processing/tests/mag/test_mag_l1c.py +354 -19
  226. imap_processing/tests/mag/test_mag_l2.py +130 -0
  227. imap_processing/tests/mag/test_mag_validation.py +247 -26
  228. imap_processing/tests/mag/validation/L1b/T009/MAGScience-normal-(2,2)-8s-20250204-16h39.csv +17 -0
  229. imap_processing/tests/mag/validation/L1b/T009/mag-l1a-l1b-t009-magi-out.csv +16 -16
  230. imap_processing/tests/mag/validation/L1b/T009/mag-l1a-l1b-t009-mago-out.csv +16 -16
  231. imap_processing/tests/mag/validation/L1b/T010/MAGScience-normal-(2,2)-8s-20250206-12h05.csv +17 -0
  232. imap_processing/tests/mag/validation/L1b/T011/MAGScience-normal-(2,2)-8s-20250204-16h08.csv +17 -0
  233. imap_processing/tests/mag/validation/L1b/T011/mag-l1a-l1b-t011-magi-out.csv +16 -16
  234. imap_processing/tests/mag/validation/L1b/T011/mag-l1a-l1b-t011-mago-out.csv +16 -16
  235. imap_processing/tests/mag/validation/L1b/T012/MAGScience-normal-(2,2)-8s-20250204-16h08.csv +17 -0
  236. imap_processing/tests/mag/validation/L1b/T012/data.bin +0 -0
  237. imap_processing/tests/mag/validation/L1b/T012/field_like_all_ranges.txt +19200 -0
  238. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-cal.cdf +0 -0
  239. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-in.csv +17 -0
  240. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-magi-out.csv +17 -0
  241. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-mago-out.csv +17 -0
  242. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-magi-normal-in.csv +1217 -0
  243. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-magi-normal-out.csv +1857 -0
  244. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-mago-normal-in.csv +1217 -0
  245. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-mago-normal-out.csv +1857 -0
  246. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-magi-normal-in.csv +1217 -0
  247. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-magi-normal-out.csv +1793 -0
  248. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-mago-normal-in.csv +1217 -0
  249. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-mago-normal-out.csv +1793 -0
  250. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-burst-in.csv +2561 -0
  251. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-normal-in.csv +961 -0
  252. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-normal-out.csv +1539 -0
  253. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-mago-normal-in.csv +1921 -0
  254. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-mago-normal-out.csv +2499 -0
  255. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-magi-normal-in.csv +865 -0
  256. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-magi-normal-out.csv +1196 -0
  257. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-mago-normal-in.csv +1729 -0
  258. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-mago-normal-out.csv +3053 -0
  259. imap_processing/tests/mag/validation/L2/imap_mag_l1b_norm-mago_20251017_v002.cdf +0 -0
  260. imap_processing/tests/mag/validation/calibration/imap_mag_l1b-calibration_20240229_v001.cdf +0 -0
  261. imap_processing/tests/mag/validation/calibration/imap_mag_l2-calibration-matrices_20251017_v004.cdf +0 -0
  262. imap_processing/tests/mag/validation/calibration/imap_mag_l2-offsets-norm_20251017_20251017_v001.cdf +0 -0
  263. imap_processing/tests/spacecraft/data/SSR_2024_190_20_08_12_0483851794_2_DA_apid0594_1packet.pkts +0 -0
  264. imap_processing/tests/spacecraft/test_quaternions.py +71 -0
  265. imap_processing/tests/spice/test_data/fake_repoint_data.csv +5 -0
  266. imap_processing/tests/spice/test_data/fake_spin_data.csv +11 -11
  267. imap_processing/tests/spice/test_geometry.py +9 -12
  268. imap_processing/tests/spice/test_kernels.py +1 -200
  269. imap_processing/tests/spice/test_pointing_frame.py +185 -0
  270. imap_processing/tests/spice/test_repoint.py +121 -0
  271. imap_processing/tests/spice/test_spin.py +50 -9
  272. imap_processing/tests/spice/test_time.py +14 -0
  273. imap_processing/tests/swapi/lut/imap_swapi_esa-unit-conversion_20250211_v000.csv +73 -0
  274. imap_processing/tests/swapi/lut/imap_swapi_lut-notes_20250211_v000.csv +1025 -0
  275. imap_processing/tests/swapi/test_swapi_l1.py +13 -11
  276. imap_processing/tests/swapi/test_swapi_l2.py +180 -8
  277. imap_processing/tests/swe/l0_data/2024051010_SWE_HK_packet.bin +0 -0
  278. imap_processing/tests/swe/l0_data/2024051011_SWE_CEM_RAW_packet.bin +0 -0
  279. imap_processing/tests/swe/l0_validation_data/idle_export_eu.SWE_APP_HK_20240510_092742.csv +49 -0
  280. imap_processing/tests/swe/l0_validation_data/idle_export_eu.SWE_CEM_RAW_20240510_092742.csv +593 -0
  281. imap_processing/tests/swe/lut/checker-board-indices.csv +24 -0
  282. imap_processing/tests/swe/lut/imap_swe_esa-lut_20250301_v000.csv +385 -0
  283. imap_processing/tests/swe/lut/imap_swe_l1b-in-flight-cal_20240510_20260716_v000.csv +3 -0
  284. imap_processing/tests/swe/test_swe_l1a.py +20 -2
  285. imap_processing/tests/swe/test_swe_l1a_cem_raw.py +52 -0
  286. imap_processing/tests/swe/test_swe_l1a_hk.py +68 -0
  287. imap_processing/tests/swe/test_swe_l1a_science.py +3 -3
  288. imap_processing/tests/swe/test_swe_l1b.py +162 -24
  289. imap_processing/tests/swe/test_swe_l2.py +153 -91
  290. imap_processing/tests/test_cli.py +171 -88
  291. imap_processing/tests/test_utils.py +140 -17
  292. imap_processing/tests/ultra/data/l0/FM45_UltraFM45_Functional_2024-01-22T0105_20240122T010548.CCSDS +0 -0
  293. imap_processing/tests/ultra/data/l0/ultra45_raw_sc_ultraimgrates_20220530_00.csv +164 -0
  294. imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_ultrarawimg_withFSWcalcs_FM45_40P_Phi28p5_BeamCal_LinearScan_phi2850_theta-000_20240207T102740.csv +3243 -3243
  295. imap_processing/tests/ultra/data/mock_data.py +369 -0
  296. imap_processing/tests/ultra/unit/conftest.py +115 -89
  297. imap_processing/tests/ultra/unit/test_badtimes.py +4 -4
  298. imap_processing/tests/ultra/unit/test_cullingmask.py +8 -6
  299. imap_processing/tests/ultra/unit/test_de.py +14 -13
  300. imap_processing/tests/ultra/unit/test_decom_apid_880.py +27 -76
  301. imap_processing/tests/ultra/unit/test_decom_apid_881.py +54 -11
  302. imap_processing/tests/ultra/unit/test_decom_apid_883.py +12 -10
  303. imap_processing/tests/ultra/unit/test_decom_apid_896.py +202 -55
  304. imap_processing/tests/ultra/unit/test_lookup_utils.py +23 -1
  305. imap_processing/tests/ultra/unit/test_spacecraft_pset.py +77 -0
  306. imap_processing/tests/ultra/unit/test_ultra_l1a.py +98 -305
  307. imap_processing/tests/ultra/unit/test_ultra_l1b.py +60 -14
  308. imap_processing/tests/ultra/unit/test_ultra_l1b_annotated.py +2 -2
  309. imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py +26 -27
  310. imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +239 -70
  311. imap_processing/tests/ultra/unit/test_ultra_l1c.py +5 -5
  312. imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +114 -83
  313. imap_processing/tests/ultra/unit/test_ultra_l2.py +230 -0
  314. imap_processing/ultra/constants.py +1 -1
  315. imap_processing/ultra/l0/decom_tools.py +27 -39
  316. imap_processing/ultra/l0/decom_ultra.py +168 -204
  317. imap_processing/ultra/l0/ultra_utils.py +152 -136
  318. imap_processing/ultra/l1a/ultra_l1a.py +55 -271
  319. imap_processing/ultra/l1b/badtimes.py +1 -4
  320. imap_processing/ultra/l1b/cullingmask.py +2 -6
  321. imap_processing/ultra/l1b/de.py +116 -57
  322. imap_processing/ultra/l1b/extendedspin.py +20 -18
  323. imap_processing/ultra/l1b/lookup_utils.py +72 -9
  324. imap_processing/ultra/l1b/ultra_l1b.py +36 -16
  325. imap_processing/ultra/l1b/ultra_l1b_culling.py +66 -30
  326. imap_processing/ultra/l1b/ultra_l1b_extended.py +297 -94
  327. imap_processing/ultra/l1c/histogram.py +2 -6
  328. imap_processing/ultra/l1c/spacecraft_pset.py +84 -0
  329. imap_processing/ultra/l1c/ultra_l1c.py +8 -9
  330. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +206 -108
  331. imap_processing/ultra/l2/ultra_l2.py +299 -0
  332. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +526 -0
  333. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +526 -0
  334. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +526 -0
  335. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +526 -0
  336. imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +2 -2
  337. imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +2 -0
  338. imap_processing/ultra/packet_definitions/README.md +38 -0
  339. imap_processing/ultra/packet_definitions/ULTRA_SCI_COMBINED.xml +15302 -482
  340. imap_processing/ultra/utils/ultra_l1_utils.py +31 -12
  341. imap_processing/utils.py +69 -29
  342. {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/METADATA +10 -6
  343. imap_processing-0.13.0.dist-info/RECORD +578 -0
  344. imap_processing/cdf/config/imap_mag_l1_variable_attrs.yaml +0 -237
  345. imap_processing/hi/l1a/housekeeping.py +0 -27
  346. imap_processing/hi/l1b/hi_eng_unit_convert_table.csv +0 -154
  347. imap_processing/swe/l1b/swe_esa_lookup_table.csv +0 -1441
  348. imap_processing/swe/l1b/swe_l1b_science.py +0 -652
  349. imap_processing/tests/codice/data/imap_codice_l1a_hi-counters-aggregated_20240429_v001.cdf +0 -0
  350. imap_processing/tests/codice/data/imap_codice_l1a_hi-counters-singles_20240429_v001.cdf +0 -0
  351. imap_processing/tests/codice/data/imap_codice_l1a_hi-omni_20240429_v001.cdf +0 -0
  352. imap_processing/tests/codice/data/imap_codice_l1a_hi-sectored_20240429_v001.cdf +0 -0
  353. imap_processing/tests/codice/data/imap_codice_l1a_hskp_20100101_v001.cdf +0 -0
  354. imap_processing/tests/codice/data/imap_codice_l1a_lo-counters-aggregated_20240429_v001.cdf +0 -0
  355. imap_processing/tests/codice/data/imap_codice_l1a_lo-counters-singles_20240429_v001.cdf +0 -0
  356. imap_processing/tests/codice/data/imap_codice_l1a_lo-nsw-angular_20240429_v001.cdf +0 -0
  357. imap_processing/tests/codice/data/imap_codice_l1a_lo-nsw-priority_20240429_v001.cdf +0 -0
  358. imap_processing/tests/codice/data/imap_codice_l1a_lo-nsw-species_20240429_v001.cdf +0 -0
  359. imap_processing/tests/codice/data/imap_codice_l1a_lo-sw-angular_20240429_v001.cdf +0 -0
  360. imap_processing/tests/codice/data/imap_codice_l1a_lo-sw-priority_20240429_v001.cdf +0 -0
  361. imap_processing/tests/codice/data/imap_codice_l1a_lo-sw-species_20240429_v001.cdf +0 -0
  362. imap_processing/tests/codice/data/imap_codice_l1b_hi-counters-aggregated_20240429_v001.cdf +0 -0
  363. imap_processing/tests/codice/data/imap_codice_l1b_hi-counters-singles_20240429_v001.cdf +0 -0
  364. imap_processing/tests/codice/data/imap_codice_l1b_hi-omni_20240429_v001.cdf +0 -0
  365. imap_processing/tests/codice/data/imap_codice_l1b_hi-sectored_20240429_v001.cdf +0 -0
  366. imap_processing/tests/codice/data/imap_codice_l1b_hskp_20100101_v001.cdf +0 -0
  367. imap_processing/tests/codice/data/imap_codice_l1b_lo-counters-aggregated_20240429_v001.cdf +0 -0
  368. imap_processing/tests/codice/data/imap_codice_l1b_lo-counters-singles_20240429_v001.cdf +0 -0
  369. imap_processing/tests/codice/data/imap_codice_l1b_lo-nsw-angular_20240429_v001.cdf +0 -0
  370. imap_processing/tests/codice/data/imap_codice_l1b_lo-nsw-priority_20240429_v001.cdf +0 -0
  371. imap_processing/tests/codice/data/imap_codice_l1b_lo-nsw-species_20240429_v001.cdf +0 -0
  372. imap_processing/tests/codice/data/imap_codice_l1b_lo-sw-angular_20240429_v001.cdf +0 -0
  373. imap_processing/tests/codice/data/imap_codice_l1b_lo-sw-priority_20240429_v001.cdf +0 -0
  374. imap_processing/tests/codice/data/imap_codice_l1b_lo-sw-species_20240429_v001.cdf +0 -0
  375. imap_processing/tests/hi/data/l1/imap_hi_l1b_45sensor-de_20250415_v999.cdf +0 -0
  376. imap_processing/tests/hit/PREFLIGHT_raw_record_2023_256_15_59_04_apid1251.pkts +0 -0
  377. imap_processing/tests/hit/PREFLIGHT_raw_record_2023_256_15_59_04_apid1252.pkts +0 -0
  378. imap_processing/tests/hit/validation_data/hskp_sample_eu.csv +0 -89
  379. imap_processing/tests/hit/validation_data/sci_sample_raw1.csv +0 -29
  380. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20231214_v001.pkts +0 -0
  381. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_de_20100101_v001.cdf +0 -0
  382. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_spin_20100101_v001.cdf +0 -0
  383. imap_processing/tests/swe/test_swe_l1b_science.py +0 -84
  384. imap_processing/tests/ultra/test_data/mock_data.py +0 -161
  385. imap_processing/ultra/l1c/pset.py +0 -40
  386. imap_processing/ultra/lookup_tables/dps_sensitivity45.cdf +0 -0
  387. imap_processing-0.11.0.dist-info/RECORD +0 -488
  388. /imap_processing/idex/packet_definitions/{idex_packet_definition.xml → idex_science_packet_definition.xml} +0 -0
  389. /imap_processing/tests/ialirt/{test_data → data}/l0/20240827095047_SWE_IALIRT_packet.bin +0 -0
  390. /imap_processing/tests/ialirt/{test_data → data}/l0/BinLog CCSDS_FRAG_TLM_20240826_152323Z_IALIRT_data_for_SDC.bin +0 -0
  391. /imap_processing/tests/ialirt/{test_data → data}/l0/IALiRT Raw Packet Telemetry.txt +0 -0
  392. /imap_processing/tests/ialirt/{test_data → data}/l0/apid01152.tlm +0 -0
  393. /imap_processing/tests/ialirt/{test_data → data}/l0/eu_SWP_IAL_20240826_152033.csv +0 -0
  394. /imap_processing/tests/ialirt/{test_data → data}/l0/hi_fsw_view_1_ccsds.bin +0 -0
  395. /imap_processing/tests/ialirt/{test_data → data}/l0/hit_ialirt_sample.ccsds +0 -0
  396. /imap_processing/tests/ialirt/{test_data → data}/l0/hit_ialirt_sample.csv +0 -0
  397. /imap_processing/tests/ialirt/{test_data → data}/l0/idle_export_eu.SWE_IALIRT_20240827_093852.csv +0 -0
  398. /imap_processing/tests/ialirt/{test_data → data}/l0/imap_codice_l1a_hi-ialirt_20240523200000_v0.0.0.cdf +0 -0
  399. /imap_processing/tests/ialirt/{test_data → data}/l0/imap_codice_l1a_lo-ialirt_20241110193700_v0.0.0.cdf +0 -0
  400. /imap_processing/{mag/l1b → tests/spacecraft}/__init__.py +0 -0
  401. /imap_processing/{swe/l1b/engineering_unit_convert_table.csv → tests/swe/lut/imap_swe_eu-conversion_20240510_v000.csv} +0 -0
  402. /imap_processing/tests/ultra/{test_data → data}/l0/FM45_40P_Phi28p5_BeamCal_LinearScan_phi28.50_theta-0.00_20240207T102740.CCSDS +0 -0
  403. /imap_processing/tests/ultra/{test_data → data}/l0/FM45_7P_Phi0.0_BeamCal_LinearScan_phi0.04_theta-0.01_20230821T121304.CCSDS +0 -0
  404. /imap_processing/tests/ultra/{test_data → data}/l0/FM45_TV_Cycle6_Hot_Ops_Front212_20240124T063837.CCSDS +0 -0
  405. /imap_processing/tests/ultra/{test_data → data}/l0/Ultra45_EM_SwRI_Cal_Run7_ThetaScan_20220530T225054.CCSDS +0 -0
  406. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_auxdata_Ultra45_EM_SwRI_Cal_Run7_ThetaScan_20220530T225054.csv +0 -0
  407. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_enaphxtofhangimg_FM45_TV_Cycle6_Hot_Ops_Front212_20240124T063837.csv +0 -0
  408. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_ultraimgrates_Ultra45_EM_SwRI_Cal_Run7_ThetaScan_20220530T225054.csv +0 -0
  409. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_ultrarawimgevent_FM45_7P_Phi00_BeamCal_LinearScan_phi004_theta-001_20230821T121304.csv +0 -0
  410. /imap_processing/tests/ultra/{test_data → data}/l1/dps_exposure_helio_45_E1.cdf +0 -0
  411. /imap_processing/tests/ultra/{test_data → data}/l1/dps_exposure_helio_45_E12.cdf +0 -0
  412. /imap_processing/tests/ultra/{test_data → data}/l1/dps_exposure_helio_45_E24.cdf +0 -0
  413. {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/LICENSE +0 -0
  414. {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/WHEEL +0 -0
  415. {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/entry_points.txt +0 -0
@@ -2,125 +2,200 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+ import tempfile
7
+ from copy import deepcopy
8
+ from pathlib import Path
5
9
  from unittest import mock
6
10
 
11
+ import astropy_healpix.healpy as hp
7
12
  import numpy as np
8
13
  import pytest
9
14
  import xarray as xr
10
15
 
16
+ from imap_processing.cdf.utils import load_cdf, write_cdf
11
17
  from imap_processing.ena_maps import ena_maps
18
+ from imap_processing.ena_maps.utils import spatial_utils
19
+ from imap_processing.ena_maps.utils.coordinates import CoordNames
12
20
  from imap_processing.spice import geometry
13
- from imap_processing.tests.ultra.test_data.mock_data import mock_l1c_pset_product
14
21
 
15
22
 
16
- @pytest.fixture()
17
- def l1c_pset_products():
18
- """Make fake L1C Ultra PSET products for testing"""
19
- l1c_spatial_bin_spacing_deg = 10
23
+ @pytest.fixture(autouse=True, scope="module")
24
+ def setup_all_pset_products(ultra_l1c_pset_datasets, rectangular_l1c_pset_datasets):
25
+ """
26
+ Setup fixture data once for all tests.
27
+
28
+ This is relatively computationally intensive for the high resolution PSETs,
29
+ so we use a module-level fixture to avoid repeating the setup code. However,
30
+ some tests need to modify the PSETs, so we use a function-level fixture to
31
+ make a deepcopy of the PSETs for each test function.
32
+ """
33
+ hp_ultra_nside = ultra_l1c_pset_datasets["nside"]
34
+ hp_ultra_l1c_pset_products = ultra_l1c_pset_datasets["products"]
35
+ rect_spacing = rectangular_l1c_pset_datasets["spacing"]
36
+ rect_rectangular_l1c_pset_products = rectangular_l1c_pset_datasets["products"]
20
37
  return {
21
- "spacing": l1c_spatial_bin_spacing_deg,
22
- "products": [
23
- mock_l1c_pset_product(
24
- spacing_deg=l1c_spatial_bin_spacing_deg,
25
- stripe_center_lon=mid_longitude,
26
- timestr=f"2025-09-{i + 1:02d}T12:00:00",
27
- head=("45" if (i % 2 == 0) else "90"),
28
- )
29
- for i, mid_longitude in enumerate(
30
- np.arange(
31
- 0,
32
- 360,
33
- 45,
34
- )
35
- )
36
- ],
38
+ "hp_ultra_nside": hp_ultra_nside,
39
+ "hp_ultra_l1c_pset_products": hp_ultra_l1c_pset_products,
40
+ "rect_spacing": rect_spacing,
41
+ "rect_rectangular_l1c_pset_products": rect_rectangular_l1c_pset_products,
37
42
  }
38
43
 
39
44
 
40
45
  class TestUltraPointingSet:
41
46
  @pytest.fixture(autouse=True)
42
- def _setup_ultra_l1c_pset_products(self, l1c_pset_products):
47
+ def _setup_ultra_l1c_pset_products(self, setup_all_pset_products):
43
48
  """Setup fixture data as class attributes"""
44
- self.l1c_spatial_bin_spacing_deg = l1c_pset_products["spacing"]
45
- self.l1c_pset_products = l1c_pset_products["products"]
49
+ self.nside = setup_all_pset_products["hp_ultra_nside"]
50
+ self.l1c_pset_products = deepcopy(
51
+ setup_all_pset_products["hp_ultra_l1c_pset_products"]
52
+ )
46
53
 
47
54
  @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
48
55
  def test_instantiate(self):
49
56
  """Test instantiation of UltraPointingSet"""
50
57
  ultra_psets = [
51
58
  ena_maps.UltraPointingSet(
59
+ l1c_product,
52
60
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
53
- l1c_dataset=l1c_product,
54
61
  )
55
62
  for l1c_product in self.l1c_pset_products
56
63
  ]
57
64
 
58
65
  for ultra_pset in ultra_psets:
59
- # Check tiling is rectangular
60
- assert ultra_pset.tiling_type == ena_maps.SkyTilingType.RECTANGULAR
66
+ # Check tiling is HEALPix
67
+ assert ultra_pset.tiling_type is ena_maps.SkyTilingType.HEALPIX
61
68
 
62
69
  # Check that the reference frame is correctly set
63
- assert ultra_pset.spice_reference_frame == geometry.SpiceFrame.IMAP_DPS
70
+ assert ultra_pset.spice_reference_frame is geometry.SpiceFrame.IMAP_DPS
64
71
 
65
72
  # Check the number of points is (360/0.5) * (180/0.5)
66
73
  np.testing.assert_equal(
67
74
  ultra_pset.num_points,
68
- int(360 * 180 / (self.l1c_spatial_bin_spacing_deg**2)),
75
+ hp.nside2npix(self.nside),
69
76
  )
70
77
 
71
78
  # Check the repr exists
72
79
  assert "UltraPointingSet" in repr(ultra_pset)
73
80
 
81
+ # Checks for the property methods:
82
+ # Check that the unwrapped_dims_dict is as expected
83
+ assert ultra_pset.unwrapped_dims_dict["counts"] == (
84
+ "epoch",
85
+ "energy_bin_geometric_mean",
86
+ "pixel",
87
+ )
88
+ # Check the non_spatial_coords are as expected
89
+ assert tuple(ultra_pset.non_spatial_coords.keys()) == (
90
+ "epoch",
91
+ "energy_bin_geometric_mean",
92
+ )
93
+
74
94
  @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
75
- def test_uneven_spacing_raises_error(self):
76
- """Test that uneven spacing in az/el raises ValueError"""
95
+ def test_init_cdf(
96
+ self,
97
+ ):
98
+ ultra_pset = self.l1c_pset_products[0]
77
99
 
78
- # Create dataset with uneven az spacing
79
- uneven_az_dataset = xr.Dataset()
80
- uneven_az_dataset["epoch"] = 1
81
- uneven_az_dataset["azimuth_bin_center"] = np.array([0, 5, 15, 20, 30])
82
- uneven_az_dataset["elevation_bin_center"] = np.arange(5)
100
+ cdf_filepath = write_cdf(ultra_pset, istp=False)
83
101
 
84
- with pytest.raises(ValueError, match="Azimuth bin spacing is not uniform"):
85
- ena_maps.UltraPointingSet(
86
- spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
87
- l1c_dataset=uneven_az_dataset,
88
- )
102
+ ultra_pset_from_dataset = ena_maps.UltraPointingSet(ultra_pset)
89
103
 
90
- uneven_az_dataset["azimuth_bin_center"] = np.arange(5)
91
- uneven_az_dataset["elevation_bin_center"] = np.array([0, 5, 15, 20, 30])
104
+ ultra_pset_from_str = ena_maps.UltraPointingSet(cdf_filepath)
105
+ ultra_pset_from_path = ena_maps.UltraPointingSet(Path(cdf_filepath))
92
106
 
93
- with pytest.raises(ValueError, match="Elevation bin spacing is not uniform"):
94
- ena_maps.UltraPointingSet(
95
- spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
96
- l1c_dataset=uneven_az_dataset,
97
- )
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
+ )
98
112
 
99
- # Even but not the same spacing between az and el
100
- uneven_az_dataset["azimuth_bin_center"] = np.arange(5)
101
- uneven_az_dataset["elevation_bin_center"] = np.arange(5) * 2
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
+ )
102
118
 
103
- with pytest.raises(
104
- ValueError, match="Azimuth and elevation bin spacing do not match:"
105
- ):
119
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
120
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
121
+ def test_different_spacing_raises_error(self):
122
+ """Test that different spaced az/el from the L1C dataset raises ValueError"""
123
+
124
+ ultra_pset_ds = self.l1c_pset_products[0]
125
+ # Modify the dataset to have different spacing
126
+ ultra_pset_ds[CoordNames.ELEVATION_L1C.value].values = np.arange(
127
+ ultra_pset_ds[CoordNames.ELEVATION_L1C.value].size
128
+ )
129
+
130
+ with pytest.raises(ValueError, match="do not match"):
106
131
  ena_maps.UltraPointingSet(
132
+ ultra_pset_ds,
107
133
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
108
- l1c_dataset=uneven_az_dataset,
109
134
  )
110
135
 
111
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
+
112
170
  class TestRectangularSkyMap:
113
171
  @pytest.fixture(autouse=True)
114
- def _setup_ultra_l1c_pset_products(self, l1c_pset_products):
172
+ def _setup_ultra_l1c_pset_products(self, setup_all_pset_products):
115
173
  """Setup fixture data as class attributes"""
116
- self.l1c_spatial_bin_spacing_deg = l1c_pset_products["spacing"]
117
- self.l1c_pset_products = l1c_pset_products["products"]
174
+ self.ultra_l1c_nside = setup_all_pset_products["hp_ultra_nside"]
175
+ self.ultra_l1c_pset_products = deepcopy(
176
+ setup_all_pset_products["hp_ultra_l1c_pset_products"]
177
+ )
118
178
  self.ultra_psets = [
119
179
  ena_maps.UltraPointingSet(
180
+ l1c_product,
120
181
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
121
- l1c_dataset=l1c_product,
122
182
  )
123
- for l1c_product in self.l1c_pset_products
183
+ for l1c_product in self.ultra_l1c_pset_products
184
+ ]
185
+
186
+ @pytest.fixture(autouse=True)
187
+ def _setup_rectangular_l1c_pset_products(self, setup_all_pset_products):
188
+ """Setup fixture data as class attributes"""
189
+ self.rectangular_l1c_spacing_deg = setup_all_pset_products["rect_spacing"]
190
+ self.rectangular_l1c_pset_products = deepcopy(
191
+ setup_all_pset_products["rect_rectangular_l1c_pset_products"]
192
+ )
193
+ self.rectangular_psets = [
194
+ ena_maps.RectangularPointingSet(
195
+ l1c_product,
196
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
197
+ )
198
+ for l1c_product in self.rectangular_l1c_pset_products
124
199
  ]
125
200
 
126
201
  def test_instantiate(self):
@@ -130,8 +205,9 @@ class TestRectangularSkyMap:
130
205
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
131
206
  )
132
207
 
133
- # Check that the map is empty
134
- assert rm.data_dict == {}
208
+ # Check that the map data is an empty xarray Dataset
209
+ assert isinstance(rm.data_1d, xr.Dataset)
210
+ assert rm.data_1d.data_vars == {}
135
211
 
136
212
  # Check that the reference frame is correctly set
137
213
  assert rm.spice_reference_frame == geometry.SpiceFrame.ECLIPJ2000
@@ -142,11 +218,80 @@ class TestRectangularSkyMap:
142
218
  # Check the repr exists
143
219
  assert "RectangularSkyMap" in repr(rm)
144
220
 
221
+ np.testing.assert_array_equal(
222
+ rm.binning_grid_shape, (360 / rm.spacing_deg, 180 / rm.spacing_deg)
223
+ )
224
+
145
225
  @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
146
226
  @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
147
- def test_project_pset_values_to_map_push_method(self, mock_frame_transform_az_el):
227
+ def test_project_healpix_pset_values_to_map_push_method(
228
+ self, mock_frame_transform_az_el
229
+ ):
230
+ """
231
+ Test projection of Healpix tiled PSET values to RectMap w "push" index matching.
232
+
233
+ If frame_transform_az_el is mocked to return the az and el unchanged,
234
+ then the map should have the same total counts in each energy bin
235
+ as the PSETs, summed.
236
+ """
237
+ index_matching_method = ena_maps.IndexMatchMethod.PUSH
238
+
239
+ # Mock frame_transform to return the az and el unchanged
240
+ mock_frame_transform_az_el.side_effect = (
241
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
242
+ )
243
+
244
+ rectangular_map = ena_maps.RectangularSkyMap(
245
+ spacing_deg=2,
246
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
247
+ )
248
+
249
+ # Project each PSET's values to the map (push method)
250
+ for ultra_pset in self.ultra_psets:
251
+ rectangular_map.project_pset_values_to_map(
252
+ ultra_pset,
253
+ value_keys=["counts", "exposure_factor"],
254
+ index_match_method=index_matching_method,
255
+ )
256
+
257
+ # Check that the map has been updated
258
+ assert "counts" in rectangular_map.data_1d.data_vars
259
+
260
+ # Check that the map has the same values as the PSETs, summed
261
+ simple_summed_pset_counts_by_energy = np.zeros(
262
+ shape=(
263
+ self.ultra_l1c_pset_products[0]["counts"].sizes[
264
+ CoordNames.ENERGY_ULTRA.value
265
+ ],
266
+ )
267
+ )
268
+ for pset in self.ultra_l1c_pset_products:
269
+ simple_summed_pset_counts_by_energy += pset["counts"].sum(
270
+ dim=[
271
+ d for d in pset["counts"].dims if d != CoordNames.ENERGY_ULTRA.value
272
+ ]
273
+ )
274
+
275
+ rmap_counts_per_energy_bin = rectangular_map.data_1d["counts"].sum(
276
+ dim=[
277
+ d
278
+ for d in rectangular_map.data_1d["counts"].dims
279
+ if d != CoordNames.ENERGY_ULTRA.value
280
+ ]
281
+ )
282
+
283
+ np.testing.assert_array_equal(
284
+ rmap_counts_per_energy_bin,
285
+ simple_summed_pset_counts_by_energy,
286
+ )
287
+
288
+ @pytest.mark.usefixtures("_setup_rectangular_l1c_pset_products")
289
+ @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
290
+ def test_project_rect_pset_values_to_map_push_method(
291
+ self, mock_frame_transform_az_el
292
+ ):
148
293
  """
149
- Test projection of PSET values to Rect. Map w "push" index matching method.
294
+ Test projection of Rect PSET values to Rect Map w "push" index matching method.
150
295
 
151
296
  If frame_transform_az_el is mocked to return the az and el unchanged, and the
152
297
  map has the same spacing as the PSETs, then the map should have
@@ -154,7 +299,7 @@ class TestRectangularSkyMap:
154
299
  """
155
300
  index_matching_method = ena_maps.IndexMatchMethod.PUSH
156
301
 
157
- pset_spacing_deg = self.ultra_psets[0].spacing_deg
302
+ pset_spacing_deg = self.rectangular_psets[0].sky_grid.spacing_deg
158
303
 
159
304
  # Mock frame_transform to return the az and el unchanged
160
305
  mock_frame_transform_az_el.side_effect = (
@@ -166,59 +311,640 @@ class TestRectangularSkyMap:
166
311
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
167
312
  )
168
313
 
169
- # Project each PSET's values to the map
170
- for ultra_pset in self.ultra_psets:
314
+ # Project each PSET's values to the map (push method)
315
+ for rectangular_pset in self.rectangular_psets:
171
316
  rectangular_map.project_pset_values_to_map(
172
- ultra_pset,
173
- value_keys=["counts", "exposure_time"],
317
+ rectangular_pset,
318
+ value_keys=["counts", "exposure_factor"],
174
319
  index_match_method=index_matching_method,
175
320
  )
176
321
 
177
322
  # Check that the map has been updated
178
- assert rectangular_map.data_dict != {}
323
+ assert "counts" in rectangular_map.data_1d.data_vars
179
324
 
180
325
  # Check that the map has the same values as the PSETs, summed
181
- simple_summed_pset_counts = np.sum(
182
- [pset["counts"].values for pset in self.l1c_pset_products], axis=0
183
- ).reshape(rectangular_map.data_dict["counts"].shape)
326
+ simple_summed_pset_counts_by_energy = np.zeros(
327
+ shape=(
328
+ self.rectangular_l1c_pset_products[0]["counts"].sizes[
329
+ CoordNames.ENERGY_ULTRA.value
330
+ ],
331
+ )
332
+ )
333
+ for pset in self.rectangular_l1c_pset_products:
334
+ simple_summed_pset_counts_by_energy += pset["counts"].sum(
335
+ dim=[
336
+ d for d in pset["counts"].dims if d != CoordNames.ENERGY_ULTRA.value
337
+ ]
338
+ )
184
339
 
185
- np.testing.assert_allclose(
186
- rectangular_map.data_dict["counts"],
187
- simple_summed_pset_counts,
340
+ rmap_counts_per_energy_bin = rectangular_map.data_1d["counts"].sum(
341
+ dim=[
342
+ d
343
+ for d in rectangular_map.data_1d["counts"].dims
344
+ if d != CoordNames.ENERGY_ULTRA.value
345
+ ]
346
+ )
347
+
348
+ np.testing.assert_array_equal(
349
+ rmap_counts_per_energy_bin,
350
+ simple_summed_pset_counts_by_energy,
188
351
  )
189
352
 
190
353
  @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
354
+ def test_project_pset_values_to_map_errors(self):
355
+ index_matching_method = ena_maps.IndexMatchMethod.PUSH
356
+ rectangular_map = ena_maps.RectangularSkyMap(
357
+ spacing_deg=1,
358
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
359
+ )
360
+
361
+ # An error should be raised if a key is not found in the PSET
362
+ with pytest.raises(ValueError, match="Value key invalid not found"):
363
+ rectangular_map.project_pset_values_to_map(
364
+ self.ultra_psets[0],
365
+ value_keys=["invalid"],
366
+ index_match_method=index_matching_method,
367
+ )
368
+
369
+ @pytest.mark.usefixtures("_setup_rectangular_l1c_pset_products")
191
370
  @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
192
- def test_project_pset_values_to_map_pull_method(self, mock_frame_transform_az_el):
193
- """Test projection to Rect. Map fails w "pull" index matching method."""
371
+ def test_project_rect_pset_values_to_map_pull_method(
372
+ self, mock_frame_transform_az_el
373
+ ):
374
+ """
375
+ Test projection Rect PSET to Rect. Map with "pull" index matching method.
376
+
377
+ NOTE: Pull index matching is only expected to be done with Rectangularly tiled
378
+ PointingSet objects.
379
+ """
194
380
 
195
381
  index_matching_method = ena_maps.IndexMatchMethod.PULL
382
+ skymap_spacing = 10
383
+
384
+ # Mock frame_transform to return the az and el unchanged
385
+ mock_frame_transform_az_el.side_effect = (
386
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
387
+ )
196
388
  rectangular_map = ena_maps.RectangularSkyMap(
197
- spacing_deg=10,
389
+ spacing_deg=skymap_spacing,
198
390
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
199
391
  )
200
392
 
201
- with pytest.raises(NotImplementedError):
393
+ # Each map pixel will add the value of a single PSET pixel, so we'll start at 0
394
+ # and add 0, 1, 2, 3, ... to the map
395
+ expected_value_every_pixel = 0
396
+
397
+ # Another way to test this is that (if the PSET pixels are
398
+ # smaller than the SkyMap pixels) the sum of the counts in all PSETs should
399
+ # be (PSET_spacing / SkyMap_spacing)^2 times the sum of the counts in the SkyMap
400
+ total_pset_counts = np.zeros_like(
401
+ self.rectangular_l1c_pset_products[0]["counts"].values
402
+ )
403
+
404
+ # Project each PSET's values to the map (pull method)
405
+ for pset_num, rectangular_pset in enumerate(self.rectangular_psets):
406
+ # Set the counts to be 0 in the first PSET, 1 in the second, etc.
407
+ rectangular_pset.data["counts"].values = np.full_like(
408
+ rectangular_pset.data["counts"].values, pset_num
409
+ )
410
+
202
411
  rectangular_map.project_pset_values_to_map(
203
- self.ultra_psets[0],
204
- value_keys=["counts", "exposure_time"],
412
+ rectangular_pset,
413
+ value_keys=["counts", "exposure_factor"],
205
414
  index_match_method=index_matching_method,
206
415
  )
416
+ expected_value_every_pixel += pset_num
417
+
418
+ total_pset_counts += rectangular_pset.data["counts"].values
419
+
420
+ # Check that the map has been updated
421
+ assert "counts" in rectangular_map.data_1d
422
+
423
+ np.testing.assert_allclose(
424
+ rectangular_map.data_1d["counts"],
425
+ expected_value_every_pixel,
426
+ )
427
+ downsample_ratio = skymap_spacing / self.rectangular_l1c_spacing_deg
428
+ np.testing.assert_allclose(
429
+ rectangular_map.data_1d["counts"].sum(),
430
+ total_pset_counts.sum() / (downsample_ratio**2),
431
+ )
432
+
433
+ # Convert to xarray Dataset and check the data is as expected
434
+ # This is a method, which could be tested separately, but that would be
435
+ # innefficient, as it would require all the same, computationally intensive
436
+ # operations to be repeated as this test
437
+ rect_map_ds = rectangular_map.to_dataset()
438
+ assert "counts" in rect_map_ds.data_vars
439
+ assert rect_map_ds["counts"].shape == (
440
+ 1,
441
+ rectangular_pset.data["counts"].sizes[CoordNames.ENERGY_ULTRA.value],
442
+ 360 / skymap_spacing,
443
+ 180 / skymap_spacing,
444
+ )
445
+ assert rect_map_ds["counts"].dims == (
446
+ CoordNames.TIME.value,
447
+ CoordNames.ENERGY_ULTRA.value,
448
+ CoordNames.AZIMUTH_L2.value,
449
+ CoordNames.ELEVATION_L2.value,
450
+ )
451
+
452
+ # Check that the data is as expected
453
+ np.testing.assert_array_equal(
454
+ rect_map_ds["counts"].values,
455
+ spatial_utils.rewrap_even_spaced_az_el_grid(
456
+ rectangular_map.data_1d["counts"].values,
457
+ rectangular_map.binning_grid_shape,
458
+ ),
459
+ )
460
+
461
+
462
+ class TestHealpixSkyMap:
463
+ @pytest.fixture(autouse=True)
464
+ def _setup_ultra_l1c_pset_products(self, setup_all_pset_products):
465
+ """Setup fixture data as class attributes"""
466
+ self.ultra_l1c_nside = setup_all_pset_products["hp_ultra_nside"]
467
+ self.ultra_l1c_pset_products = deepcopy(
468
+ setup_all_pset_products["hp_ultra_l1c_pset_products"]
469
+ )
470
+ self.ultra_psets = [
471
+ ena_maps.UltraPointingSet(
472
+ l1c_product,
473
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
474
+ )
475
+ for l1c_product in self.ultra_l1c_pset_products
476
+ ]
477
+
478
+ @pytest.fixture(autouse=True)
479
+ def _setup_rectangular_l1c_pset_products(self, setup_all_pset_products):
480
+ """Setup fixture data as class attributes"""
481
+ self.rectangular_l1c_spacing_deg = setup_all_pset_products["rect_spacing"]
482
+ self.rectangular_l1c_pset_products = deepcopy(
483
+ setup_all_pset_products["rect_rectangular_l1c_pset_products"]
484
+ )
485
+ self.rectangular_psets = [
486
+ ena_maps.RectangularPointingSet(
487
+ l1c_product,
488
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
489
+ )
490
+ for l1c_product in self.rectangular_l1c_pset_products
491
+ ]
492
+
493
+ @pytest.mark.parametrize(
494
+ "nside",
495
+ [8, 16, 32],
496
+ )
497
+ @pytest.mark.parametrize("nested", [True, False], ids=["nested", "ring"])
498
+ def test_instantiate(self, nside, nested):
499
+ """Test instantiation of HealpixSkyMap"""
500
+ hp_map = ena_maps.HealpixSkyMap(
501
+ nside=nside,
502
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
503
+ nested=nested,
504
+ )
505
+
506
+ # Check that the map data is an empty xarray Dataset
507
+ assert isinstance(hp_map.data_1d, xr.Dataset)
508
+ assert hp_map.data_1d.data_vars == {}
509
+
510
+ # Check that the reference frame is correctly set
511
+ assert hp_map.spice_reference_frame is geometry.SpiceFrame.ECLIPJ2000
512
+ # Check that the nside and nested properties are set correctly
513
+ np.testing.assert_equal(hp_map.nside, nside)
514
+ np.testing.assert_equal(hp_map.nested, nested)
515
+ # Check the number of points is 12 * nside^2
516
+ np.testing.assert_equal(hp_map.num_points, 12 * nside**2)
517
+ # There will be az, el values for each pixel
518
+ assert hp_map.az_el_points.shape == (hp_map.num_points, 2)
519
+ # The az must be in the range [0, 360) degrees
520
+ # and el in the range [-90, 90)
521
+ assert np.all(hp_map.az_el_points[:, 0] >= 0)
522
+ assert np.all(hp_map.az_el_points[:, 0] < 360)
523
+ assert np.all(hp_map.az_el_points[:, 1] >= -90)
524
+ assert np.all(hp_map.az_el_points[:, 1] < 90)
525
+
526
+ # Check that the binning grid shape is just a tuple of num_points
527
+ np.testing.assert_equal(hp_map.binning_grid_shape, (hp_map.num_points,))
528
+
529
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
530
+ @pytest.mark.parametrize(
531
+ "nside,degree_tolerance",
532
+ [
533
+ (8, 6),
534
+ (16, 3),
535
+ (32, 2),
536
+ ],
537
+ )
538
+ @pytest.mark.parametrize("nested", [True, False], ids=["nested", "ring"])
539
+ @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
540
+ def test_project_healpix_pset_values_to_map_push_method(
541
+ self, mock_frame_transform_az_el, nside, degree_tolerance, nested
542
+ ):
543
+ """
544
+ Test that PointingSet which contains bright spot pushes to correct spot in map.
545
+
546
+ Parameterized over nside (of the map, not the PSET), nested.
547
+ The tolerance for lower nsides must be higher because the
548
+ Healpix pixels are larger.
549
+ """
550
+
551
+ # Mock frame_transform to return the az and el unchanged
552
+ mock_frame_transform_az_el.side_effect = (
553
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
554
+ )
555
+
556
+ index_matching_method = ena_maps.IndexMatchMethod.PUSH
557
+
558
+ # Create a PointingSet with a bright spot
559
+ mock_pset_input_frame = ena_maps.UltraPointingSet(
560
+ self.ultra_l1c_pset_products[0],
561
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
562
+ )
563
+ mock_pset_input_frame.data["counts"].values = np.zeros_like(
564
+ mock_pset_input_frame.data["counts"].values
565
+ )
566
+
567
+ input_bright_pixel_number = hp.ang2pix(
568
+ nside=mock_pset_input_frame.nside,
569
+ theta=180,
570
+ phi=0,
571
+ nest=mock_pset_input_frame.nested,
572
+ lonlat=True,
573
+ )
574
+ input_bright_pixel_az_el_deg = mock_pset_input_frame.az_el_points[
575
+ input_bright_pixel_number
576
+ ]
577
+ mock_pset_input_frame.data["counts"].values[
578
+ :,
579
+ :,
580
+ input_bright_pixel_number,
581
+ ] = 1
582
+
583
+ # Create a Healpix map
584
+ hp_map = ena_maps.HealpixSkyMap(
585
+ nside=nside,
586
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
587
+ nested=nested,
588
+ )
589
+
590
+ # Project the PointingSet to the Healpix map
591
+ hp_map.project_pset_values_to_map(
592
+ mock_pset_input_frame,
593
+ value_keys=[
594
+ "counts",
595
+ ],
596
+ index_match_method=index_matching_method,
597
+ )
598
+
599
+ # Check that the map has been updated
600
+ assert "counts" in hp_map.data_1d.data_vars
601
+
602
+ # Find the maximum value in the spatial pixel dimension of the healpix map
603
+ bright_hp_pixel_index = hp_map.data_1d["counts"][0, :].values.argmax()
604
+ bright_hp_pixel_az_el = hp_map.az_el_points[bright_hp_pixel_index]
605
+
606
+ np.testing.assert_allclose(
607
+ bright_hp_pixel_az_el,
608
+ input_bright_pixel_az_el_deg,
609
+ atol=degree_tolerance,
610
+ )
611
+
612
+ @pytest.mark.usefixtures("_setup_rectangular_l1c_pset_products")
613
+ @pytest.mark.parametrize(
614
+ "nside,degree_tolerance",
615
+ [
616
+ (8, 6),
617
+ (16, 3),
618
+ (32, 2),
619
+ ],
620
+ )
621
+ @pytest.mark.parametrize("nested", [True, False], ids=["nested", "ring"])
622
+ @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
623
+ def test_project_rect_pset_values_to_map_push_method(
624
+ self, mock_frame_transform_az_el, nside, degree_tolerance, nested
625
+ ):
626
+ """
627
+ Test that PointingSet which contains bright spot pushes to correct spot in map.
628
+
629
+ Parameterized over nside, nested. The tolerance for lower nsides must be higher
630
+ because the Healpix pixels are larger.
631
+ """
632
+
633
+ # Mock frame_transform to return the az and el unchanged
634
+ mock_frame_transform_az_el.side_effect = (
635
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
636
+ )
637
+
638
+ index_matching_method = ena_maps.IndexMatchMethod.PUSH
639
+
640
+ # Create a PointingSet with a bright spot
641
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
642
+ self.rectangular_l1c_pset_products[0],
643
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
644
+ )
645
+ mock_pset_input_frame.data["counts"].values = np.zeros_like(
646
+ mock_pset_input_frame.data["counts"].values
647
+ )
648
+
649
+ input_bright_pixel_az_el_deg = (110, 55)
650
+ mock_pset_input_frame.data["counts"].values[
651
+ :,
652
+ :,
653
+ int(
654
+ input_bright_pixel_az_el_deg[0]
655
+ // mock_pset_input_frame.sky_grid.spacing_deg
656
+ ),
657
+ int(
658
+ (90 + input_bright_pixel_az_el_deg[1])
659
+ // mock_pset_input_frame.sky_grid.spacing_deg
660
+ ),
661
+ ] = 1
662
+
663
+ # Create a Healpix map
664
+ hp_map = ena_maps.HealpixSkyMap(
665
+ nside=nside,
666
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
667
+ nested=nested,
668
+ )
669
+
670
+ # Project the PointingSet to the Healpix map
671
+ hp_map.project_pset_values_to_map(
672
+ mock_pset_input_frame,
673
+ value_keys=[
674
+ "counts",
675
+ ],
676
+ index_match_method=index_matching_method,
677
+ )
678
+
679
+ # Check that the map has been updated
680
+ assert "counts" in hp_map.data_1d.data_vars
681
+
682
+ # Find the maximum value in the spatial pixel dimension of the healpix map
683
+ bright_hp_pixel_index = hp_map.data_1d["counts"][0, 0].argmax(dim="pixel")
684
+ bright_hp_pixel_az_el = hp_map.az_el_points[bright_hp_pixel_index]
685
+
686
+ np.testing.assert_allclose(
687
+ bright_hp_pixel_az_el,
688
+ input_bright_pixel_az_el_deg,
689
+ atol=degree_tolerance,
690
+ )
691
+
692
+ # Convert to xarray Dataset and check the data is as expected
693
+ hp_map_ds = hp_map.to_dataset()
694
+ assert "counts" in hp_map_ds.data_vars
695
+ assert hp_map_ds["counts"].shape == (
696
+ 1,
697
+ mock_pset_input_frame.data["counts"].sizes[CoordNames.ENERGY_ULTRA.value],
698
+ hp_map.num_points,
699
+ )
700
+ assert hp_map_ds["counts"].dims == (
701
+ CoordNames.TIME.value,
702
+ CoordNames.ENERGY_ULTRA.value,
703
+ CoordNames.HEALPIX_INDEX.value,
704
+ )
705
+ np.testing.assert_array_equal(
706
+ hp_map_ds["counts"].values,
707
+ hp_map.data_1d["counts"].values,
708
+ )
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
+ )
207
924
 
208
925
 
209
926
  class TestIndexMatching:
210
927
  @pytest.fixture(autouse=True)
211
- def _setup_ultra_l1c_pset_products(self, l1c_pset_products):
928
+ def _setup_rectangular_l1c_pset_products(self, setup_all_pset_products):
212
929
  """Setup fixture data as class attributes"""
213
- self.l1c_spatial_bin_spacing_deg = l1c_pset_products["spacing"]
214
- self.l1c_pset_products = l1c_pset_products["products"]
930
+ self.rectangular_l1c_spacing_deg = setup_all_pset_products["rect_spacing"]
931
+ self.rectangular_l1c_pset_products = deepcopy(
932
+ setup_all_pset_products["rect_rectangular_l1c_pset_products"]
933
+ )
934
+ self.rectangular_psets = [
935
+ ena_maps.RectangularPointingSet(
936
+ l1c_product,
937
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
938
+ )
939
+ for l1c_product in self.rectangular_l1c_pset_products
940
+ ]
215
941
 
216
942
  @pytest.mark.parametrize(
217
943
  "map_spacing_deg",
218
944
  [0.5, 1, 10],
219
945
  )
220
946
  @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
221
- def test_match_coords_to_indices_pset_to_rect_map(
947
+ def test_match_coords_to_indices_rect_pset_to_rect_map(
222
948
  self, mock_frame_transform_az_el, map_spacing_deg
223
949
  ):
224
950
  # Mock frame_transform to return the az and el unchanged
@@ -227,8 +953,8 @@ class TestIndexMatching:
227
953
  )
228
954
 
229
955
  # Mock a PSET, overriding the az/el points
230
- mock_pset_input_frame = ena_maps.UltraPointingSet(
231
- l1c_dataset=self.l1c_pset_products[0],
956
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
957
+ self.rectangular_l1c_pset_products[0],
232
958
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
233
959
  )
234
960
  manual_az_el_coords = np.array(
@@ -246,7 +972,7 @@ class TestIndexMatching:
246
972
  [359.999999, 89.99999],
247
973
  ]
248
974
  )
249
- mock_pset_input_frame.az_el_points = np.deg2rad(manual_az_el_coords)
975
+ mock_pset_input_frame.az_el_points = manual_az_el_coords
250
976
 
251
977
  # Manually calculate the resulting 1D pixel indices for each az/el pair
252
978
  # (num of pixels in an az row spanning 180 deg of elevation) * (current az row)
@@ -259,15 +985,15 @@ class TestIndexMatching:
259
985
  ]
260
986
  )
261
987
 
262
- # Mock the rectangular map and check the output values
263
- mock_rect_map = ena_maps.RectangularSkyMap(
988
+ # Create the rectangular map and check the output values
989
+ rect_map = ena_maps.RectangularSkyMap(
264
990
  spacing_deg=map_spacing_deg,
265
991
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
266
992
  )
267
993
  flat_indices_input_grid_output_frame = ena_maps.match_coords_to_indices(
268
- mock_pset_input_frame, mock_rect_map
994
+ mock_pset_input_frame, rect_map
269
995
  )
270
- assert mock_rect_map.num_points == 360 * 180 / map_spacing_deg**2
996
+ assert rect_map.num_points == 360 * 180 / map_spacing_deg**2
271
997
  assert len(flat_indices_input_grid_output_frame) == len(manual_az_el_coords)
272
998
  np.testing.assert_equal(
273
999
  flat_indices_input_grid_output_frame, expected_output_pixel
@@ -275,35 +1001,267 @@ class TestIndexMatching:
275
1001
 
276
1002
  # Check that the map's az/el points at the matched indices
277
1003
  # are the same as the input az/el points to within the spacing of the map
278
- matched_map_az_el = mock_rect_map.az_el_points[
279
- flat_indices_input_grid_output_frame
280
- ]
1004
+ matched_map_az_el = rect_map.az_el_points[flat_indices_input_grid_output_frame]
281
1005
  np.testing.assert_allclose(
282
1006
  matched_map_az_el[:, 0],
283
1007
  mock_pset_input_frame.az_el_points[:, 0],
284
- atol=np.deg2rad(map_spacing_deg),
1008
+ atol=map_spacing_deg,
285
1009
  )
286
1010
 
287
- def test_match_coords_to_indices_pset_to_healpix_map_other_map(
1011
+ @pytest.mark.parametrize(
1012
+ "nside,degree_tolerance",
1013
+ [
1014
+ (8, 12),
1015
+ (16, 6),
1016
+ (32, 3),
1017
+ ],
1018
+ ids=["nside8", "nside16", "nside32"],
1019
+ )
1020
+ @pytest.mark.parametrize("nested", [True, False], ids=["nested", "ring"])
1021
+ @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
1022
+ def test_match_coords_to_indices_rect_pset_to_healpix_map(
1023
+ self, mock_frame_transform_az_el, nside, degree_tolerance, nested
1024
+ ):
1025
+ # Mock frame_transform to return the az and el unchanged
1026
+ mock_frame_transform_az_el.side_effect = (
1027
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
1028
+ )
1029
+ hp_map = ena_maps.HealpixSkyMap(
1030
+ nside=nside, spice_frame=geometry.SpiceFrame.ECLIPJ2000, nested=nested
1031
+ )
1032
+
1033
+ # Make a PointingSet
1034
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
1035
+ self.rectangular_l1c_pset_products[0],
1036
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
1037
+ )
1038
+
1039
+ # Match the PSET to the Healpix map
1040
+ healpix_indices_of_rect_pixels = ena_maps.match_coords_to_indices(
1041
+ mock_pset_input_frame, hp_map
1042
+ )
1043
+
1044
+ # Check that the map's az/el points at the matched indices
1045
+ # are the same as the input az/el points to within degree_tolerance,
1046
+ # but we must ignore the polar regions and azimuthal wrap-around regions
1047
+ rect_equatorial_elevations_mask = (
1048
+ np.abs(mock_pset_input_frame.az_el_points[:, 1]) < 60
1049
+ )
1050
+ rect_az_non_wraparound_mask = (
1051
+ mock_pset_input_frame.az_el_points[:, 0] < 340
1052
+ ) & (mock_pset_input_frame.az_el_points[:, 0] > 20)
1053
+ rect_good_az_el_mask = (
1054
+ rect_equatorial_elevations_mask & rect_az_non_wraparound_mask
1055
+ )
1056
+ matched_map_az_el = np.column_stack(
1057
+ hp.pix2ang(
1058
+ nside=nside,
1059
+ ipix=healpix_indices_of_rect_pixels,
1060
+ nest=nested,
1061
+ lonlat=True,
1062
+ )
1063
+ )
1064
+ np.testing.assert_allclose(
1065
+ matched_map_az_el[rect_good_az_el_mask, 0],
1066
+ mock_pset_input_frame.az_el_points[rect_good_az_el_mask, 0],
1067
+ atol=degree_tolerance,
1068
+ )
1069
+
1070
+ def test_match_coords_to_indices_pset_to_invalid_map(
288
1071
  self,
289
1072
  ):
290
- mock_pset_input_frame = ena_maps.UltraPointingSet(
291
- l1c_dataset=self.l1c_pset_products[0],
1073
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
1074
+ self.rectangular_l1c_pset_products[0],
292
1075
  spice_reference_frame=geometry.SpiceFrame.ECLIPJ2000,
293
1076
  )
294
-
295
1077
  # Until implemented, just change the tiling on a RectangularSkyMap
296
- mock_hp_map = ena_maps.RectangularSkyMap(
1078
+ mock_invalid_map = ena_maps.RectangularSkyMap(
297
1079
  spacing_deg=2,
298
1080
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
299
1081
  )
300
- mock_hp_map.tiling_type = ena_maps.SkyTilingType.HEALPIX
1082
+ mock_invalid_map.tiling_type = "INVALID"
301
1083
 
302
- # Should raise NotImplementedError
303
- with pytest.raises(NotImplementedError):
304
- ena_maps.match_coords_to_indices(mock_pset_input_frame, mock_hp_map)
305
-
306
- mock_other_map = mock_hp_map
307
- mock_other_map.tiling_type = "INVALID"
1084
+ # Should raise ValueError if the tiling type is invalid
308
1085
  with pytest.raises(ValueError, match="Tiling type of the output frame"):
309
- ena_maps.match_coords_to_indices(mock_pset_input_frame, mock_other_map)
1086
+ ena_maps.match_coords_to_indices(mock_pset_input_frame, mock_invalid_map)
1087
+
1088
+ def test_match_coords_to_indices_pset_to_pset_error(self):
1089
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
1090
+ self.rectangular_l1c_pset_products[0],
1091
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
1092
+ )
1093
+ mock_pset_output_frame = ena_maps.RectangularPointingSet(
1094
+ self.rectangular_l1c_pset_products[1],
1095
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
1096
+ )
1097
+ with pytest.raises(
1098
+ ValueError, match="Cannot match indices between two PointingSet objects"
1099
+ ):
1100
+ ena_maps.match_coords_to_indices(
1101
+ mock_pset_input_frame, mock_pset_output_frame
1102
+ )
1103
+
1104
+ def test_match_coords_to_indices_map_to_map_no_et_error(self):
1105
+ mock_rect_map_1 = ena_maps.RectangularSkyMap(
1106
+ spacing_deg=2,
1107
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
1108
+ )
1109
+ mock_rect_map_2 = ena_maps.RectangularSkyMap(
1110
+ spacing_deg=4,
1111
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
1112
+ )
1113
+ with pytest.raises(
1114
+ ValueError,
1115
+ match="Event time must be specified if both objects are SkyMaps.",
1116
+ ):
1117
+ ena_maps.match_coords_to_indices(mock_rect_map_1, mock_rect_map_2)
1118
+
1119
+ # No error if event time is specified
1120
+ _ = ena_maps.match_coords_to_indices(
1121
+ mock_rect_map_1, mock_rect_map_2, event_et=0
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