imap-processing 0.11.0__py3-none-any.whl → 0.12.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 (288) hide show
  1. imap_processing/__init__.py +10 -11
  2. imap_processing/_version.py +2 -2
  3. imap_processing/ccsds/excel_to_xtce.py +65 -16
  4. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -28
  5. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +365 -42
  6. imap_processing/cdf/config/imap_glows_global_cdf_attrs.yaml +0 -5
  7. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +10 -11
  8. imap_processing/cdf/config/imap_hi_variable_attrs.yaml +17 -19
  9. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +26 -13
  10. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +106 -116
  11. imap_processing/cdf/config/imap_hit_l1b_variable_attrs.yaml +120 -145
  12. imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml +14 -0
  13. imap_processing/cdf/config/imap_idex_global_cdf_attrs.yaml +6 -9
  14. imap_processing/cdf/config/imap_idex_l1a_variable_attrs.yaml +1 -1
  15. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +0 -12
  16. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +1 -1
  17. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +9 -21
  18. imap_processing/cdf/config/imap_mag_l1a_variable_attrs.yaml +361 -0
  19. imap_processing/cdf/config/imap_mag_l1b_variable_attrs.yaml +160 -0
  20. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +160 -0
  21. imap_processing/cdf/config/imap_spacecraft_global_cdf_attrs.yaml +18 -0
  22. imap_processing/cdf/config/imap_spacecraft_variable_attrs.yaml +40 -0
  23. imap_processing/cdf/config/imap_swapi_global_cdf_attrs.yaml +1 -5
  24. imap_processing/cdf/config/imap_swe_global_cdf_attrs.yaml +12 -4
  25. imap_processing/cdf/config/imap_swe_l1a_variable_attrs.yaml +16 -2
  26. imap_processing/cdf/config/imap_swe_l1b_variable_attrs.yaml +48 -52
  27. imap_processing/cdf/config/imap_swe_l2_variable_attrs.yaml +71 -47
  28. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +2 -14
  29. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +51 -2
  30. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +29 -14
  31. imap_processing/cdf/utils.py +13 -7
  32. imap_processing/cli.py +23 -8
  33. imap_processing/codice/codice_l1a.py +207 -85
  34. imap_processing/codice/constants.py +1322 -568
  35. imap_processing/codice/decompress.py +2 -6
  36. imap_processing/ena_maps/ena_maps.py +480 -116
  37. imap_processing/ena_maps/utils/coordinates.py +19 -0
  38. imap_processing/ena_maps/utils/map_utils.py +14 -17
  39. imap_processing/ena_maps/utils/spatial_utils.py +45 -47
  40. imap_processing/hi/l1a/hi_l1a.py +24 -18
  41. imap_processing/hi/l1a/histogram.py +0 -1
  42. imap_processing/hi/l1a/science_direct_event.py +6 -8
  43. imap_processing/hi/l1b/hi_l1b.py +31 -39
  44. imap_processing/hi/l1c/hi_l1c.py +405 -17
  45. imap_processing/hi/utils.py +58 -12
  46. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt0-factors_20250219_v002.csv +205 -0
  47. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt1-factors_20250219_v002.csv +205 -0
  48. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt2-factors_20250219_v002.csv +205 -0
  49. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt3-factors_20250219_v002.csv +205 -0
  50. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-summed-dt0-factors_20250219_v002.csv +68 -0
  51. imap_processing/hit/hit_utils.py +173 -1
  52. imap_processing/hit/l0/constants.py +20 -11
  53. imap_processing/hit/l0/decom_hit.py +18 -4
  54. imap_processing/hit/l1a/hit_l1a.py +45 -54
  55. imap_processing/hit/l1b/constants.py +317 -0
  56. imap_processing/hit/l1b/hit_l1b.py +367 -18
  57. imap_processing/hit/l2/constants.py +281 -0
  58. imap_processing/hit/l2/hit_l2.py +614 -0
  59. imap_processing/hit/packet_definitions/hit_packet_definitions.xml +1323 -71
  60. imap_processing/ialirt/l0/mag_l0_ialirt_data.py +155 -0
  61. imap_processing/ialirt/l0/parse_mag.py +246 -0
  62. imap_processing/ialirt/l0/process_swe.py +252 -0
  63. imap_processing/ialirt/packet_definitions/ialirt.xml +7 -3
  64. imap_processing/ialirt/packet_definitions/ialirt_mag.xml +115 -0
  65. imap_processing/ialirt/utils/grouping.py +114 -0
  66. imap_processing/ialirt/utils/time.py +29 -0
  67. imap_processing/idex/atomic_masses.csv +22 -0
  68. imap_processing/idex/decode.py +2 -2
  69. imap_processing/idex/idex_constants.py +25 -0
  70. imap_processing/idex/idex_l1a.py +6 -7
  71. imap_processing/idex/idex_l1b.py +4 -31
  72. imap_processing/idex/idex_l2a.py +789 -0
  73. imap_processing/idex/idex_variable_unpacking_and_eu_conversion.csv +39 -33
  74. imap_processing/lo/l0/lo_science.py +6 -0
  75. imap_processing/lo/l1a/lo_l1a.py +0 -1
  76. imap_processing/lo/l1b/lo_l1b.py +177 -25
  77. imap_processing/mag/constants.py +8 -0
  78. imap_processing/mag/imap_mag_sdc-configuration_v001.yaml +6 -0
  79. imap_processing/mag/l0/decom_mag.py +10 -3
  80. imap_processing/mag/l1a/mag_l1a.py +22 -11
  81. imap_processing/mag/l1a/mag_l1a_data.py +28 -3
  82. imap_processing/mag/l1b/mag_l1b.py +190 -48
  83. imap_processing/mag/l1c/interpolation_methods.py +211 -0
  84. imap_processing/mag/l1c/mag_l1c.py +447 -9
  85. imap_processing/quality_flags.py +1 -0
  86. imap_processing/spacecraft/packet_definitions/scid_x252.xml +538 -0
  87. imap_processing/spacecraft/quaternions.py +123 -0
  88. imap_processing/spice/geometry.py +16 -19
  89. imap_processing/spice/repoint.py +120 -0
  90. imap_processing/swapi/l1/swapi_l1.py +4 -0
  91. imap_processing/swapi/l2/swapi_l2.py +0 -1
  92. imap_processing/swe/l1a/swe_l1a.py +47 -8
  93. imap_processing/swe/l1a/swe_science.py +5 -2
  94. imap_processing/swe/l1b/swe_l1b_science.py +103 -56
  95. imap_processing/swe/l2/swe_l2.py +60 -65
  96. imap_processing/swe/packet_definitions/swe_packet_definition.xml +1121 -1
  97. imap_processing/swe/utils/swe_constants.py +63 -0
  98. imap_processing/swe/utils/swe_utils.py +85 -28
  99. imap_processing/tests/ccsds/test_data/expected_output.xml +40 -1
  100. imap_processing/tests/ccsds/test_excel_to_xtce.py +23 -20
  101. imap_processing/tests/cdf/test_data/imap_instrument2_global_cdf_attrs.yaml +0 -2
  102. imap_processing/tests/codice/conftest.py +1 -1
  103. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-counters-aggregated_20241110193700_v0.0.0.cdf +0 -0
  104. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-counters-singles_20241110193700_v0.0.0.cdf +0 -0
  105. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-ialirt_20241110193700_v0.0.0.cdf +0 -0
  106. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-omni_20241110193700_v0.0.0.cdf +0 -0
  107. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-pha_20241110193700_v0.0.0.cdf +0 -0
  108. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-priorities_20241110193700_v0.0.0.cdf +0 -0
  109. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-sectored_20241110193700_v0.0.0.cdf +0 -0
  110. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-counters-aggregated_20241110193700_v0.0.0.cdf +0 -0
  111. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-counters-singles_20241110193700_v0.0.0.cdf +0 -0
  112. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-ialirt_20241110193700_v0.0.0.cdf +0 -0
  113. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-angular_20241110193700_v0.0.0.cdf +0 -0
  114. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-priority_20241110193700_v0.0.0.cdf +0 -0
  115. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-species_20241110193700_v0.0.0.cdf +0 -0
  116. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-pha_20241110193700_v0.0.0.cdf +0 -0
  117. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-angular_20241110193700_v0.0.0.cdf +0 -0
  118. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-priority_20241110193700_v0.0.0.cdf +0 -0
  119. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-species_20241110193700_v0.0.0.cdf +0 -0
  120. imap_processing/tests/codice/test_codice_l1a.py +110 -46
  121. imap_processing/tests/codice/test_decompress.py +4 -4
  122. imap_processing/tests/conftest.py +166 -10
  123. imap_processing/tests/ena_maps/conftest.py +51 -0
  124. imap_processing/tests/ena_maps/test_ena_maps.py +638 -109
  125. imap_processing/tests/ena_maps/test_map_utils.py +66 -43
  126. imap_processing/tests/ena_maps/test_spatial_utils.py +16 -20
  127. imap_processing/tests/hi/data/l0/H45_diag_fee_20250208.bin +0 -0
  128. imap_processing/tests/hi/data/l0/H45_diag_fee_20250208_verify.csv +205 -0
  129. imap_processing/tests/hi/test_hi_l1b.py +12 -15
  130. imap_processing/tests/hi/test_hi_l1c.py +234 -6
  131. imap_processing/tests/hi/test_l1a.py +30 -0
  132. imap_processing/tests/hi/test_science_direct_event.py +1 -1
  133. imap_processing/tests/hi/test_utils.py +24 -2
  134. imap_processing/tests/hit/helpers/l1_validation.py +39 -39
  135. imap_processing/tests/hit/test_data/hskp_sample.ccsds +0 -0
  136. imap_processing/tests/hit/test_data/imap_hit_l0_raw_20100105_v001.pkts +0 -0
  137. imap_processing/tests/hit/test_decom_hit.py +4 -0
  138. imap_processing/tests/hit/test_hit_l1a.py +24 -28
  139. imap_processing/tests/hit/test_hit_l1b.py +304 -40
  140. imap_processing/tests/hit/test_hit_l2.py +454 -0
  141. imap_processing/tests/hit/test_hit_utils.py +112 -2
  142. imap_processing/tests/hit/validation_data/hskp_sample_eu_3_6_2025.csv +89 -0
  143. imap_processing/tests/hit/validation_data/hskp_sample_raw.csv +89 -88
  144. imap_processing/tests/ialirt/test_data/l0/461971383-404.bin +0 -0
  145. imap_processing/tests/ialirt/test_data/l0/461971384-405.bin +0 -0
  146. imap_processing/tests/ialirt/test_data/l0/461971385-406.bin +0 -0
  147. imap_processing/tests/ialirt/test_data/l0/461971386-407.bin +0 -0
  148. imap_processing/tests/ialirt/test_data/l0/461971387-408.bin +0 -0
  149. imap_processing/tests/ialirt/test_data/l0/461971388-409.bin +0 -0
  150. imap_processing/tests/ialirt/test_data/l0/461971389-410.bin +0 -0
  151. imap_processing/tests/ialirt/test_data/l0/461971390-411.bin +0 -0
  152. imap_processing/tests/ialirt/test_data/l0/461971391-412.bin +0 -0
  153. imap_processing/tests/ialirt/test_data/l0/sample_decoded_i-alirt_data.csv +383 -0
  154. imap_processing/tests/ialirt/unit/test_grouping.py +81 -0
  155. imap_processing/tests/ialirt/unit/test_parse_mag.py +168 -0
  156. imap_processing/tests/ialirt/unit/test_process_swe.py +208 -3
  157. imap_processing/tests/ialirt/unit/test_time.py +16 -0
  158. imap_processing/tests/idex/conftest.py +62 -6
  159. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20231218_v001.pkts +0 -0
  160. imap_processing/tests/idex/test_data/impact_14_tof_high_data.txt +4508 -4508
  161. imap_processing/tests/idex/test_idex_l1a.py +48 -4
  162. imap_processing/tests/idex/test_idex_l1b.py +3 -3
  163. imap_processing/tests/idex/test_idex_l2a.py +383 -0
  164. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_de_20241022_v002.cdf +0 -0
  165. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_spin_20241022_v002.cdf +0 -0
  166. imap_processing/tests/lo/test_lo_l1b.py +148 -4
  167. imap_processing/tests/lo/test_lo_science.py +1 -0
  168. imap_processing/tests/mag/conftest.py +69 -0
  169. imap_processing/tests/mag/test_mag_decom.py +1 -1
  170. imap_processing/tests/mag/test_mag_l1a.py +38 -0
  171. imap_processing/tests/mag/test_mag_l1b.py +34 -53
  172. imap_processing/tests/mag/test_mag_l1c.py +251 -20
  173. imap_processing/tests/mag/test_mag_validation.py +109 -25
  174. imap_processing/tests/mag/validation/L1b/T009/MAGScience-normal-(2,2)-8s-20250204-16h39.csv +17 -0
  175. imap_processing/tests/mag/validation/L1b/T009/mag-l1a-l1b-t009-magi-out.csv +16 -16
  176. imap_processing/tests/mag/validation/L1b/T009/mag-l1a-l1b-t009-mago-out.csv +16 -16
  177. imap_processing/tests/mag/validation/L1b/T010/MAGScience-normal-(2,2)-8s-20250206-12h05.csv +17 -0
  178. imap_processing/tests/mag/validation/L1b/T011/MAGScience-normal-(2,2)-8s-20250204-16h08.csv +17 -0
  179. imap_processing/tests/mag/validation/L1b/T011/mag-l1a-l1b-t011-magi-out.csv +16 -16
  180. imap_processing/tests/mag/validation/L1b/T011/mag-l1a-l1b-t011-mago-out.csv +16 -16
  181. imap_processing/tests/mag/validation/L1b/T012/MAGScience-normal-(2,2)-8s-20250204-16h08.csv +17 -0
  182. imap_processing/tests/mag/validation/L1b/T012/data.bin +0 -0
  183. imap_processing/tests/mag/validation/L1b/T012/field_like_all_ranges.txt +19200 -0
  184. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-cal.cdf +0 -0
  185. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-in.csv +17 -0
  186. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-magi-out.csv +17 -0
  187. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-mago-out.csv +17 -0
  188. imap_processing/tests/mag/validation/imap_calibration_mag_20240229_v01.cdf +0 -0
  189. imap_processing/tests/spacecraft/__init__.py +0 -0
  190. imap_processing/tests/spacecraft/data/SSR_2024_190_20_08_12_0483851794_2_DA_apid0594_1packet.pkts +0 -0
  191. imap_processing/tests/spacecraft/test_quaternions.py +71 -0
  192. imap_processing/tests/spice/test_data/fake_repoint_data.csv +5 -0
  193. imap_processing/tests/spice/test_geometry.py +6 -9
  194. imap_processing/tests/spice/test_repoint.py +111 -0
  195. imap_processing/tests/swapi/test_swapi_l1.py +7 -3
  196. imap_processing/tests/swe/l0_data/2024051010_SWE_HK_packet.bin +0 -0
  197. imap_processing/tests/swe/l0_data/2024051011_SWE_CEM_RAW_packet.bin +0 -0
  198. imap_processing/tests/swe/l0_validation_data/idle_export_eu.SWE_APP_HK_20240510_092742.csv +49 -0
  199. imap_processing/tests/swe/l0_validation_data/idle_export_eu.SWE_CEM_RAW_20240510_092742.csv +593 -0
  200. imap_processing/tests/swe/test_swe_l1a.py +18 -0
  201. imap_processing/tests/swe/test_swe_l1a_cem_raw.py +52 -0
  202. imap_processing/tests/swe/test_swe_l1a_hk.py +68 -0
  203. imap_processing/tests/swe/test_swe_l1b_science.py +23 -4
  204. imap_processing/tests/swe/test_swe_l2.py +112 -30
  205. imap_processing/tests/test_cli.py +2 -2
  206. imap_processing/tests/test_utils.py +138 -16
  207. imap_processing/tests/ultra/data/l0/FM45_UltraFM45_Functional_2024-01-22T0105_20240122T010548.CCSDS +0 -0
  208. imap_processing/tests/ultra/data/l0/ultra45_raw_sc_ultraimgrates_20220530_00.csv +164 -0
  209. 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
  210. imap_processing/tests/ultra/data/mock_data.py +341 -0
  211. imap_processing/tests/ultra/unit/conftest.py +69 -26
  212. imap_processing/tests/ultra/unit/test_badtimes.py +2 -0
  213. imap_processing/tests/ultra/unit/test_cullingmask.py +4 -0
  214. imap_processing/tests/ultra/unit/test_de.py +12 -4
  215. imap_processing/tests/ultra/unit/test_decom_apid_881.py +44 -0
  216. imap_processing/tests/ultra/unit/test_spacecraft_pset.py +78 -0
  217. imap_processing/tests/ultra/unit/test_ultra_l1a.py +28 -12
  218. imap_processing/tests/ultra/unit/test_ultra_l1b.py +34 -6
  219. imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py +22 -26
  220. imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +86 -51
  221. imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +94 -52
  222. imap_processing/ultra/l0/decom_tools.py +6 -5
  223. imap_processing/ultra/l1a/ultra_l1a.py +28 -56
  224. imap_processing/ultra/l1b/de.py +72 -28
  225. imap_processing/ultra/l1b/extendedspin.py +12 -14
  226. imap_processing/ultra/l1b/ultra_l1b.py +34 -9
  227. imap_processing/ultra/l1b/ultra_l1b_culling.py +65 -29
  228. imap_processing/ultra/l1b/ultra_l1b_extended.py +64 -19
  229. imap_processing/ultra/l1c/spacecraft_pset.py +86 -0
  230. imap_processing/ultra/l1c/ultra_l1c.py +7 -4
  231. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +112 -61
  232. imap_processing/ultra/lookup_tables/ultra_90_dps_exposure_compressed.cdf +0 -0
  233. imap_processing/ultra/utils/ultra_l1_utils.py +20 -2
  234. imap_processing/utils.py +68 -28
  235. {imap_processing-0.11.0.dist-info → imap_processing-0.12.0.dist-info}/METADATA +8 -5
  236. {imap_processing-0.11.0.dist-info → imap_processing-0.12.0.dist-info}/RECORD +250 -199
  237. imap_processing/cdf/config/imap_mag_l1_variable_attrs.yaml +0 -237
  238. imap_processing/hi/l1a/housekeeping.py +0 -27
  239. imap_processing/tests/codice/data/imap_codice_l1a_hi-counters-aggregated_20240429_v001.cdf +0 -0
  240. imap_processing/tests/codice/data/imap_codice_l1a_hi-counters-singles_20240429_v001.cdf +0 -0
  241. imap_processing/tests/codice/data/imap_codice_l1a_hi-omni_20240429_v001.cdf +0 -0
  242. imap_processing/tests/codice/data/imap_codice_l1a_hi-sectored_20240429_v001.cdf +0 -0
  243. imap_processing/tests/codice/data/imap_codice_l1a_hskp_20100101_v001.cdf +0 -0
  244. imap_processing/tests/codice/data/imap_codice_l1a_lo-counters-aggregated_20240429_v001.cdf +0 -0
  245. imap_processing/tests/codice/data/imap_codice_l1a_lo-counters-singles_20240429_v001.cdf +0 -0
  246. imap_processing/tests/codice/data/imap_codice_l1a_lo-nsw-angular_20240429_v001.cdf +0 -0
  247. imap_processing/tests/codice/data/imap_codice_l1a_lo-nsw-priority_20240429_v001.cdf +0 -0
  248. imap_processing/tests/codice/data/imap_codice_l1a_lo-nsw-species_20240429_v001.cdf +0 -0
  249. imap_processing/tests/codice/data/imap_codice_l1a_lo-sw-angular_20240429_v001.cdf +0 -0
  250. imap_processing/tests/codice/data/imap_codice_l1a_lo-sw-priority_20240429_v001.cdf +0 -0
  251. imap_processing/tests/codice/data/imap_codice_l1a_lo-sw-species_20240429_v001.cdf +0 -0
  252. imap_processing/tests/codice/data/imap_codice_l1b_hi-counters-aggregated_20240429_v001.cdf +0 -0
  253. imap_processing/tests/codice/data/imap_codice_l1b_hi-counters-singles_20240429_v001.cdf +0 -0
  254. imap_processing/tests/codice/data/imap_codice_l1b_hi-omni_20240429_v001.cdf +0 -0
  255. imap_processing/tests/codice/data/imap_codice_l1b_hi-sectored_20240429_v001.cdf +0 -0
  256. imap_processing/tests/codice/data/imap_codice_l1b_hskp_20100101_v001.cdf +0 -0
  257. imap_processing/tests/codice/data/imap_codice_l1b_lo-counters-aggregated_20240429_v001.cdf +0 -0
  258. imap_processing/tests/codice/data/imap_codice_l1b_lo-counters-singles_20240429_v001.cdf +0 -0
  259. imap_processing/tests/codice/data/imap_codice_l1b_lo-nsw-angular_20240429_v001.cdf +0 -0
  260. imap_processing/tests/codice/data/imap_codice_l1b_lo-nsw-priority_20240429_v001.cdf +0 -0
  261. imap_processing/tests/codice/data/imap_codice_l1b_lo-nsw-species_20240429_v001.cdf +0 -0
  262. imap_processing/tests/codice/data/imap_codice_l1b_lo-sw-angular_20240429_v001.cdf +0 -0
  263. imap_processing/tests/codice/data/imap_codice_l1b_lo-sw-priority_20240429_v001.cdf +0 -0
  264. imap_processing/tests/codice/data/imap_codice_l1b_lo-sw-species_20240429_v001.cdf +0 -0
  265. imap_processing/tests/hi/data/l1/imap_hi_l1b_45sensor-de_20250415_v999.cdf +0 -0
  266. imap_processing/tests/hit/PREFLIGHT_raw_record_2023_256_15_59_04_apid1251.pkts +0 -0
  267. imap_processing/tests/hit/PREFLIGHT_raw_record_2023_256_15_59_04_apid1252.pkts +0 -0
  268. imap_processing/tests/hit/validation_data/hskp_sample_eu.csv +0 -89
  269. imap_processing/tests/hit/validation_data/sci_sample_raw1.csv +0 -29
  270. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20231214_v001.pkts +0 -0
  271. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_de_20100101_v001.cdf +0 -0
  272. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_spin_20100101_v001.cdf +0 -0
  273. imap_processing/tests/ultra/test_data/mock_data.py +0 -161
  274. imap_processing/ultra/l1c/pset.py +0 -40
  275. /imap_processing/tests/ultra/{test_data → data}/l0/FM45_40P_Phi28p5_BeamCal_LinearScan_phi28.50_theta-0.00_20240207T102740.CCSDS +0 -0
  276. /imap_processing/tests/ultra/{test_data → data}/l0/FM45_7P_Phi0.0_BeamCal_LinearScan_phi0.04_theta-0.01_20230821T121304.CCSDS +0 -0
  277. /imap_processing/tests/ultra/{test_data → data}/l0/FM45_TV_Cycle6_Hot_Ops_Front212_20240124T063837.CCSDS +0 -0
  278. /imap_processing/tests/ultra/{test_data → data}/l0/Ultra45_EM_SwRI_Cal_Run7_ThetaScan_20220530T225054.CCSDS +0 -0
  279. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_auxdata_Ultra45_EM_SwRI_Cal_Run7_ThetaScan_20220530T225054.csv +0 -0
  280. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_enaphxtofhangimg_FM45_TV_Cycle6_Hot_Ops_Front212_20240124T063837.csv +0 -0
  281. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_ultraimgrates_Ultra45_EM_SwRI_Cal_Run7_ThetaScan_20220530T225054.csv +0 -0
  282. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_ultrarawimgevent_FM45_7P_Phi00_BeamCal_LinearScan_phi004_theta-001_20230821T121304.csv +0 -0
  283. /imap_processing/tests/ultra/{test_data → data}/l1/dps_exposure_helio_45_E1.cdf +0 -0
  284. /imap_processing/tests/ultra/{test_data → data}/l1/dps_exposure_helio_45_E12.cdf +0 -0
  285. /imap_processing/tests/ultra/{test_data → data}/l1/dps_exposure_helio_45_E24.cdf +0 -0
  286. {imap_processing-0.11.0.dist-info → imap_processing-0.12.0.dist-info}/LICENSE +0 -0
  287. {imap_processing-0.11.0.dist-info → imap_processing-0.12.0.dist-info}/WHEEL +0 -0
  288. {imap_processing-0.11.0.dist-info → imap_processing-0.12.0.dist-info}/entry_points.txt +0 -0
@@ -2,47 +2,50 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from copy import deepcopy
5
6
  from unittest import mock
6
7
 
8
+ import astropy_healpix.healpy as hp
7
9
  import numpy as np
8
10
  import pytest
9
11
  import xarray as xr
10
12
 
11
13
  from imap_processing.ena_maps import ena_maps
14
+ from imap_processing.ena_maps.utils import spatial_utils
15
+ from imap_processing.ena_maps.utils.coordinates import CoordNames
12
16
  from imap_processing.spice import geometry
13
- from imap_processing.tests.ultra.test_data.mock_data import mock_l1c_pset_product
14
17
 
15
18
 
16
- @pytest.fixture()
17
- def l1c_pset_products():
18
- """Make fake L1C Ultra PSET products for testing"""
19
- l1c_spatial_bin_spacing_deg = 10
19
+ @pytest.fixture(autouse=True, scope="module")
20
+ def setup_all_pset_products(ultra_l1c_pset_datasets, rectangular_l1c_pset_datasets):
21
+ """
22
+ Setup fixture data once for all tests.
23
+
24
+ This is relatively computationally intensive for the high resolution PSETs,
25
+ so we use a module-level fixture to avoid repeating the setup code. However,
26
+ some tests need to modify the PSETs, so we use a function-level fixture to
27
+ make a deepcopy of the PSETs for each test function.
28
+ """
29
+ hp_ultra_nside = ultra_l1c_pset_datasets["nside"]
30
+ hp_ultra_l1c_pset_products = ultra_l1c_pset_datasets["products"]
31
+ rect_spacing = rectangular_l1c_pset_datasets["spacing"]
32
+ rect_rectangular_l1c_pset_products = rectangular_l1c_pset_datasets["products"]
20
33
  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
- ],
34
+ "hp_ultra_nside": hp_ultra_nside,
35
+ "hp_ultra_l1c_pset_products": hp_ultra_l1c_pset_products,
36
+ "rect_spacing": rect_spacing,
37
+ "rect_rectangular_l1c_pset_products": rect_rectangular_l1c_pset_products,
37
38
  }
38
39
 
39
40
 
40
41
  class TestUltraPointingSet:
41
42
  @pytest.fixture(autouse=True)
42
- def _setup_ultra_l1c_pset_products(self, l1c_pset_products):
43
+ def _setup_ultra_l1c_pset_products(self, setup_all_pset_products):
43
44
  """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"]
45
+ self.nside = setup_all_pset_products["hp_ultra_nside"]
46
+ self.l1c_pset_products = deepcopy(
47
+ setup_all_pset_products["hp_ultra_l1c_pset_products"]
48
+ )
46
49
 
47
50
  @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
48
51
  def test_instantiate(self):
@@ -56,71 +59,81 @@ class TestUltraPointingSet:
56
59
  ]
57
60
 
58
61
  for ultra_pset in ultra_psets:
59
- # Check tiling is rectangular
60
- assert ultra_pset.tiling_type == ena_maps.SkyTilingType.RECTANGULAR
62
+ # Check tiling is HEALPix
63
+ assert ultra_pset.tiling_type is ena_maps.SkyTilingType.HEALPIX
61
64
 
62
65
  # Check that the reference frame is correctly set
63
- assert ultra_pset.spice_reference_frame == geometry.SpiceFrame.IMAP_DPS
66
+ assert ultra_pset.spice_reference_frame is geometry.SpiceFrame.IMAP_DPS
64
67
 
65
68
  # Check the number of points is (360/0.5) * (180/0.5)
66
69
  np.testing.assert_equal(
67
70
  ultra_pset.num_points,
68
- int(360 * 180 / (self.l1c_spatial_bin_spacing_deg**2)),
71
+ hp.nside2npix(self.nside),
69
72
  )
70
73
 
71
74
  # Check the repr exists
72
75
  assert "UltraPointingSet" in repr(ultra_pset)
73
76
 
74
- @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"""
77
-
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)
83
-
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,
77
+ # Checks for the property methods:
78
+ # Check that the unwrapped_dims_dict is as expected
79
+ assert ultra_pset.unwrapped_dims_dict["counts"] == (
80
+ "epoch",
81
+ "energy",
82
+ "pixel",
88
83
  )
89
-
90
- uneven_az_dataset["azimuth_bin_center"] = np.arange(5)
91
- uneven_az_dataset["elevation_bin_center"] = np.array([0, 5, 15, 20, 30])
92
-
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,
84
+ # Check the non_spatial_coords are as expected
85
+ assert tuple(ultra_pset.non_spatial_coords.keys()) == (
86
+ "epoch",
87
+ "energy",
97
88
  )
98
89
 
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
90
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
91
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
92
+ def test_different_spacing_raises_error(self):
93
+ """Test that different spaced az/el from the L1C dataset raises ValueError"""
102
94
 
103
- with pytest.raises(
104
- ValueError, match="Azimuth and elevation bin spacing do not match:"
105
- ):
95
+ ultra_pset_ds = self.l1c_pset_products[0]
96
+ # Modify the dataset to have different spacing
97
+ ultra_pset_ds[CoordNames.ELEVATION_L1C.value].values = np.arange(
98
+ ultra_pset_ds[CoordNames.ELEVATION_L1C.value].size
99
+ )
100
+
101
+ with pytest.raises(ValueError, match="do not match"):
106
102
  ena_maps.UltraPointingSet(
107
103
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
108
- l1c_dataset=uneven_az_dataset,
104
+ l1c_dataset=ultra_pset_ds,
109
105
  )
110
106
 
111
107
 
112
108
  class TestRectangularSkyMap:
113
109
  @pytest.fixture(autouse=True)
114
- def _setup_ultra_l1c_pset_products(self, l1c_pset_products):
110
+ def _setup_ultra_l1c_pset_products(self, setup_all_pset_products):
115
111
  """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"]
112
+ self.ultra_l1c_nside = setup_all_pset_products["hp_ultra_nside"]
113
+ self.ultra_l1c_pset_products = deepcopy(
114
+ setup_all_pset_products["hp_ultra_l1c_pset_products"]
115
+ )
118
116
  self.ultra_psets = [
119
117
  ena_maps.UltraPointingSet(
120
118
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
121
119
  l1c_dataset=l1c_product,
122
120
  )
123
- for l1c_product in self.l1c_pset_products
121
+ for l1c_product in self.ultra_l1c_pset_products
122
+ ]
123
+
124
+ @pytest.fixture(autouse=True)
125
+ def _setup_rectangular_l1c_pset_products(self, setup_all_pset_products):
126
+ """Setup fixture data as class attributes"""
127
+ self.rectangular_l1c_spacing_deg = setup_all_pset_products["rect_spacing"]
128
+ self.rectangular_l1c_pset_products = deepcopy(
129
+ setup_all_pset_products["rect_rectangular_l1c_pset_products"]
130
+ )
131
+ self.rectangular_psets = [
132
+ ena_maps.RectangularPointingSet(
133
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
134
+ l1c_dataset=l1c_product,
135
+ )
136
+ for l1c_product in self.rectangular_l1c_pset_products
124
137
  ]
125
138
 
126
139
  def test_instantiate(self):
@@ -130,8 +143,9 @@ class TestRectangularSkyMap:
130
143
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
131
144
  )
132
145
 
133
- # Check that the map is empty
134
- assert rm.data_dict == {}
146
+ # Check that the map data is an empty xarray Dataset
147
+ assert isinstance(rm.data_1d, xr.Dataset)
148
+ assert rm.data_1d.data_vars == {}
135
149
 
136
150
  # Check that the reference frame is correctly set
137
151
  assert rm.spice_reference_frame == geometry.SpiceFrame.ECLIPJ2000
@@ -142,11 +156,78 @@ class TestRectangularSkyMap:
142
156
  # Check the repr exists
143
157
  assert "RectangularSkyMap" in repr(rm)
144
158
 
159
+ np.testing.assert_array_equal(
160
+ rm.binning_grid_shape, (360 / rm.spacing_deg, 180 / rm.spacing_deg)
161
+ )
162
+
145
163
  @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
146
164
  @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):
165
+ def test_project_healpix_pset_values_to_map_push_method(
166
+ self, mock_frame_transform_az_el
167
+ ):
168
+ """
169
+ Test projection of Healpix tiled PSET values to RectMap w "push" index matching.
170
+
171
+ If frame_transform_az_el is mocked to return the az and el unchanged,
172
+ then the map should have the same total counts in each energy bin
173
+ as the PSETs, summed.
174
+ """
175
+ index_matching_method = ena_maps.IndexMatchMethod.PUSH
176
+
177
+ # Mock frame_transform to return the az and el unchanged
178
+ mock_frame_transform_az_el.side_effect = (
179
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
180
+ )
181
+
182
+ rectangular_map = ena_maps.RectangularSkyMap(
183
+ spacing_deg=2,
184
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
185
+ )
186
+
187
+ # Project each PSET's values to the map (push method)
188
+ for ultra_pset in self.ultra_psets:
189
+ rectangular_map.project_pset_values_to_map(
190
+ ultra_pset,
191
+ value_keys=["counts", "exposure_time"],
192
+ index_match_method=index_matching_method,
193
+ )
194
+
195
+ # Check that the map has been updated
196
+ assert "counts" in rectangular_map.data_1d.data_vars
197
+
198
+ # Check that the map has the same values as the PSETs, summed
199
+ simple_summed_pset_counts_by_energy = np.zeros(
200
+ shape=(
201
+ self.ultra_l1c_pset_products[0]["counts"].sizes[
202
+ CoordNames.ENERGY.value
203
+ ],
204
+ )
205
+ )
206
+ for pset in self.ultra_l1c_pset_products:
207
+ simple_summed_pset_counts_by_energy += pset["counts"].sum(
208
+ dim=[d for d in pset["counts"].dims if d != CoordNames.ENERGY.value]
209
+ )
210
+
211
+ rmap_counts_per_energy_bin = rectangular_map.data_1d["counts"].sum(
212
+ dim=[
213
+ d
214
+ for d in rectangular_map.data_1d["counts"].dims
215
+ if d != CoordNames.ENERGY.value
216
+ ]
217
+ )
218
+
219
+ np.testing.assert_array_equal(
220
+ rmap_counts_per_energy_bin,
221
+ simple_summed_pset_counts_by_energy,
222
+ )
223
+
224
+ @pytest.mark.usefixtures("_setup_rectangular_l1c_pset_products")
225
+ @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
226
+ def test_project_rect_pset_values_to_map_push_method(
227
+ self, mock_frame_transform_az_el
228
+ ):
148
229
  """
149
- Test projection of PSET values to Rect. Map w "push" index matching method.
230
+ Test projection of Rect PSET values to Rect Map w "push" index matching method.
150
231
 
151
232
  If frame_transform_az_el is mocked to return the az and el unchanged, and the
152
233
  map has the same spacing as the PSETs, then the map should have
@@ -154,7 +235,7 @@ class TestRectangularSkyMap:
154
235
  """
155
236
  index_matching_method = ena_maps.IndexMatchMethod.PUSH
156
237
 
157
- pset_spacing_deg = self.ultra_psets[0].spacing_deg
238
+ pset_spacing_deg = self.rectangular_psets[0].spacing_deg
158
239
 
159
240
  # Mock frame_transform to return the az and el unchanged
160
241
  mock_frame_transform_az_el.side_effect = (
@@ -166,59 +247,420 @@ class TestRectangularSkyMap:
166
247
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
167
248
  )
168
249
 
169
- # Project each PSET's values to the map
170
- for ultra_pset in self.ultra_psets:
250
+ # Project each PSET's values to the map (push method)
251
+ for rectangular_pset in self.rectangular_psets:
171
252
  rectangular_map.project_pset_values_to_map(
172
- ultra_pset,
253
+ rectangular_pset,
173
254
  value_keys=["counts", "exposure_time"],
174
255
  index_match_method=index_matching_method,
175
256
  )
176
257
 
177
258
  # Check that the map has been updated
178
- assert rectangular_map.data_dict != {}
259
+ assert "counts" in rectangular_map.data_1d.data_vars
179
260
 
180
261
  # 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)
262
+ simple_summed_pset_counts_by_energy = np.zeros(
263
+ shape=(
264
+ self.rectangular_l1c_pset_products[0]["counts"].sizes[
265
+ CoordNames.ENERGY.value
266
+ ],
267
+ )
268
+ )
269
+ for pset in self.rectangular_l1c_pset_products:
270
+ simple_summed_pset_counts_by_energy += pset["counts"].sum(
271
+ dim=[d for d in pset["counts"].dims if d != CoordNames.ENERGY.value]
272
+ )
184
273
 
185
- np.testing.assert_allclose(
186
- rectangular_map.data_dict["counts"],
187
- simple_summed_pset_counts,
274
+ rmap_counts_per_energy_bin = rectangular_map.data_1d["counts"].sum(
275
+ dim=[
276
+ d
277
+ for d in rectangular_map.data_1d["counts"].dims
278
+ if d != CoordNames.ENERGY.value
279
+ ]
280
+ )
281
+
282
+ np.testing.assert_array_equal(
283
+ rmap_counts_per_energy_bin,
284
+ simple_summed_pset_counts_by_energy,
188
285
  )
189
286
 
190
287
  @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
288
+ def test_project_pset_values_to_map_errors(self):
289
+ index_matching_method = ena_maps.IndexMatchMethod.PUSH
290
+ rectangular_map = ena_maps.RectangularSkyMap(
291
+ spacing_deg=1,
292
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
293
+ )
294
+
295
+ # An error should be raised if a key is not found in the PSET
296
+ with pytest.raises(ValueError, match="Value key invalid not found"):
297
+ rectangular_map.project_pset_values_to_map(
298
+ self.ultra_psets[0],
299
+ value_keys=["invalid"],
300
+ index_match_method=index_matching_method,
301
+ )
302
+
303
+ @pytest.mark.usefixtures("_setup_rectangular_l1c_pset_products")
191
304
  @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."""
305
+ def test_project_rect_pset_values_to_map_pull_method(
306
+ self, mock_frame_transform_az_el
307
+ ):
308
+ """
309
+ Test projection Rect PSET to Rect. Map with "pull" index matching method.
310
+
311
+ NOTE: Pull index matching is only expected to be done with Rectangularly tiled
312
+ PointingSet objects.
313
+ """
194
314
 
195
315
  index_matching_method = ena_maps.IndexMatchMethod.PULL
316
+ skymap_spacing = 10
317
+
318
+ # Mock frame_transform to return the az and el unchanged
319
+ mock_frame_transform_az_el.side_effect = (
320
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
321
+ )
196
322
  rectangular_map = ena_maps.RectangularSkyMap(
197
- spacing_deg=10,
323
+ spacing_deg=skymap_spacing,
198
324
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
199
325
  )
200
326
 
201
- with pytest.raises(NotImplementedError):
327
+ # Each map pixel will add the value of a single PSET pixel, so we'll start at 0
328
+ # and add 0, 1, 2, 3, ... to the map
329
+ expected_value_every_pixel = 0
330
+
331
+ # Another way to test this is that (if the PSET pixels are
332
+ # smaller than the SkyMap pixels) the sum of the counts in all PSETs should
333
+ # be (PSET_spacing / SkyMap_spacing)^2 times the sum of the counts in the SkyMap
334
+ total_pset_counts = np.zeros_like(
335
+ self.rectangular_l1c_pset_products[0]["counts"].values
336
+ )
337
+
338
+ # Project each PSET's values to the map (pull method)
339
+ for pset_num, rectangular_pset in enumerate(self.rectangular_psets):
340
+ # Set the counts to be 0 in the first PSET, 1 in the second, etc.
341
+ rectangular_pset.data["counts"].values = np.full_like(
342
+ rectangular_pset.data["counts"].values, pset_num
343
+ )
344
+
202
345
  rectangular_map.project_pset_values_to_map(
203
- self.ultra_psets[0],
346
+ rectangular_pset,
204
347
  value_keys=["counts", "exposure_time"],
205
348
  index_match_method=index_matching_method,
206
349
  )
350
+ expected_value_every_pixel += pset_num
351
+
352
+ total_pset_counts += rectangular_pset.data["counts"].values
353
+
354
+ # Check that the map has been updated
355
+ assert "counts" in rectangular_map.data_1d
356
+
357
+ np.testing.assert_allclose(
358
+ rectangular_map.data_1d["counts"],
359
+ expected_value_every_pixel,
360
+ )
361
+ downsample_ratio = skymap_spacing / self.rectangular_l1c_spacing_deg
362
+ np.testing.assert_allclose(
363
+ rectangular_map.data_1d["counts"].sum(),
364
+ total_pset_counts.sum() / (downsample_ratio**2),
365
+ )
366
+
367
+ # Convert to xarray Dataset and check the data is as expected
368
+ # This is a method, which could be tested separately, but that would be
369
+ # innefficient, as it would require all the same, computationally intensive
370
+ # operations to be repeated as this test
371
+ rect_map_ds = rectangular_map.to_dataset()
372
+ assert "counts" in rect_map_ds.data_vars
373
+ assert rect_map_ds["counts"].shape == (
374
+ 1,
375
+ rectangular_pset.data["counts"].sizes[CoordNames.ENERGY.value],
376
+ 360 / skymap_spacing,
377
+ 180 / skymap_spacing,
378
+ )
379
+ assert rect_map_ds["counts"].dims == (
380
+ CoordNames.TIME.value,
381
+ CoordNames.ENERGY.value,
382
+ CoordNames.AZIMUTH_L2.value,
383
+ CoordNames.ELEVATION_L2.value,
384
+ )
385
+
386
+ # Check that the data is as expected
387
+ np.testing.assert_array_equal(
388
+ rect_map_ds["counts"].values,
389
+ spatial_utils.rewrap_even_spaced_az_el_grid(
390
+ rectangular_map.data_1d["counts"].values,
391
+ rectangular_map.binning_grid_shape,
392
+ ),
393
+ )
394
+
395
+
396
+ class TestHealpixSkyMap:
397
+ @pytest.fixture(autouse=True)
398
+ def _setup_ultra_l1c_pset_products(self, setup_all_pset_products):
399
+ """Setup fixture data as class attributes"""
400
+ self.ultra_l1c_nside = setup_all_pset_products["hp_ultra_nside"]
401
+ self.ultra_l1c_pset_products = deepcopy(
402
+ setup_all_pset_products["hp_ultra_l1c_pset_products"]
403
+ )
404
+ self.ultra_psets = [
405
+ ena_maps.UltraPointingSet(
406
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
407
+ l1c_dataset=l1c_product,
408
+ )
409
+ for l1c_product in self.ultra_l1c_pset_products
410
+ ]
411
+
412
+ @pytest.fixture(autouse=True)
413
+ def _setup_rectangular_l1c_pset_products(self, setup_all_pset_products):
414
+ """Setup fixture data as class attributes"""
415
+ self.rectangular_l1c_spacing_deg = setup_all_pset_products["rect_spacing"]
416
+ self.rectangular_l1c_pset_products = deepcopy(
417
+ setup_all_pset_products["rect_rectangular_l1c_pset_products"]
418
+ )
419
+ self.rectangular_psets = [
420
+ ena_maps.RectangularPointingSet(
421
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
422
+ l1c_dataset=l1c_product,
423
+ )
424
+ for l1c_product in self.rectangular_l1c_pset_products
425
+ ]
426
+
427
+ @pytest.mark.parametrize(
428
+ "nside",
429
+ [8, 16, 32],
430
+ )
431
+ @pytest.mark.parametrize("nested", [True, False], ids=["nested", "ring"])
432
+ def test_instantiate(self, nside, nested):
433
+ """Test instantiation of HealpixSkyMap"""
434
+ hp_map = ena_maps.HealpixSkyMap(
435
+ nside=nside,
436
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
437
+ nested=nested,
438
+ )
439
+
440
+ # Check that the map data is an empty xarray Dataset
441
+ assert isinstance(hp_map.data_1d, xr.Dataset)
442
+ assert hp_map.data_1d.data_vars == {}
443
+
444
+ # Check that the reference frame is correctly set
445
+ assert hp_map.spice_reference_frame is geometry.SpiceFrame.ECLIPJ2000
446
+ # Check that the nside and nested properties are set correctly
447
+ np.testing.assert_equal(hp_map.nside, nside)
448
+ np.testing.assert_equal(hp_map.nested, nested)
449
+ # Check the number of points is 12 * nside^2
450
+ np.testing.assert_equal(hp_map.num_points, 12 * nside**2)
451
+ # There will be az, el values for each pixel
452
+ assert hp_map.az_el_points.shape == (hp_map.num_points, 2)
453
+ # The az must be in the range [0, 360) degrees
454
+ # and el in the range [-90, 90)
455
+ assert np.all(hp_map.az_el_points[:, 0] >= 0)
456
+ assert np.all(hp_map.az_el_points[:, 0] < 360)
457
+ assert np.all(hp_map.az_el_points[:, 1] >= -90)
458
+ assert np.all(hp_map.az_el_points[:, 1] < 90)
459
+
460
+ # Check that the binning grid shape is just a tuple of num_points
461
+ np.testing.assert_equal(hp_map.binning_grid_shape, (hp_map.num_points,))
462
+
463
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
464
+ @pytest.mark.parametrize(
465
+ "nside,degree_tolerance",
466
+ [
467
+ (8, 6),
468
+ (16, 3),
469
+ (32, 2),
470
+ ],
471
+ )
472
+ @pytest.mark.parametrize("nested", [True, False], ids=["nested", "ring"])
473
+ @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
474
+ def test_project_healpix_pset_values_to_map_push_method(
475
+ self, mock_frame_transform_az_el, nside, degree_tolerance, nested
476
+ ):
477
+ """
478
+ Test that PointingSet which contains bright spot pushes to correct spot in map.
479
+
480
+ Parameterized over nside (of the map, not the PSET), nested.
481
+ The tolerance for lower nsides must be higher because the
482
+ Healpix pixels are larger.
483
+ """
484
+
485
+ # Mock frame_transform to return the az and el unchanged
486
+ mock_frame_transform_az_el.side_effect = (
487
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
488
+ )
489
+
490
+ index_matching_method = ena_maps.IndexMatchMethod.PUSH
491
+
492
+ # Create a PointingSet with a bright spot
493
+ mock_pset_input_frame = ena_maps.UltraPointingSet(
494
+ l1c_dataset=self.ultra_l1c_pset_products[0],
495
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
496
+ )
497
+ mock_pset_input_frame.data["counts"].values = np.zeros_like(
498
+ mock_pset_input_frame.data["counts"].values
499
+ )
500
+
501
+ input_bright_pixel_number = hp.ang2pix(
502
+ nside=mock_pset_input_frame.nside,
503
+ theta=180,
504
+ phi=0,
505
+ nest=mock_pset_input_frame.nested,
506
+ lonlat=True,
507
+ )
508
+ input_bright_pixel_az_el_deg = mock_pset_input_frame.az_el_points[
509
+ input_bright_pixel_number
510
+ ]
511
+ mock_pset_input_frame.data["counts"].values[
512
+ :,
513
+ :,
514
+ input_bright_pixel_number,
515
+ ] = 1
516
+
517
+ # Create a Healpix map
518
+ hp_map = ena_maps.HealpixSkyMap(
519
+ nside=nside,
520
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
521
+ nested=nested,
522
+ )
523
+
524
+ # Project the PointingSet to the Healpix map
525
+ hp_map.project_pset_values_to_map(
526
+ mock_pset_input_frame,
527
+ value_keys=[
528
+ "counts",
529
+ ],
530
+ index_match_method=index_matching_method,
531
+ )
532
+
533
+ # Check that the map has been updated
534
+ assert "counts" in hp_map.data_1d.data_vars
535
+
536
+ # 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()
538
+ bright_hp_pixel_az_el = hp_map.az_el_points[bright_hp_pixel_index]
539
+
540
+ np.testing.assert_allclose(
541
+ bright_hp_pixel_az_el,
542
+ input_bright_pixel_az_el_deg,
543
+ atol=degree_tolerance,
544
+ )
545
+
546
+ @pytest.mark.usefixtures("_setup_rectangular_l1c_pset_products")
547
+ @pytest.mark.parametrize(
548
+ "nside,degree_tolerance",
549
+ [
550
+ (8, 6),
551
+ (16, 3),
552
+ (32, 2),
553
+ ],
554
+ )
555
+ @pytest.mark.parametrize("nested", [True, False], ids=["nested", "ring"])
556
+ @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
557
+ def test_project_rect_pset_values_to_map_push_method(
558
+ self, mock_frame_transform_az_el, nside, degree_tolerance, nested
559
+ ):
560
+ """
561
+ Test that PointingSet which contains bright spot pushes to correct spot in map.
562
+
563
+ Parameterized over nside, nested. The tolerance for lower nsides must be higher
564
+ because the Healpix pixels are larger.
565
+ """
566
+
567
+ # Mock frame_transform to return the az and el unchanged
568
+ mock_frame_transform_az_el.side_effect = (
569
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
570
+ )
571
+
572
+ index_matching_method = ena_maps.IndexMatchMethod.PUSH
573
+
574
+ # Create a PointingSet with a bright spot
575
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
576
+ l1c_dataset=self.rectangular_l1c_pset_products[0],
577
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
578
+ )
579
+ mock_pset_input_frame.data["counts"].values = np.zeros_like(
580
+ mock_pset_input_frame.data["counts"].values
581
+ )
582
+
583
+ input_bright_pixel_az_el_deg = (110, 55)
584
+ mock_pset_input_frame.data["counts"].values[
585
+ :,
586
+ :,
587
+ int(input_bright_pixel_az_el_deg[0] // mock_pset_input_frame.spacing_deg),
588
+ int(
589
+ (90 + input_bright_pixel_az_el_deg[1])
590
+ // mock_pset_input_frame.spacing_deg
591
+ ),
592
+ ] = 1
593
+
594
+ # Create a Healpix map
595
+ hp_map = ena_maps.HealpixSkyMap(
596
+ nside=nside,
597
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
598
+ nested=nested,
599
+ )
600
+
601
+ # Project the PointingSet to the Healpix map
602
+ hp_map.project_pset_values_to_map(
603
+ mock_pset_input_frame,
604
+ value_keys=[
605
+ "counts",
606
+ ],
607
+ index_match_method=index_matching_method,
608
+ )
609
+
610
+ # Check that the map has been updated
611
+ assert "counts" in hp_map.data_1d.data_vars
612
+
613
+ # Find the maximum value in the spatial pixel dimension of the healpix map
614
+ bright_hp_pixel_index = hp_map.data_1d["counts"][0, 0].argmax(dim="pixel")
615
+ bright_hp_pixel_az_el = hp_map.az_el_points[bright_hp_pixel_index]
616
+
617
+ np.testing.assert_allclose(
618
+ bright_hp_pixel_az_el,
619
+ input_bright_pixel_az_el_deg,
620
+ atol=degree_tolerance,
621
+ )
622
+
623
+ # Convert to xarray Dataset and check the data is as expected
624
+ hp_map_ds = hp_map.to_dataset()
625
+ assert "counts" in hp_map_ds.data_vars
626
+ assert hp_map_ds["counts"].shape == (
627
+ 1,
628
+ mock_pset_input_frame.data["counts"].sizes[CoordNames.ENERGY.value],
629
+ hp_map.num_points,
630
+ )
631
+ assert hp_map_ds["counts"].dims == (
632
+ CoordNames.TIME.value,
633
+ CoordNames.ENERGY.value,
634
+ CoordNames.HEALPIX_INDEX.value,
635
+ )
636
+ np.testing.assert_array_equal(
637
+ hp_map_ds["counts"].values,
638
+ hp_map.data_1d["counts"].values,
639
+ )
207
640
 
208
641
 
209
642
  class TestIndexMatching:
210
643
  @pytest.fixture(autouse=True)
211
- def _setup_ultra_l1c_pset_products(self, l1c_pset_products):
644
+ def _setup_rectangular_l1c_pset_products(self, setup_all_pset_products):
212
645
  """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"]
646
+ self.rectangular_l1c_spacing_deg = setup_all_pset_products["rect_spacing"]
647
+ self.rectangular_l1c_pset_products = deepcopy(
648
+ setup_all_pset_products["rect_rectangular_l1c_pset_products"]
649
+ )
650
+ self.rectangular_psets = [
651
+ ena_maps.RectangularPointingSet(
652
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
653
+ l1c_dataset=l1c_product,
654
+ )
655
+ for l1c_product in self.rectangular_l1c_pset_products
656
+ ]
215
657
 
216
658
  @pytest.mark.parametrize(
217
659
  "map_spacing_deg",
218
660
  [0.5, 1, 10],
219
661
  )
220
662
  @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
221
- def test_match_coords_to_indices_pset_to_rect_map(
663
+ def test_match_coords_to_indices_rect_pset_to_rect_map(
222
664
  self, mock_frame_transform_az_el, map_spacing_deg
223
665
  ):
224
666
  # Mock frame_transform to return the az and el unchanged
@@ -227,8 +669,8 @@ class TestIndexMatching:
227
669
  )
228
670
 
229
671
  # Mock a PSET, overriding the az/el points
230
- mock_pset_input_frame = ena_maps.UltraPointingSet(
231
- l1c_dataset=self.l1c_pset_products[0],
672
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
673
+ l1c_dataset=self.rectangular_l1c_pset_products[0],
232
674
  spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
233
675
  )
234
676
  manual_az_el_coords = np.array(
@@ -246,7 +688,7 @@ class TestIndexMatching:
246
688
  [359.999999, 89.99999],
247
689
  ]
248
690
  )
249
- mock_pset_input_frame.az_el_points = np.deg2rad(manual_az_el_coords)
691
+ mock_pset_input_frame.az_el_points = manual_az_el_coords
250
692
 
251
693
  # Manually calculate the resulting 1D pixel indices for each az/el pair
252
694
  # (num of pixels in an az row spanning 180 deg of elevation) * (current az row)
@@ -259,15 +701,15 @@ class TestIndexMatching:
259
701
  ]
260
702
  )
261
703
 
262
- # Mock the rectangular map and check the output values
263
- mock_rect_map = ena_maps.RectangularSkyMap(
704
+ # Create the rectangular map and check the output values
705
+ rect_map = ena_maps.RectangularSkyMap(
264
706
  spacing_deg=map_spacing_deg,
265
707
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
266
708
  )
267
709
  flat_indices_input_grid_output_frame = ena_maps.match_coords_to_indices(
268
- mock_pset_input_frame, mock_rect_map
710
+ mock_pset_input_frame, rect_map
269
711
  )
270
- assert mock_rect_map.num_points == 360 * 180 / map_spacing_deg**2
712
+ assert rect_map.num_points == 360 * 180 / map_spacing_deg**2
271
713
  assert len(flat_indices_input_grid_output_frame) == len(manual_az_el_coords)
272
714
  np.testing.assert_equal(
273
715
  flat_indices_input_grid_output_frame, expected_output_pixel
@@ -275,35 +717,122 @@ class TestIndexMatching:
275
717
 
276
718
  # Check that the map's az/el points at the matched indices
277
719
  # 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
- ]
720
+ matched_map_az_el = rect_map.az_el_points[flat_indices_input_grid_output_frame]
281
721
  np.testing.assert_allclose(
282
722
  matched_map_az_el[:, 0],
283
723
  mock_pset_input_frame.az_el_points[:, 0],
284
- atol=np.deg2rad(map_spacing_deg),
724
+ atol=map_spacing_deg,
725
+ )
726
+
727
+ @pytest.mark.parametrize(
728
+ "nside,degree_tolerance",
729
+ [
730
+ (8, 12),
731
+ (16, 6),
732
+ (32, 3),
733
+ ],
734
+ ids=["nside8", "nside16", "nside32"],
735
+ )
736
+ @pytest.mark.parametrize("nested", [True, False], ids=["nested", "ring"])
737
+ @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
738
+ def test_match_coords_to_indices_rect_pset_to_healpix_map(
739
+ self, mock_frame_transform_az_el, nside, degree_tolerance, nested
740
+ ):
741
+ # Mock frame_transform to return the az and el unchanged
742
+ mock_frame_transform_az_el.side_effect = (
743
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
744
+ )
745
+ hp_map = ena_maps.HealpixSkyMap(
746
+ nside=nside, spice_frame=geometry.SpiceFrame.ECLIPJ2000, nested=nested
747
+ )
748
+
749
+ # Make a PointingSet
750
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
751
+ l1c_dataset=self.rectangular_l1c_pset_products[0],
752
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
285
753
  )
286
754
 
287
- def test_match_coords_to_indices_pset_to_healpix_map_other_map(
755
+ # Match the PSET to the Healpix map
756
+ healpix_indices_of_rect_pixels = ena_maps.match_coords_to_indices(
757
+ mock_pset_input_frame, hp_map
758
+ )
759
+
760
+ # Check that the map's az/el points at the matched indices
761
+ # are the same as the input az/el points to within degree_tolerance,
762
+ # but we must ignore the polar regions and azimuthal wrap-around regions
763
+ rect_equatorial_elevations_mask = (
764
+ np.abs(mock_pset_input_frame.az_el_points[:, 1]) < 60
765
+ )
766
+ rect_az_non_wraparound_mask = (
767
+ mock_pset_input_frame.az_el_points[:, 0] < 340
768
+ ) & (mock_pset_input_frame.az_el_points[:, 0] > 20)
769
+ rect_good_az_el_mask = (
770
+ rect_equatorial_elevations_mask & rect_az_non_wraparound_mask
771
+ )
772
+ matched_map_az_el = np.column_stack(
773
+ hp.pix2ang(
774
+ nside=nside,
775
+ ipix=healpix_indices_of_rect_pixels,
776
+ nest=nested,
777
+ lonlat=True,
778
+ )
779
+ )
780
+ np.testing.assert_allclose(
781
+ matched_map_az_el[rect_good_az_el_mask, 0],
782
+ mock_pset_input_frame.az_el_points[rect_good_az_el_mask, 0],
783
+ atol=degree_tolerance,
784
+ )
785
+
786
+ def test_match_coords_to_indices_pset_to_invalid_map(
288
787
  self,
289
788
  ):
290
- mock_pset_input_frame = ena_maps.UltraPointingSet(
291
- l1c_dataset=self.l1c_pset_products[0],
789
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
790
+ l1c_dataset=self.rectangular_l1c_pset_products[0],
292
791
  spice_reference_frame=geometry.SpiceFrame.ECLIPJ2000,
293
792
  )
294
-
295
793
  # Until implemented, just change the tiling on a RectangularSkyMap
296
- mock_hp_map = ena_maps.RectangularSkyMap(
794
+ mock_invalid_map = ena_maps.RectangularSkyMap(
297
795
  spacing_deg=2,
298
796
  spice_frame=geometry.SpiceFrame.ECLIPJ2000,
299
797
  )
300
- mock_hp_map.tiling_type = ena_maps.SkyTilingType.HEALPIX
301
-
302
- # Should raise NotImplementedError
303
- with pytest.raises(NotImplementedError):
304
- ena_maps.match_coords_to_indices(mock_pset_input_frame, mock_hp_map)
798
+ mock_invalid_map.tiling_type = "INVALID"
305
799
 
306
- mock_other_map = mock_hp_map
307
- mock_other_map.tiling_type = "INVALID"
800
+ # Should raise ValueError if the tiling type is invalid
308
801
  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)
802
+ ena_maps.match_coords_to_indices(mock_pset_input_frame, mock_invalid_map)
803
+
804
+ def test_match_coords_to_indices_pset_to_pset_error(self):
805
+ mock_pset_input_frame = ena_maps.RectangularPointingSet(
806
+ l1c_dataset=self.rectangular_l1c_pset_products[0],
807
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
808
+ )
809
+ mock_pset_output_frame = ena_maps.RectangularPointingSet(
810
+ l1c_dataset=self.rectangular_l1c_pset_products[1],
811
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
812
+ )
813
+ with pytest.raises(
814
+ ValueError, match="Cannot match indices between two PointingSet objects"
815
+ ):
816
+ ena_maps.match_coords_to_indices(
817
+ mock_pset_input_frame, mock_pset_output_frame
818
+ )
819
+
820
+ def test_match_coords_to_indices_map_to_map_no_et_error(self):
821
+ mock_rect_map_1 = ena_maps.RectangularSkyMap(
822
+ spacing_deg=2,
823
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
824
+ )
825
+ mock_rect_map_2 = ena_maps.RectangularSkyMap(
826
+ spacing_deg=4,
827
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
828
+ )
829
+ with pytest.raises(
830
+ ValueError,
831
+ match="Event time must be specified if both objects are SkyMaps.",
832
+ ):
833
+ ena_maps.match_coords_to_indices(mock_rect_map_1, mock_rect_map_2)
834
+
835
+ # No error if event time is specified
836
+ _ = ena_maps.match_coords_to_indices(
837
+ mock_rect_map_1, mock_rect_map_2, event_et=0
838
+ )