imap-processing 0.9.0__py3-none-any.whl → 0.11.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 (243) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +749 -442
  3. imap_processing/cdf/config/imap_glows_global_cdf_attrs.yaml +7 -0
  4. imap_processing/cdf/config/imap_glows_l1a_variable_attrs.yaml +8 -2
  5. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +0 -1
  6. imap_processing/cdf/config/imap_glows_l2_variable_attrs.yaml +358 -0
  7. imap_processing/cdf/config/imap_hi_variable_attrs.yaml +59 -25
  8. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +22 -0
  9. imap_processing/cdf/config/imap_idex_l1a_variable_attrs.yaml +32 -8
  10. imap_processing/cdf/config/imap_idex_l1b_variable_attrs.yaml +94 -5
  11. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +65 -37
  12. imap_processing/cdf/config/imap_swapi_variable_attrs.yaml +16 -1
  13. imap_processing/cdf/config/imap_swe_global_cdf_attrs.yaml +7 -0
  14. imap_processing/cdf/config/imap_swe_l1a_variable_attrs.yaml +14 -14
  15. imap_processing/cdf/config/imap_swe_l1b_variable_attrs.yaml +25 -24
  16. imap_processing/cdf/config/imap_swe_l2_variable_attrs.yaml +238 -0
  17. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +100 -92
  18. imap_processing/cdf/utils.py +2 -2
  19. imap_processing/cli.py +45 -9
  20. imap_processing/codice/codice_l1a.py +104 -58
  21. imap_processing/codice/constants.py +111 -155
  22. imap_processing/codice/data/esa_sweep_values.csv +256 -256
  23. imap_processing/codice/data/lo_stepping_values.csv +128 -128
  24. imap_processing/ena_maps/ena_maps.py +519 -0
  25. imap_processing/ena_maps/utils/map_utils.py +145 -0
  26. imap_processing/ena_maps/utils/spatial_utils.py +226 -0
  27. imap_processing/glows/__init__.py +3 -0
  28. imap_processing/glows/ancillary/imap_glows_pipeline_settings_v001.json +52 -0
  29. imap_processing/glows/l1a/glows_l1a.py +72 -14
  30. imap_processing/glows/l1b/glows_l1b.py +2 -1
  31. imap_processing/glows/l1b/glows_l1b_data.py +25 -1
  32. imap_processing/glows/l2/glows_l2.py +324 -0
  33. imap_processing/glows/l2/glows_l2_data.py +156 -51
  34. imap_processing/hi/l1a/science_direct_event.py +57 -51
  35. imap_processing/hi/l1b/hi_l1b.py +43 -28
  36. imap_processing/hi/l1c/hi_l1c.py +225 -42
  37. imap_processing/hi/utils.py +20 -3
  38. imap_processing/hit/l0/constants.py +2 -2
  39. imap_processing/hit/l0/decom_hit.py +1 -1
  40. imap_processing/hit/l1a/hit_l1a.py +94 -13
  41. imap_processing/hit/l1b/hit_l1b.py +158 -9
  42. imap_processing/ialirt/l0/process_codicehi.py +156 -0
  43. imap_processing/ialirt/l0/process_codicelo.py +5 -2
  44. imap_processing/ialirt/packet_definitions/ialirt.xml +28 -20
  45. imap_processing/ialirt/packet_definitions/ialirt_codicehi.xml +241 -0
  46. imap_processing/ialirt/packet_definitions/ialirt_swapi.xml +170 -0
  47. imap_processing/ialirt/packet_definitions/ialirt_swe.xml +258 -0
  48. imap_processing/ialirt/process_ephemeris.py +72 -40
  49. imap_processing/idex/decode.py +241 -0
  50. imap_processing/idex/idex_l1a.py +143 -81
  51. imap_processing/idex/idex_l1b.py +244 -10
  52. imap_processing/lo/l0/lo_science.py +61 -0
  53. imap_processing/lo/l1a/lo_l1a.py +98 -10
  54. imap_processing/lo/l1b/lo_l1b.py +2 -2
  55. imap_processing/lo/l1c/lo_l1c.py +2 -2
  56. imap_processing/lo/packet_definitions/lo_xtce.xml +1082 -9178
  57. imap_processing/mag/l0/decom_mag.py +2 -2
  58. imap_processing/mag/l1a/mag_l1a.py +7 -7
  59. imap_processing/mag/l1a/mag_l1a_data.py +62 -30
  60. imap_processing/mag/l1b/mag_l1b.py +11 -6
  61. imap_processing/quality_flags.py +18 -3
  62. imap_processing/spice/geometry.py +149 -177
  63. imap_processing/spice/kernels.py +26 -26
  64. imap_processing/spice/spin.py +233 -0
  65. imap_processing/spice/time.py +96 -31
  66. imap_processing/swapi/l1/swapi_l1.py +60 -31
  67. imap_processing/swapi/packet_definitions/swapi_packet_definition.xml +363 -384
  68. imap_processing/swe/l1a/swe_l1a.py +8 -3
  69. imap_processing/swe/l1a/swe_science.py +24 -24
  70. imap_processing/swe/l1b/swe_l1b.py +2 -1
  71. imap_processing/swe/l1b/swe_l1b_science.py +181 -122
  72. imap_processing/swe/l2/swe_l2.py +337 -70
  73. imap_processing/swe/utils/swe_utils.py +28 -0
  74. imap_processing/tests/cdf/test_utils.py +2 -2
  75. imap_processing/tests/codice/conftest.py +20 -17
  76. imap_processing/tests/codice/data/validation/imap_codice_l1a_hskp_20241110193622_v0.0.0.cdf +0 -0
  77. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-counters-aggregated_20241110193700_v0.0.0.cdf +0 -0
  78. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-counters-singles_20241110193700_v0.0.0.cdf +0 -0
  79. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-angular_20241110193700_v0.0.0.cdf +0 -0
  80. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-priority_20241110193700_v0.0.0.cdf +0 -0
  81. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-species_20241110193700_v0.0.0.cdf +0 -0
  82. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-angular_20241110193700_v0.0.0.cdf +0 -0
  83. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-priority_20241110193700_v0.0.0.cdf +0 -0
  84. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-species_20241110193700_v0.0.0.cdf +0 -0
  85. imap_processing/tests/codice/test_codice_l0.py +55 -121
  86. imap_processing/tests/codice/test_codice_l1a.py +147 -59
  87. imap_processing/tests/conftest.py +81 -22
  88. imap_processing/tests/ena_maps/test_ena_maps.py +309 -0
  89. imap_processing/tests/ena_maps/test_map_utils.py +286 -0
  90. imap_processing/tests/ena_maps/test_spatial_utils.py +161 -0
  91. imap_processing/tests/glows/conftest.py +7 -1
  92. imap_processing/tests/glows/test_glows_l1a_cdf.py +3 -7
  93. imap_processing/tests/glows/test_glows_l1a_data.py +34 -6
  94. imap_processing/tests/glows/test_glows_l1b_data.py +29 -17
  95. imap_processing/tests/glows/test_glows_l2.py +101 -0
  96. imap_processing/tests/hi/conftest.py +3 -3
  97. imap_processing/tests/hi/data/l1/imap_hi_l1b_45sensor-de_20250415_v999.cdf +0 -0
  98. imap_processing/tests/hi/data/l1/imap_his_pset-calibration-prod-config_20240101_v001.csv +31 -0
  99. imap_processing/tests/hi/test_hi_l1b.py +14 -9
  100. imap_processing/tests/hi/test_hi_l1c.py +136 -36
  101. imap_processing/tests/hi/test_l1a.py +0 -2
  102. imap_processing/tests/hi/test_science_direct_event.py +18 -14
  103. imap_processing/tests/hi/test_utils.py +16 -11
  104. imap_processing/tests/hit/helpers/__init__.py +0 -0
  105. imap_processing/tests/hit/helpers/l1_validation.py +405 -0
  106. imap_processing/tests/hit/test_data/sci_sample.ccsds +0 -0
  107. imap_processing/tests/hit/test_decom_hit.py +8 -10
  108. imap_processing/tests/hit/test_hit_l1a.py +117 -180
  109. imap_processing/tests/hit/test_hit_l1b.py +149 -55
  110. imap_processing/tests/hit/validation_data/hit_l1b_standard_sample2_nsrl_v4_3decimals.csv +62 -0
  111. imap_processing/tests/hit/validation_data/sci_sample_raw.csv +62 -0
  112. imap_processing/tests/ialirt/test_data/l0/20240827095047_SWE_IALIRT_packet.bin +0 -0
  113. imap_processing/tests/ialirt/test_data/l0/BinLog CCSDS_FRAG_TLM_20240826_152323Z_IALIRT_data_for_SDC.bin +0 -0
  114. imap_processing/tests/ialirt/test_data/l0/eu_SWP_IAL_20240826_152033.csv +644 -0
  115. imap_processing/tests/ialirt/test_data/l0/hi_fsw_view_1_ccsds.bin +0 -0
  116. imap_processing/tests/ialirt/test_data/l0/idle_export_eu.SWE_IALIRT_20240827_093852.csv +914 -0
  117. imap_processing/tests/ialirt/test_data/l0/imap_codice_l1a_hi-ialirt_20240523200000_v0.0.0.cdf +0 -0
  118. imap_processing/tests/ialirt/unit/test_process_codicehi.py +106 -0
  119. imap_processing/tests/ialirt/unit/test_process_ephemeris.py +33 -5
  120. imap_processing/tests/ialirt/unit/test_process_swapi.py +85 -0
  121. imap_processing/tests/ialirt/unit/test_process_swe.py +106 -0
  122. imap_processing/tests/idex/conftest.py +29 -1
  123. imap_processing/tests/idex/test_data/compressed_2023_102_14_24_55.pkts +0 -0
  124. imap_processing/tests/idex/test_data/non_compressed_2023_102_14_22_26.pkts +0 -0
  125. imap_processing/tests/idex/test_idex_l0.py +6 -3
  126. imap_processing/tests/idex/test_idex_l1a.py +151 -1
  127. imap_processing/tests/idex/test_idex_l1b.py +124 -2
  128. imap_processing/tests/lo/test_lo_l1a.py +62 -2
  129. imap_processing/tests/lo/test_lo_science.py +85 -0
  130. imap_processing/tests/lo/validation_data/Instrument_FM1_T104_R129_20240803_ILO_SPIN_EU.csv +2 -0
  131. imap_processing/tests/mag/conftest.py +16 -0
  132. imap_processing/tests/mag/test_mag_decom.py +6 -4
  133. imap_processing/tests/mag/test_mag_l1a.py +36 -7
  134. imap_processing/tests/mag/test_mag_l1b.py +55 -4
  135. imap_processing/tests/mag/test_mag_validation.py +148 -0
  136. imap_processing/tests/mag/validation/L1a/T001/all_p_ones.txt +19200 -0
  137. imap_processing/tests/mag/validation/L1a/T001/mag-l0-l1a-t001-in.bin +0 -0
  138. imap_processing/tests/mag/validation/L1a/T001/mag-l0-l1a-t001-out.csv +17 -0
  139. imap_processing/tests/mag/validation/L1a/T002/all_n_ones.txt +19200 -0
  140. imap_processing/tests/mag/validation/L1a/T002/mag-l0-l1a-t002-in.bin +0 -0
  141. imap_processing/tests/mag/validation/L1a/T002/mag-l0-l1a-t002-out.csv +17 -0
  142. imap_processing/tests/mag/validation/L1a/T003/field_like.txt +19200 -0
  143. imap_processing/tests/mag/validation/L1a/T003/mag-l0-l1a-t003-in.bin +0 -0
  144. imap_processing/tests/mag/validation/L1a/T003/mag-l0-l1a-t003-out.csv +17 -0
  145. imap_processing/tests/mag/validation/L1a/T004/field_like.txt +19200 -0
  146. imap_processing/tests/mag/validation/L1a/T004/mag-l0-l1a-t004-in.bin +0 -0
  147. imap_processing/tests/mag/validation/L1a/T004/mag-l0-l1a-t004-out.csv +17 -0
  148. imap_processing/tests/mag/validation/L1a/T005/field_like_range_change.txt +19200 -0
  149. imap_processing/tests/mag/validation/L1a/T005/mag-l0-l1a-t005-in.bin +0 -0
  150. imap_processing/tests/mag/validation/L1a/T005/mag-l0-l1a-t005-out.csv +17 -0
  151. imap_processing/tests/mag/validation/L1a/T006/hdr_field.txt +19200 -0
  152. imap_processing/tests/mag/validation/L1a/T006/mag-l0-l1a-t006-in.bin +0 -0
  153. imap_processing/tests/mag/validation/L1a/T006/mag-l0-l1a-t006-out.csv +17 -0
  154. imap_processing/tests/mag/validation/L1a/T007/hdr_field_and_range_change.txt +19200 -0
  155. imap_processing/tests/mag/validation/L1a/T007/mag-l0-l1a-t007-in.bin +0 -0
  156. imap_processing/tests/mag/validation/L1a/T007/mag-l0-l1a-t007-out.csv +17 -0
  157. imap_processing/tests/mag/validation/L1a/T008/field_like_range_change.txt +19200 -0
  158. imap_processing/tests/mag/validation/L1a/T008/mag-l0-l1a-t008-in.bin +0 -0
  159. imap_processing/tests/mag/validation/L1a/T008/mag-l0-l1a-t008-out.csv +17 -0
  160. imap_processing/tests/mag/validation/L1b/T009/data.bin +0 -0
  161. imap_processing/tests/mag/validation/L1b/T009/field_like_all_ranges.txt +19200 -0
  162. imap_processing/tests/mag/validation/L1b/T009/mag-l1a-l1b-t009-in.csv +17 -0
  163. imap_processing/tests/mag/validation/L1b/T009/mag-l1a-l1b-t009-magi-out.csv +17 -0
  164. imap_processing/tests/mag/validation/L1b/T009/mag-l1a-l1b-t009-mago-out.csv +17 -0
  165. imap_processing/tests/mag/validation/L1b/T010/data.bin +0 -0
  166. imap_processing/tests/mag/validation/L1b/T010/field_like_all_ranges.txt +19200 -0
  167. imap_processing/tests/mag/validation/L1b/T010/mag-l1a-l1b-t010-in.csv +17 -0
  168. imap_processing/tests/mag/validation/L1b/T010/mag-l1a-l1b-t010-magi-out.csv +17 -0
  169. imap_processing/tests/mag/validation/L1b/T010/mag-l1a-l1b-t010-mago-out.csv +17 -0
  170. imap_processing/tests/mag/validation/L1b/T011/data.bin +0 -0
  171. imap_processing/tests/mag/validation/L1b/T011/field_like_all_ranges.txt +19200 -0
  172. imap_processing/tests/mag/validation/L1b/T011/mag-l1a-l1b-t011-in.csv +17 -0
  173. imap_processing/tests/mag/validation/L1b/T011/mag-l1a-l1b-t011-magi-out.csv +17 -0
  174. imap_processing/tests/mag/validation/L1b/T011/mag-l1a-l1b-t011-mago-out.csv +17 -0
  175. imap_processing/tests/spice/test_geometry.py +128 -133
  176. imap_processing/tests/spice/test_kernels.py +37 -37
  177. imap_processing/tests/spice/test_spin.py +184 -0
  178. imap_processing/tests/spice/test_time.py +43 -20
  179. imap_processing/tests/swapi/test_swapi_l1.py +11 -10
  180. imap_processing/tests/swapi/test_swapi_l2.py +13 -3
  181. imap_processing/tests/swe/test_swe_l1a.py +1 -1
  182. imap_processing/tests/swe/test_swe_l1b.py +20 -3
  183. imap_processing/tests/swe/test_swe_l1b_science.py +54 -35
  184. imap_processing/tests/swe/test_swe_l2.py +148 -5
  185. imap_processing/tests/test_cli.py +39 -7
  186. imap_processing/tests/test_quality_flags.py +19 -19
  187. imap_processing/tests/test_utils.py +3 -2
  188. imap_processing/tests/ultra/test_data/l0/ultra45_raw_sc_ultrarawimg_withFSWcalcs_FM45_40P_Phi28p5_BeamCal_LinearScan_phi2850_theta-000_20240207T102740.csv +3314 -3314
  189. imap_processing/tests/ultra/test_data/mock_data.py +161 -0
  190. imap_processing/tests/ultra/unit/conftest.py +73 -0
  191. imap_processing/tests/ultra/unit/test_badtimes.py +58 -0
  192. imap_processing/tests/ultra/unit/test_cullingmask.py +87 -0
  193. imap_processing/tests/ultra/unit/test_de.py +61 -60
  194. imap_processing/tests/ultra/unit/test_ultra_l1a.py +3 -3
  195. imap_processing/tests/ultra/unit/test_ultra_l1b.py +51 -77
  196. imap_processing/tests/ultra/unit/test_ultra_l1b_annotated.py +5 -5
  197. imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py +114 -0
  198. imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +86 -26
  199. imap_processing/tests/ultra/unit/test_ultra_l1c.py +1 -1
  200. imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +3 -3
  201. imap_processing/ultra/constants.py +11 -1
  202. imap_processing/ultra/l1a/ultra_l1a.py +2 -2
  203. imap_processing/ultra/l1b/badtimes.py +22 -5
  204. imap_processing/ultra/l1b/cullingmask.py +31 -5
  205. imap_processing/ultra/l1b/de.py +32 -37
  206. imap_processing/ultra/l1b/extendedspin.py +44 -20
  207. imap_processing/ultra/l1b/ultra_l1b.py +21 -22
  208. imap_processing/ultra/l1b/ultra_l1b_culling.py +190 -0
  209. imap_processing/ultra/l1b/ultra_l1b_extended.py +81 -30
  210. imap_processing/ultra/l1c/histogram.py +6 -2
  211. imap_processing/ultra/l1c/pset.py +6 -2
  212. imap_processing/ultra/l1c/ultra_l1c.py +2 -3
  213. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +4 -3
  214. imap_processing/ultra/utils/ultra_l1_utils.py +70 -14
  215. imap_processing/utils.py +2 -2
  216. {imap_processing-0.9.0.dist-info → imap_processing-0.11.0.dist-info}/METADATA +7 -2
  217. {imap_processing-0.9.0.dist-info → imap_processing-0.11.0.dist-info}/RECORD +235 -152
  218. imap_processing/tests/codice/data/eu_unit_lookup_table.csv +0 -101
  219. imap_processing/tests/codice/data/idle_export_eu.COD_NHK_20230822_122700 2.csv +0 -100
  220. imap_processing/tests/codice/data/idle_export_raw.COD_NHK_20230822_122700.csv +0 -100
  221. imap_processing/tests/codice/data/imap_codice_l0_raw_20241110_v001.pkts +0 -0
  222. imap_processing/tests/hi/test_data/l1a/imap_hi_l1a_45sensor-de_20250415_v000.cdf +0 -0
  223. imap_processing/tests/hit/test_data/sci_sample1.ccsds +0 -0
  224. imap_processing/tests/ultra/unit/test_spatial_utils.py +0 -125
  225. imap_processing/ultra/utils/spatial_utils.py +0 -221
  226. /imap_processing/tests/hi/{test_data → data}/l0/20231030_H45_APP_NHK.bin +0 -0
  227. /imap_processing/tests/hi/{test_data → data}/l0/20231030_H45_APP_NHK.csv +0 -0
  228. /imap_processing/tests/hi/{test_data → data}/l0/20231030_H45_SCI_CNT.bin +0 -0
  229. /imap_processing/tests/hi/{test_data → data}/l0/20231030_H45_SCI_DE.bin +0 -0
  230. /imap_processing/tests/hi/{test_data → data}/l0/H90_NHK_20241104.bin +0 -0
  231. /imap_processing/tests/hi/{test_data → data}/l0/H90_sci_cnt_20241104.bin +0 -0
  232. /imap_processing/tests/hi/{test_data → data}/l0/H90_sci_de_20241104.bin +0 -0
  233. /imap_processing/tests/hi/{test_data → data}/l0/README.txt +0 -0
  234. /imap_processing/tests/idex/{imap_idex_l0_raw_20231214_v001.pkts → test_data/imap_idex_l0_raw_20231214_v001.pkts} +0 -0
  235. /imap_processing/tests/idex/{impact_14_tof_high_data.txt → test_data/impact_14_tof_high_data.txt} +0 -0
  236. /imap_processing/tests/mag/{imap_mag_l1a_norm-magi_20251017_v001.cdf → validation/imap_mag_l1a_norm-magi_20251017_v001.cdf} +0 -0
  237. /imap_processing/tests/mag/{mag_l0_test_data.pkts → validation/mag_l0_test_data.pkts} +0 -0
  238. /imap_processing/tests/mag/{mag_l0_test_output.csv → validation/mag_l0_test_output.csv} +0 -0
  239. /imap_processing/tests/mag/{mag_l1_test_data.pkts → validation/mag_l1_test_data.pkts} +0 -0
  240. /imap_processing/tests/mag/{mag_l1a_test_output.csv → validation/mag_l1a_test_output.csv} +0 -0
  241. {imap_processing-0.9.0.dist-info → imap_processing-0.11.0.dist-info}/LICENSE +0 -0
  242. {imap_processing-0.9.0.dist-info → imap_processing-0.11.0.dist-info}/WHEEL +0 -0
  243. {imap_processing-0.9.0.dist-info → imap_processing-0.11.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,309 @@
1
+ """Test classes and methods in ena_maps.py."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest import mock
6
+
7
+ import numpy as np
8
+ import pytest
9
+ import xarray as xr
10
+
11
+ from imap_processing.ena_maps import ena_maps
12
+ from imap_processing.spice import geometry
13
+ from imap_processing.tests.ultra.test_data.mock_data import mock_l1c_pset_product
14
+
15
+
16
+ @pytest.fixture()
17
+ def l1c_pset_products():
18
+ """Make fake L1C Ultra PSET products for testing"""
19
+ l1c_spatial_bin_spacing_deg = 10
20
+ 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
+ ],
37
+ }
38
+
39
+
40
+ class TestUltraPointingSet:
41
+ @pytest.fixture(autouse=True)
42
+ def _setup_ultra_l1c_pset_products(self, l1c_pset_products):
43
+ """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"]
46
+
47
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
48
+ def test_instantiate(self):
49
+ """Test instantiation of UltraPointingSet"""
50
+ ultra_psets = [
51
+ ena_maps.UltraPointingSet(
52
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
53
+ l1c_dataset=l1c_product,
54
+ )
55
+ for l1c_product in self.l1c_pset_products
56
+ ]
57
+
58
+ for ultra_pset in ultra_psets:
59
+ # Check tiling is rectangular
60
+ assert ultra_pset.tiling_type == ena_maps.SkyTilingType.RECTANGULAR
61
+
62
+ # Check that the reference frame is correctly set
63
+ assert ultra_pset.spice_reference_frame == geometry.SpiceFrame.IMAP_DPS
64
+
65
+ # Check the number of points is (360/0.5) * (180/0.5)
66
+ np.testing.assert_equal(
67
+ ultra_pset.num_points,
68
+ int(360 * 180 / (self.l1c_spatial_bin_spacing_deg**2)),
69
+ )
70
+
71
+ # Check the repr exists
72
+ assert "UltraPointingSet" in repr(ultra_pset)
73
+
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,
88
+ )
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,
97
+ )
98
+
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
102
+
103
+ with pytest.raises(
104
+ ValueError, match="Azimuth and elevation bin spacing do not match:"
105
+ ):
106
+ ena_maps.UltraPointingSet(
107
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
108
+ l1c_dataset=uneven_az_dataset,
109
+ )
110
+
111
+
112
+ class TestRectangularSkyMap:
113
+ @pytest.fixture(autouse=True)
114
+ def _setup_ultra_l1c_pset_products(self, l1c_pset_products):
115
+ """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"]
118
+ self.ultra_psets = [
119
+ ena_maps.UltraPointingSet(
120
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
121
+ l1c_dataset=l1c_product,
122
+ )
123
+ for l1c_product in self.l1c_pset_products
124
+ ]
125
+
126
+ def test_instantiate(self):
127
+ """Test instantiation of RectangularSkyMap"""
128
+ rm = ena_maps.RectangularSkyMap(
129
+ spacing_deg=2,
130
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
131
+ )
132
+
133
+ # Check that the map is empty
134
+ assert rm.data_dict == {}
135
+
136
+ # Check that the reference frame is correctly set
137
+ assert rm.spice_reference_frame == geometry.SpiceFrame.ECLIPJ2000
138
+
139
+ # Check the number of points is (360/2) * (180/2)
140
+ np.testing.assert_equal(rm.num_points, int(360 * 180 / 4))
141
+
142
+ # Check the repr exists
143
+ assert "RectangularSkyMap" in repr(rm)
144
+
145
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
146
+ @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):
148
+ """
149
+ Test projection of PSET values to Rect. Map w "push" index matching method.
150
+
151
+ If frame_transform_az_el is mocked to return the az and el unchanged, and the
152
+ map has the same spacing as the PSETs, then the map should have
153
+ the same values as the PSETs, summed.
154
+ """
155
+ index_matching_method = ena_maps.IndexMatchMethod.PUSH
156
+
157
+ pset_spacing_deg = self.ultra_psets[0].spacing_deg
158
+
159
+ # Mock frame_transform to return the az and el unchanged
160
+ mock_frame_transform_az_el.side_effect = (
161
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
162
+ )
163
+
164
+ rectangular_map = ena_maps.RectangularSkyMap(
165
+ spacing_deg=pset_spacing_deg,
166
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
167
+ )
168
+
169
+ # Project each PSET's values to the map
170
+ for ultra_pset in self.ultra_psets:
171
+ rectangular_map.project_pset_values_to_map(
172
+ ultra_pset,
173
+ value_keys=["counts", "exposure_time"],
174
+ index_match_method=index_matching_method,
175
+ )
176
+
177
+ # Check that the map has been updated
178
+ assert rectangular_map.data_dict != {}
179
+
180
+ # 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)
184
+
185
+ np.testing.assert_allclose(
186
+ rectangular_map.data_dict["counts"],
187
+ simple_summed_pset_counts,
188
+ )
189
+
190
+ @pytest.mark.usefixtures("_setup_ultra_l1c_pset_products")
191
+ @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."""
194
+
195
+ index_matching_method = ena_maps.IndexMatchMethod.PULL
196
+ rectangular_map = ena_maps.RectangularSkyMap(
197
+ spacing_deg=10,
198
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
199
+ )
200
+
201
+ with pytest.raises(NotImplementedError):
202
+ rectangular_map.project_pset_values_to_map(
203
+ self.ultra_psets[0],
204
+ value_keys=["counts", "exposure_time"],
205
+ index_match_method=index_matching_method,
206
+ )
207
+
208
+
209
+ class TestIndexMatching:
210
+ @pytest.fixture(autouse=True)
211
+ def _setup_ultra_l1c_pset_products(self, l1c_pset_products):
212
+ """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"]
215
+
216
+ @pytest.mark.parametrize(
217
+ "map_spacing_deg",
218
+ [0.5, 1, 10],
219
+ )
220
+ @mock.patch("imap_processing.spice.geometry.frame_transform_az_el")
221
+ def test_match_coords_to_indices_pset_to_rect_map(
222
+ self, mock_frame_transform_az_el, map_spacing_deg
223
+ ):
224
+ # Mock frame_transform to return the az and el unchanged
225
+ mock_frame_transform_az_el.side_effect = (
226
+ lambda et, az_el, from_frame, to_frame, degrees: az_el
227
+ )
228
+
229
+ # Mock a PSET, overriding the az/el points
230
+ mock_pset_input_frame = ena_maps.UltraPointingSet(
231
+ l1c_dataset=self.l1c_pset_products[0],
232
+ spice_reference_frame=geometry.SpiceFrame.IMAP_DPS,
233
+ )
234
+ manual_az_el_coords = np.array(
235
+ [
236
+ [0, -90], # always -> RectangularSkyMap pixel 0
237
+ [0.4999999, -90],
238
+ [180.5, -89.5],
239
+ [359.5, -89.5],
240
+ [0.5, 0],
241
+ [180.5, 0],
242
+ [359.5, 0],
243
+ [0.5, 89.5],
244
+ [180.5, 89.5],
245
+ [359.5, 89.5],
246
+ [359.999999, 89.99999],
247
+ ]
248
+ )
249
+ mock_pset_input_frame.az_el_points = np.deg2rad(manual_az_el_coords)
250
+
251
+ # Manually calculate the resulting 1D pixel indices for each az/el pair
252
+ # (num of pixels in an az row spanning 180 deg of elevation) * (current az row)
253
+ # + (pixel along in current az row)
254
+ expected_output_pixel = np.array(
255
+ [
256
+ (az // map_spacing_deg) * (180 // map_spacing_deg)
257
+ + ((90 + el) // map_spacing_deg)
258
+ for [az, el] in manual_az_el_coords
259
+ ]
260
+ )
261
+
262
+ # Mock the rectangular map and check the output values
263
+ mock_rect_map = ena_maps.RectangularSkyMap(
264
+ spacing_deg=map_spacing_deg,
265
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
266
+ )
267
+ flat_indices_input_grid_output_frame = ena_maps.match_coords_to_indices(
268
+ mock_pset_input_frame, mock_rect_map
269
+ )
270
+ assert mock_rect_map.num_points == 360 * 180 / map_spacing_deg**2
271
+ assert len(flat_indices_input_grid_output_frame) == len(manual_az_el_coords)
272
+ np.testing.assert_equal(
273
+ flat_indices_input_grid_output_frame, expected_output_pixel
274
+ )
275
+
276
+ # Check that the map's az/el points at the matched indices
277
+ # 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
+ ]
281
+ np.testing.assert_allclose(
282
+ matched_map_az_el[:, 0],
283
+ mock_pset_input_frame.az_el_points[:, 0],
284
+ atol=np.deg2rad(map_spacing_deg),
285
+ )
286
+
287
+ def test_match_coords_to_indices_pset_to_healpix_map_other_map(
288
+ self,
289
+ ):
290
+ mock_pset_input_frame = ena_maps.UltraPointingSet(
291
+ l1c_dataset=self.l1c_pset_products[0],
292
+ spice_reference_frame=geometry.SpiceFrame.ECLIPJ2000,
293
+ )
294
+
295
+ # Until implemented, just change the tiling on a RectangularSkyMap
296
+ mock_hp_map = ena_maps.RectangularSkyMap(
297
+ spacing_deg=2,
298
+ spice_frame=geometry.SpiceFrame.ECLIPJ2000,
299
+ )
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)
305
+
306
+ mock_other_map = mock_hp_map
307
+ mock_other_map.tiling_type = "INVALID"
308
+ 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)
@@ -0,0 +1,286 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from imap_processing.ena_maps.utils import map_utils
5
+
6
+
7
+ class TestENAMapMappingUtils:
8
+ def test_bin_single_array_at_indices(
9
+ self,
10
+ ):
11
+ """Test coverage for bin_single_array_at_indices function w/ simple 1D input"""
12
+ value_array = np.array([1, 2, 3, 4, 5, 6])
13
+ input_indices = np.array([0, 1, 2, 2, 1, 0])
14
+ projection_indices = np.array([1, 2, 3, 1, 2, 3])
15
+ projection_grid_shape = (5,)
16
+ expected_projection_values = np.array([0, 4, 4, 4, 0])
17
+ projection_values = map_utils.bin_single_array_at_indices(
18
+ value_array,
19
+ input_indices=input_indices,
20
+ projection_indices=projection_indices,
21
+ projection_grid_shape=projection_grid_shape,
22
+ )
23
+ np.testing.assert_equal(projection_values, expected_projection_values)
24
+
25
+ def test_bin_single_array_at_indices_extra_axis(
26
+ self,
27
+ ):
28
+ """Test coverage for bin_single_array_at_indices function w/ simple 2D input,
29
+ Corresponding to an extra axis that is not spatially binned.
30
+ """
31
+ # Binning will occur along axis 0 (combining 1, 2, 3 and 4, 5, 6 separately)
32
+ value_array = np.array(
33
+ [
34
+ [1, 4],
35
+ [2, 5],
36
+ [3, 6],
37
+ ]
38
+ )
39
+ input_indices = np.array([0, 1, 2, 2])
40
+ projection_indices = np.array([1, 0, 1, 6])
41
+ projection_grid_shape = (7, 1)
42
+ expected_projection_values = np.array(
43
+ [
44
+ [2, 5],
45
+ [4, 10],
46
+ [0, 0],
47
+ [0, 0],
48
+ [0, 0],
49
+ [0, 0],
50
+ [3, 6],
51
+ ]
52
+ )
53
+ projection_values = map_utils.bin_single_array_at_indices(
54
+ value_array,
55
+ input_indices=input_indices,
56
+ projection_indices=projection_indices,
57
+ projection_grid_shape=projection_grid_shape,
58
+ )
59
+
60
+ np.testing.assert_equal(projection_values, expected_projection_values)
61
+
62
+ @pytest.mark.parametrize(
63
+ "projection_grid_shape", [(1, 1), (10, 10), (180, 360), (360, 720), (360, 180)]
64
+ )
65
+ @pytest.mark.parametrize(
66
+ "input_grid_shape", [(1, 1), (10, 10), (180, 360), (360, 720), (360, 180)]
67
+ )
68
+ def test_bin_single_array_at_indices_complex_2d(
69
+ self, projection_grid_shape, input_grid_shape
70
+ ):
71
+ """Test coverage for bin_single_array_at_indices function w/ complex 2D input,
72
+ Corresponding to an extra axis that is not spatially binned.
73
+ Parameterized across different input and projection grid shapes.
74
+ """
75
+ np.random.seed(0)
76
+ extra_axis_size = 11 # Another axis which is not spatially binned, e.g. energy
77
+ input_grid_size = np.prod(input_grid_shape)
78
+ projection_grid_size = np.prod(projection_grid_shape)
79
+ value_array = np.random.rand(input_grid_size, extra_axis_size)
80
+ input_indices = np.random.randint(0, input_grid_size, size=1000)
81
+ projection_indices = np.random.randint(0, projection_grid_size, size=1000)
82
+ projection_values = map_utils.bin_single_array_at_indices(
83
+ value_array,
84
+ input_indices=input_indices,
85
+ projection_indices=projection_indices,
86
+ projection_grid_shape=projection_grid_shape,
87
+ )
88
+
89
+ # Explicitly check that the shape of the output is the same as projection grid
90
+ np.testing.assert_equal(
91
+ projection_values.shape,
92
+ (
93
+ projection_grid_size,
94
+ extra_axis_size,
95
+ ),
96
+ )
97
+
98
+ # Create the expected projection values by summing the input values in a loop
99
+ # This is different from the binning function, which uses np.bincount
100
+ expected_projection_values = np.zeros((projection_grid_size, extra_axis_size))
101
+ for ii, ip in zip(input_indices, projection_indices):
102
+ expected_projection_values[ip, :] += value_array[ii, :]
103
+
104
+ np.testing.assert_allclose(projection_values, expected_projection_values)
105
+
106
+ @pytest.mark.parametrize("projection_grid_shape", [(1, 1), (10, 10), (180, 360)])
107
+ @pytest.mark.parametrize("input_grid_shape", [(1, 1), (10, 10), (180, 360)])
108
+ @pytest.mark.parametrize("num_extra_dims", [1, 2, 3, 5])
109
+ def test_bin_single_array_at_indices_complex_3d(
110
+ self, projection_grid_shape, input_grid_shape, num_extra_dims
111
+ ):
112
+ """Test coverage for bin_single_array_at_indices function w/ complex N-Dim input
113
+ Corresponding to 2 extra axes that are not spatially binned.
114
+ Parameterized across different input and projection grid shapes.
115
+ """
116
+ np.random.seed(0)
117
+ extra_axes_sizes = np.full(num_extra_dims, 3, dtype=int).tolist()
118
+ input_grid_size = np.prod(input_grid_shape)
119
+ projection_grid_size = np.prod(projection_grid_shape)
120
+ value_array = np.random.rand(input_grid_size, *extra_axes_sizes)
121
+ input_indices = np.random.randint(0, input_grid_size, size=1000)
122
+ projection_indices = np.random.randint(0, projection_grid_size, size=1000)
123
+ projection_values = map_utils.bin_single_array_at_indices(
124
+ value_array,
125
+ input_indices=input_indices,
126
+ projection_indices=projection_indices,
127
+ projection_grid_shape=projection_grid_shape,
128
+ )
129
+
130
+ # Explicitly check that the shape of the output is the same as projection grid
131
+ np.testing.assert_equal(
132
+ projection_values.shape,
133
+ (projection_grid_size, *extra_axes_sizes),
134
+ )
135
+
136
+ # Create the expected projection values by summing the input values in a loop
137
+ # This is different from the binning function, which uses np.bincount
138
+ expected_projection_values = np.zeros((projection_grid_size, *extra_axes_sizes))
139
+ for ii, ip in zip(input_indices, projection_indices):
140
+ expected_projection_values[ip, ...] += value_array[ii, ...]
141
+
142
+ np.testing.assert_allclose(projection_values, expected_projection_values)
143
+
144
+ # Parameterize by the size of the projection grid,
145
+ # which is not necessarily same size as input grid
146
+ @pytest.mark.parametrize("projection_grid_shape", [(1, 1), (10, 10), (360, 720)])
147
+ def test_bin_values_at_indices_collapse_to_idx_zero(self, projection_grid_shape):
148
+ """Test coverage for bin_values_at_indices function w/ dict of multiple
149
+ 1D input value arrays and a single 2D input value array.
150
+ All input values are binned to the first index of the projection grid.
151
+ Parameterized across different projection grid shapes.
152
+ """
153
+ # 1D input values (2nd will be scalar multiple of 1st)
154
+ input_values_1d_1 = np.array([0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
155
+ scale_factor_1d = 1.5
156
+ input_values_1d_2 = input_values_1d_1 * scale_factor_1d
157
+
158
+ # 2D input values. The second axis (different cols) will be summed independently
159
+ input_values_2d = np.array(
160
+ [
161
+ [-0.5, 0, 0.5],
162
+ [1, 2, 3],
163
+ [4, 5, 6],
164
+ [7, 8, 9],
165
+ [0, 0, 0],
166
+ [0, 0, 0],
167
+ [0, 0, 0],
168
+ [0, 0, 0],
169
+ [0, 0, 0],
170
+ [0, 0, 0],
171
+ [0, 0, 0],
172
+ [0, 0, 0],
173
+ ]
174
+ )
175
+ extra_axis_size_2d = input_values_2d.shape[1]
176
+
177
+ # 3D input values
178
+ input_values_3d = np.zeros((input_values_2d.shape[0], 3, 3))
179
+ input_values_3d[:2] = np.array(
180
+ [
181
+ [
182
+ [1, 2, 3],
183
+ [4, 5, 6],
184
+ [7, 8, 9],
185
+ ],
186
+ [
187
+ [10, 11, 12],
188
+ [13, 14, 15],
189
+ [16, 17, 18],
190
+ ],
191
+ ]
192
+ )
193
+ extra_axes_size_3d = input_values_3d.shape[1:]
194
+
195
+ # Set up the expected projection values
196
+ expected_projection_values_1d_1 = np.zeros(projection_grid_shape).ravel()
197
+ expected_projection_values_1d_1[0] = np.sum(input_values_1d_1)
198
+ expected_projection_values_1d_2 = (
199
+ expected_projection_values_1d_1 * scale_factor_1d
200
+ )
201
+ expected_projection_values_2d = np.zeros(
202
+ (np.prod(projection_grid_shape), extra_axis_size_2d)
203
+ )
204
+ expected_projection_values_2d[0, :] = np.array([11.5, 15, 18.5])
205
+ expected_projection_values_3d = np.zeros(
206
+ (np.prod(projection_grid_shape), *extra_axes_size_3d)
207
+ )
208
+ expected_projection_values_3d[0, :, :] = np.array(
209
+ [
210
+ [11, 13, 15],
211
+ [17, 19, 21],
212
+ [23, 25, 27],
213
+ ]
214
+ )
215
+
216
+ input_values_to_bin = {
217
+ "sum_variable_1d_1": input_values_1d_1,
218
+ "sum_variable_1d_2": input_values_1d_2,
219
+ "sum_variable_2d": np.array(input_values_2d),
220
+ "sum_variable_3d": np.array(input_values_3d),
221
+ }
222
+
223
+ # Set up indices
224
+ input_indices = np.arange(len(input_values_1d_1))
225
+ projection_indices = np.zeros_like(input_indices)
226
+
227
+ output_dict = map_utils.bin_values_at_indices(
228
+ projection_indices=projection_indices,
229
+ projection_grid_shape=projection_grid_shape,
230
+ input_values_to_bin=input_values_to_bin,
231
+ input_indices=input_indices,
232
+ )
233
+
234
+ np.testing.assert_equal(
235
+ output_dict["sum_variable_1d_1"], expected_projection_values_1d_1
236
+ )
237
+ np.testing.assert_equal(
238
+ output_dict["sum_variable_1d_2"], expected_projection_values_1d_2
239
+ )
240
+ np.testing.assert_equal(
241
+ output_dict["sum_variable_2d"], expected_projection_values_2d
242
+ )
243
+ np.testing.assert_equal(
244
+ output_dict["sum_variable_3d"], expected_projection_values_3d
245
+ )
246
+
247
+ def test_bin_values_at_indices_2d_indices_raises(self):
248
+ """2D indices are not supported for binning.
249
+ Test that ValueError is raised."""
250
+ input_values = np.array([1, 2, 3])
251
+ input_indices = np.array([[0, 1], [1, 2]])
252
+ projection_indices = np.array([0, 1, 2])
253
+ projection_grid_shape = (3,)
254
+
255
+ with pytest.raises(
256
+ ValueError,
257
+ match=(
258
+ "Indices must be 1D arrays. If using a rectangular grid, "
259
+ "the indices must be unwrapped."
260
+ ),
261
+ ):
262
+ map_utils.bin_single_array_at_indices(
263
+ input_values,
264
+ input_indices=input_indices,
265
+ projection_indices=projection_indices,
266
+ projection_grid_shape=projection_grid_shape,
267
+ )
268
+
269
+ def test_bin_values_at_indices_mismatched_sizes_raises(self):
270
+ """Mismatched input and projection indices should raise an error.
271
+ Test that ValueError is raised."""
272
+ input_values = np.array([1, 2, 3])
273
+ input_indices = np.array([0, 1, 0, 1])
274
+ projection_indices = np.array([0, 1, 2])
275
+ projection_grid_shape = (3,)
276
+
277
+ with pytest.raises(
278
+ ValueError,
279
+ match=("The number of input and projection indices must be the same"),
280
+ ):
281
+ map_utils.bin_single_array_at_indices(
282
+ input_values,
283
+ input_indices=input_indices,
284
+ projection_indices=projection_indices,
285
+ projection_grid_shape=projection_grid_shape,
286
+ )