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
@@ -7,13 +7,19 @@ import pathlib
7
7
  from abc import ABC, abstractmethod
8
8
  from enum import Enum
9
9
 
10
+ import astropy_healpix.healpy as hp
10
11
  import numpy as np
11
12
  import xarray as xr
12
13
  from numpy.typing import NDArray
13
14
 
14
15
  from imap_processing.cdf.utils import load_cdf
15
16
  from imap_processing.ena_maps.utils import map_utils, spatial_utils
17
+
18
+ # The coordinate names can vary between L1C and L2 data (e.g. azimuth vs longitude),
19
+ # so we define an enum to handle the coordinate names.
20
+ from imap_processing.ena_maps.utils.coordinates import CoordNames
16
21
  from imap_processing.spice import geometry
22
+ from imap_processing.spice.time import ttj2000ns_to_et
17
23
 
18
24
  logger = logging.getLogger(__name__)
19
25
 
@@ -60,7 +66,7 @@ class IndexMatchMethod(Enum):
60
66
  def match_coords_to_indices(
61
67
  input_object: PointingSet | AbstractSkyMap,
62
68
  output_object: PointingSet | AbstractSkyMap,
63
- event_time: float | None = None,
69
+ event_et: float | None = None,
64
70
  ) -> NDArray:
65
71
  """
66
72
  Find the output indices corresponding to each input coord between 2 spatial objects.
@@ -92,10 +98,11 @@ def match_coords_to_indices(
92
98
  The object containing a grid or tessellation of spatial pixels
93
99
  into which the input spatial pixel centers will 'land', and be matched to
94
100
  corresponding pixel 1D indices in the output frame.
95
- event_time : float, optional
101
+ event_et : float, optional
96
102
  Event time at which to transform the input spatial object to the output frame.
97
103
  This can be manually specified, e.g., for converting between Maps which do not
98
104
  contain an epoch value.
105
+ If specified, must be in SPICE compatible ET.
99
106
  The default value is None, in which case the event time of the PointingSet
100
107
  object is used.
101
108
 
@@ -121,12 +128,14 @@ def match_coords_to_indices(
121
128
  if isinstance(input_object, PointingSet) and isinstance(output_object, PointingSet):
122
129
  raise ValueError("Cannot match indices between two PointingSet objects.")
123
130
 
124
- # If event_time is not specified, use event_time of the PointingSet, if present.
125
- if event_time is None:
131
+ # If event_et is not specified, use epoch of the PointingSet, if present.
132
+ # The epoch will be in units of terrestrial time (TT) J2000 nanoseconds,
133
+ # which must be converted to ephemeris time (ET) for SPICE.
134
+ if event_et is None:
126
135
  if isinstance(input_object, PointingSet):
127
- event_time = input_object.data["epoch"].values
136
+ event_et = ttj2000ns_to_et(input_object.data["epoch"].values)
128
137
  elif isinstance(output_object, PointingSet):
129
- event_time = output_object.data["epoch"].values
138
+ event_et = ttj2000ns_to_et(output_object.data["epoch"].values)
130
139
  else:
131
140
  raise ValueError(
132
141
  "Event time must be specified if both objects are SkyMaps."
@@ -137,11 +146,11 @@ def match_coords_to_indices(
137
146
 
138
147
  # Transform the input pixel centers to the output frame
139
148
  input_obj_az_el_output_frame = geometry.frame_transform_az_el(
140
- et=event_time,
149
+ et=event_et,
141
150
  az_el=input_obj_az_el_input_frame,
142
151
  from_frame=input_object.spice_reference_frame,
143
152
  to_frame=output_object.spice_reference_frame,
144
- degrees=False,
153
+ degrees=True,
145
154
  )
146
155
 
147
156
  # The way indices are matched depends on the tiling type of the 2nd object
@@ -174,29 +183,17 @@ def match_coords_to_indices(
174
183
  elif output_object.tiling_type is SkyTilingType.HEALPIX:
175
184
  # To match to a Healpix tessellation, we need to use the healpy function ang2pix
176
185
  # which directly returns the index on the output frame's Healpix tessellation.
177
- """
178
- Leaving this as a placeholder for now, so we don't yet
179
- need to add a healpy dependency. It will look something like the
180
- following code, much simpler than the rectangular case:
181
-
182
- ```python
183
- import healpy as hp
184
186
  flat_indices_input_grid_output_frame = hp.ang2pix(
185
- nside=spatial_object_output_frame.nside,
186
- theta=np.rad2deg(obj1_az_el_points_frame2[:, 0]), # Lon
187
- phi=np.rad2deg(obj1_az_el_points_frame2[:, 1]), # Lat
188
- nest=False,
187
+ nside=output_object.nside,
188
+ theta=input_obj_az_el_output_frame[:, 0], # Lon in degrees
189
+ phi=input_obj_az_el_output_frame[:, 1], # Lat in degrees
190
+ nest=output_object.nested,
189
191
  lonlat=True,
190
192
  )
191
- ```
192
- """
193
- raise NotImplementedError(
194
- "Index matching for output tiling type Healpix is not yet implemented."
195
- )
196
-
197
193
  else:
198
194
  raise ValueError(
199
195
  "Tiling type of the output frame must be either RECTANGULAR or HEALPIX."
196
+ f"Received: {output_object.tiling_type}"
200
197
  )
201
198
 
202
199
  return flat_indices_input_grid_output_frame
@@ -207,6 +204,9 @@ class PointingSet(ABC):
207
204
  """
208
205
  Abstract class to contain pointing set (PSET) data in the context of ENA sky maps.
209
206
 
207
+ Any spatial axes - (azimuth, elevation) for Rectangularly gridded tilings or
208
+ (pixel index) for Healpix - must be stored in the last axis/axes of each data array.
209
+
210
210
  Parameters
211
211
  ----------
212
212
  dataset : xr.Dataset
@@ -222,6 +222,49 @@ class PointingSet(ABC):
222
222
  self.num_points = 0
223
223
  self.az_el_points = np.zeros((self.num_points, 2))
224
224
  self.data = xr.Dataset()
225
+ self.spatial_coords: tuple[str, ...] = ()
226
+
227
+ @property
228
+ def unwrapped_dims_dict(self) -> dict[str, tuple[str, ...]]:
229
+ """
230
+ Get dimensions of each variable in the pointing set, with only 1 spatial dim.
231
+
232
+ Returns
233
+ -------
234
+ unwrapped_dims_dict : dict[str, tuple[str, ...]]
235
+ Dictionary of variable names and their dimensions, with only 1 spatial dim.
236
+ The generic pixel dimension is always included.
237
+ E.g.: {"counts": ("epoch", "energy_bin_center", "pixel")} .
238
+ """
239
+ variable_dims = {}
240
+ for var_name in self.data.data_vars:
241
+ pset_dims = self.data[var_name].dims
242
+ non_spatial_dims = tuple(
243
+ dim for dim in pset_dims if dim not in self.spatial_coords
244
+ )
245
+
246
+ variable_dims[var_name] = (
247
+ *non_spatial_dims,
248
+ CoordNames.GENERIC_PIXEL.value,
249
+ )
250
+ return variable_dims
251
+
252
+ @property
253
+ def non_spatial_coords(self) -> dict[str, xr.DataArray]:
254
+ """
255
+ Get the non-spatial coordinates of the pointing set.
256
+
257
+ Returns
258
+ -------
259
+ non_spatial_coords : dict[str, xr.DataArray]
260
+ Dictionary of coordinate names and their data arrays.
261
+ E.g.: {"epoch": [12345,], "energy": [100, 200, 300]} .
262
+ """
263
+ non_spatial_coords = {}
264
+ for coord_name in self.data.coords:
265
+ if coord_name not in self.spatial_coords:
266
+ non_spatial_coords[coord_name] = self.data[coord_name]
267
+ return non_spatial_coords
225
268
 
226
269
  def __repr__(self) -> str:
227
270
  """
@@ -233,34 +276,32 @@ class PointingSet(ABC):
233
276
  String representation of the pointing set.
234
277
  """
235
278
  return (
236
- f"{self.__class__} PointingSet"
279
+ f"{self.__class__.__name__} PointingSet"
237
280
  f"(spice_reference_frame={self.spice_reference_frame})"
238
281
  )
239
282
 
240
283
 
241
- class UltraPointingSet(PointingSet):
284
+ class RectangularPointingSet(PointingSet):
242
285
  """
243
- PSET object specifically for ULTRA data, nominally at Level 1C.
286
+ Pointing set object for rectangularly tiled data. Currently used in testing.
244
287
 
245
288
  Parameters
246
289
  ----------
247
290
  l1c_dataset : xr.Dataset | pathlib.Path | str
248
291
  L1c xarray dataset containing the pointing set data or the path to the dataset.
249
- Currently, the dataset is expected to be in a rectangular grid,
292
+ Currently, the dataset is expected to be tiled in a rectangular grid,
250
293
  with data_vars indexed along the coordinates:
251
294
  - 'epoch' : time value (1 value per PSET)
252
- - 'azimuth_bin_center' : azimuth bin center values
253
- - 'elevation_bin_center' : elevation bin center values
254
- Some data_vars may additionally be indexed by energy bin;
255
- however, only the spatial axes are used in this class.
295
+ - 'longitude' : (number of longitude/az bins in L1C)
296
+ - 'latitude' : (number of latitude/el bins in L1C)
256
297
  spice_reference_frame : geometry.SpiceFrame
257
298
  The reference Spice frame of the pointing set. Default is IMAP_DPS.
258
299
 
259
300
  Raises
260
301
  ------
261
302
  ValueError
262
- If the azimuth or elevation bin centers do not match the constructed grid.
263
- Or if the azimuth or elevation bin spacing is not uniform.
303
+ If the longitude/az or latitude/el bin centers don't match the constructed grid.
304
+ Or if the longitude or latitude bin spacing is not uniform.
264
305
  ValueError
265
306
  If multiple epochs are found in the dataset.
266
307
  """
@@ -284,15 +325,16 @@ class UltraPointingSet(PointingSet):
284
325
  if len(np.unique(self.epoch)) > 1:
285
326
  raise ValueError("Multiple epochs found in the dataset.")
286
327
 
287
- # The rest of the constructor handles the rectangular grid
288
- # aspects of the Ultra PSET.
289
- # NOTE: This may be changed to Healpix tessellation in the future
290
328
  self.tiling_type = SkyTilingType.RECTANGULAR
329
+ self.spatial_coords = (
330
+ CoordNames.AZIMUTH_L1C.value,
331
+ CoordNames.ELEVATION_L1C.value,
332
+ )
291
333
 
292
334
  # Ensure 1D axes grids are uniformly spaced,
293
335
  # then set spacing based on data's azimuth bin spacing.
294
- az_bin_delta = np.diff(self.data["azimuth_bin_center"])
295
- el_bin_delta = np.diff(self.data["elevation_bin_center"])
336
+ az_bin_delta = np.diff(self.data[CoordNames.AZIMUTH_L1C.value])
337
+ el_bin_delta = np.diff(self.data[CoordNames.ELEVATION_L1C.value])
296
338
  if not np.allclose(az_bin_delta, az_bin_delta[0], atol=1e-10, rtol=0):
297
339
  raise ValueError("Azimuth bin spacing is not uniform.")
298
340
  if not np.allclose(el_bin_delta, el_bin_delta[0], atol=1e-10, rtol=0):
@@ -304,26 +346,26 @@ class UltraPointingSet(PointingSet):
304
346
  )
305
347
  self.spacing_deg = az_bin_delta[0]
306
348
 
307
- # Build the azimuth and elevation grids with an AzElSkyGrid object
349
+ # Build the az/azimuth and el/elevation grids with an AzElSkyGrid object
308
350
  # and check that the 1D axes match the dataset's az and el.
309
351
  self.sky_grid = spatial_utils.AzElSkyGrid(
310
352
  spacing_deg=self.spacing_deg,
311
353
  )
312
354
 
313
355
  for dim, constructed_bins in zip(
314
- ["azimuth", "elevation"],
356
+ [CoordNames.AZIMUTH_L1C.value, CoordNames.ELEVATION_L1C.value],
315
357
  [self.sky_grid.az_bin_midpoints, self.sky_grid.el_bin_midpoints],
316
358
  ):
317
359
  if not np.allclose(
318
- sorted(np.rad2deg(constructed_bins)),
319
- self.data[f"{dim}_bin_center"],
360
+ sorted(constructed_bins),
361
+ self.data[dim],
320
362
  atol=1e-10,
321
363
  rtol=0,
322
364
  ):
323
365
  raise ValueError(
324
366
  f"{dim} bin centers do not match."
325
- f"Constructed: {np.rad2deg(constructed_bins)}"
326
- f"Dataset: {self.data[f'{dim}_bin_center']}"
367
+ f"Constructed: {constructed_bins}"
368
+ f"Dataset: {self.data[dim]}"
327
369
  )
328
370
 
329
371
  # Unwrap the az, el grids to series of points tiling the sky and combine them
@@ -344,6 +386,98 @@ class UltraPointingSet(PointingSet):
344
386
  self.az_bin_edges = self.sky_grid.az_bin_edges
345
387
  self.el_bin_edges = self.sky_grid.el_bin_edges
346
388
 
389
+
390
+ class UltraPointingSet(PointingSet):
391
+ """
392
+ Pointing set object specifically for Healpix-tiled ULTRA data, nominally at Level1C.
393
+
394
+ Parameters
395
+ ----------
396
+ l1c_dataset : xr.Dataset | pathlib.Path | str
397
+ L1c xarray dataset containing the pointing set data or the path to the dataset.
398
+ Currently, the dataset is expected to be tiled in a HEALPix tessellation,
399
+ with data_vars indexed along the coordinates:
400
+ - 'epoch' : time value (1 value per PSET, from the mean of the PSET)
401
+ - 'energy' : (number of energy bins in L1C)
402
+ - 'healpix_index' : HEALPix pixel index
403
+ Only the 'healpix_index' coordinate is used in this class for projection.
404
+ spice_reference_frame : geometry.SpiceFrame
405
+ The reference Spice frame of the pointing set. Default is IMAP_DPS.
406
+
407
+ Raises
408
+ ------
409
+ ValueError
410
+ If the longitude/az or latitude/el bin centers don't match the constructed grid.
411
+ Or if the longitude or latitude bin spacing is not uniform.
412
+ ValueError
413
+ If multiple epochs are found in the dataset.
414
+ """
415
+
416
+ def __init__(
417
+ self,
418
+ l1c_dataset: xr.Dataset | pathlib.Path | str,
419
+ spice_reference_frame: geometry.SpiceFrame = geometry.SpiceFrame.IMAP_DPS,
420
+ ):
421
+ # Store the reference frame of the pointing set
422
+ self.spice_reference_frame = spice_reference_frame
423
+
424
+ # Read in the data and store the xarray dataset as data attr
425
+ if isinstance(l1c_dataset, (str, pathlib.Path)):
426
+ self.data = load_cdf(pathlib.Path(l1c_dataset))
427
+ elif isinstance(l1c_dataset, xr.Dataset):
428
+ self.data = l1c_dataset
429
+
430
+ # A PSET must have a single epoch
431
+ self.epoch = self.data["epoch"].values
432
+ if len(np.unique(self.epoch)) > 1:
433
+ raise ValueError("Multiple epochs found in the dataset.")
434
+
435
+ # Set the tiling type and number of points
436
+ self.tiling_type = SkyTilingType.HEALPIX
437
+ self.spatial_coords = (CoordNames.HEALPIX_INDEX.value,)
438
+ self.num_points = self.data[CoordNames.HEALPIX_INDEX.value].size
439
+ self.nside = hp.npix_to_nside(self.num_points)
440
+
441
+ # Determine if the HEALPix tessellation is nested, default is False
442
+ self.nested = bool(
443
+ self.data[CoordNames.HEALPIX_INDEX.value].attrs.get("nested", False)
444
+ )
445
+
446
+ # Get the azimuth and elevation coordinates of the healpix pixel centers (deg)
447
+ self.azimuth_pixel_center, self.elevation_pixel_center = hp.pix2ang(
448
+ nside=self.nside,
449
+ ipix=np.arange(self.num_points),
450
+ nest=self.nested,
451
+ lonlat=True,
452
+ )
453
+
454
+ # Verify that the azimuth and elevation of the healpix pixel centers
455
+ # match the data's azimuth and elevation bin centers.
456
+ # NOTE: They can have different names in the L1C dataset
457
+ # (e.g. "longitude"/"latitude" vs "azimuth"/"elevation").
458
+ for dim, constructed_bins in zip(
459
+ [CoordNames.AZIMUTH_L1C.value, CoordNames.ELEVATION_L1C.value],
460
+ [self.azimuth_pixel_center, self.elevation_pixel_center],
461
+ ):
462
+ if not np.allclose(
463
+ self.data[dim],
464
+ constructed_bins,
465
+ atol=1e-10,
466
+ rtol=0,
467
+ ):
468
+ raise ValueError(
469
+ f"{dim} pixel centers do not match the data's {dim} bin centers."
470
+ f"Constructed: {constructed_bins}"
471
+ f"Dataset: {self.data[dim]}"
472
+ )
473
+
474
+ # The coordinates of the healpix pixel centers are stored as a 2D array
475
+ # of shape (num_points, 2) where column 0 is the lon/az
476
+ # and column 1 is the lat/el.
477
+ self.az_el_points = np.column_stack(
478
+ (self.azimuth_pixel_center, self.elevation_pixel_center)
479
+ )
480
+
347
481
  def __repr__(self) -> str:
348
482
  """
349
483
  Return a string representation of the UltraPointingSet.
@@ -362,65 +496,87 @@ class UltraPointingSet(PointingSet):
362
496
 
363
497
  # Define the Map classes
364
498
  class AbstractSkyMap(ABC):
365
- """Abstract base class to contain map data in the context of ENA sky maps."""
499
+ """
500
+ Abstract base class to contain map data in the context of ENA sky maps.
501
+
502
+ Data values are stored internally in an xarray Dataset, in the .data_1d attribute.
503
+ where the final (-1) axis is the only spatial dimension.
504
+ If the map is rectangular, this axis is the raveled 2D grid.
505
+ If the map is Healpix, this axis is the 1D array of Healpix pixel indices.
506
+
507
+ The data can be also accessed via the to_dataset method, which rewraps the data to
508
+ a 2D grid shape if the map is rectangular and formats the data as an xarray
509
+ Dataset with the correct dims and coords.
510
+ """
366
511
 
367
512
  @abstractmethod
368
513
  def __init__(self) -> None:
369
- pass
370
-
371
- def __repr__(self) -> str:
514
+ self.tiling_type: SkyTilingType
515
+ self.sky_grid: spatial_utils.AzElSkyGrid
516
+ self.num_points: int
517
+ self.non_spatial_coords: dict[str, xr.DataArray | NDArray]
518
+ self.spatial_coords: dict[str, xr.DataArray | NDArray]
519
+ self.binning_grid_shape: tuple[int, ...]
520
+ self.data_1d: xr.Dataset
521
+
522
+ def to_dataset(self) -> xr.Dataset:
372
523
  """
373
- Return a string representation of the map.
524
+ Get the SkyMap data as a formatted xarray Dataset.
374
525
 
375
526
  Returns
376
527
  -------
377
- str
378
- String representation of the map.
528
+ xr.Dataset
529
+ The SkyMap data as a formatted xarray Dataset with dims and coords.
530
+ If the SkyMap is empty, an empty xarray Dataset is returned.
531
+ If the SkyMap is Rectangular, the data is rewrapped to a 2D grid of
532
+ lon/lat (AKA az/el) coordinates.
533
+ If the SkyMap is Healpix, the data is unchanged from the data_1d, but
534
+ the pixel coordinate is renamed to CoordNames.HEALPIX_INDEX.value.
379
535
  """
380
- return f"{self.__class__} Map)"
381
-
382
-
383
- class RectangularSkyMap(AbstractSkyMap):
384
- """
385
- Map which tiles the sky with a 2D rectangular grid of azimuth/elevation pixels.
386
-
387
- NOTE: Internally, the map is stored as a 1D array of pixels.
388
-
389
- Parameters
390
- ----------
391
- spacing_deg : float
392
- The spacing of the rectangular grid in degrees.
393
- spice_frame : geometry.SpiceFrame
394
- The reference Spice frame of the map.
395
- """
396
-
397
- def __init__(
398
- self,
399
- spacing_deg: float,
400
- spice_frame: geometry.SpiceFrame,
401
- ):
402
- # Define the core properties of the map:
403
- self.tiling_type = SkyTilingType.RECTANGULAR # Type of tiling of the sky
404
- self.spacing_deg = spacing_deg
405
- self.spice_reference_frame = spice_frame
406
- self.sky_grid = spatial_utils.AzElSkyGrid(
407
- spacing_deg=self.spacing_deg,
408
- )
409
-
410
- # Solid angles of each pixel in the map grid in units of steradians
411
- self.solid_angle_grid = spatial_utils.build_solid_angle_map(
412
- spacing_deg=self.spacing_deg,
413
- )
414
-
415
- # Unwrap the az, el, solid angle grids to series of points tiling the sky
416
- az_points = self.sky_grid.az_grid.ravel()
417
- el_points = self.sky_grid.el_grid.ravel()
418
- self.az_el_points = np.column_stack((az_points, el_points))
419
- self.solid_angle_points = self.solid_angle_grid.ravel()
420
- self.num_points = self.az_el_points.shape[0]
536
+ if len(self.data_1d.data_vars) == 0:
537
+ # If the map is empty, return an empty xarray Dataset,
538
+ # with the unaltered spatial coords of the map
539
+ return xr.Dataset(
540
+ {},
541
+ coords={**self.spatial_coords},
542
+ )
421
543
 
422
- # Initialize empty data dictionary to store map data
423
- self.data_dict: dict[str, NDArray] = {}
544
+ if self.tiling_type is SkyTilingType.HEALPIX:
545
+ # return the data_1d as is, but with the pixel coordinate
546
+ # renamed to CoordNames.HEALPIX_INDEX.value
547
+ return self.data_1d.rename(
548
+ {CoordNames.GENERIC_PIXEL.value: CoordNames.HEALPIX_INDEX.value}
549
+ )
550
+ elif self.tiling_type is SkyTilingType.RECTANGULAR:
551
+ # Rewrap each data array in the data_1d to the original 2D grid shape
552
+ rewrapped_data = {}
553
+ for key in self.data_1d.data_vars:
554
+ # drop pixel dim from the end, and add the spatial coords as dims
555
+ rewrapped_dims = [
556
+ dim
557
+ for dim in self.data_1d[key].dims
558
+ if dim != CoordNames.GENERIC_PIXEL.value
559
+ ]
560
+ rewrapped_dims.extend(self.spatial_coords.keys())
561
+ rewrapped_data[key] = xr.DataArray(
562
+ spatial_utils.rewrap_even_spaced_az_el_grid(
563
+ self.data_1d[key].values,
564
+ self.binning_grid_shape,
565
+ ),
566
+ dims=rewrapped_dims,
567
+ )
568
+ # Add the output coordinates to the rewrapped data, excluding the pixel
569
+ self.non_spatial_coords.update(
570
+ {
571
+ key: self.data_1d[key].coords[key]
572
+ for key in self.data_1d[key].coords
573
+ if key != CoordNames.GENERIC_PIXEL.value
574
+ }
575
+ )
576
+ return xr.Dataset(
577
+ rewrapped_data,
578
+ coords={**self.non_spatial_coords, **self.spatial_coords},
579
+ )
424
580
 
425
581
  def project_pset_values_to_map(
426
582
  self,
@@ -440,7 +596,7 @@ class RectangularSkyMap(AbstractSkyMap):
440
596
  pointing_set : PointingSet
441
597
  The pointing set containing the values to project to the map.
442
598
  value_keys : list[tuple[str, IndexMatchMethod]] | None
443
- The keys of the values to project to the map.
599
+ The keys of the values in the PointingSet to project to the map.
444
600
  Ex.: ["counts", "flux"]
445
601
  data_vars named each key must be present, and of the same dimensionality in
446
602
  each pointing set which is to be projected to the map.
@@ -456,45 +612,185 @@ class RectangularSkyMap(AbstractSkyMap):
456
612
  """
457
613
  if value_keys is None:
458
614
  value_keys = list(pointing_set.data.data_vars.keys())
459
-
460
615
  for value_key in value_keys:
461
616
  if value_key not in pointing_set.data.data_vars:
462
617
  raise ValueError(f"Value key {value_key} not found in pointing set.")
463
618
 
464
- # Determine the indices of the sky map grid that correspond to
465
- # each pixel in the pointing set.
466
619
  if index_match_method is IndexMatchMethod.PUSH:
620
+ # Determine the indices of the sky map grid that correspond to
621
+ # each pixel in the pointing set.
467
622
  matched_indices_push = match_coords_to_indices(
468
623
  input_object=pointing_set,
469
624
  output_object=self,
470
625
  )
626
+ elif index_match_method is IndexMatchMethod.PULL:
627
+ # Determine the indices of the pointing set grid that correspond to
628
+ # each pixel in the sky map.
629
+ matched_indices_pull = match_coords_to_indices(
630
+ input_object=self,
631
+ output_object=pointing_set,
632
+ )
633
+ else:
634
+ raise NotImplementedError(
635
+ "Only PUSH and PULL index matching methods are supported."
636
+ )
471
637
 
472
638
  for value_key in value_keys:
639
+ pset_values = pointing_set.data[value_key]
640
+
473
641
  # If multiple spatial axes present
474
642
  # (i.e (az, el) for rectangular coordinate PSET),
475
643
  # flatten them in the values array to match the raveled indices
476
- raveled_pset_data = pointing_set.data[value_key].data.reshape(
477
- pointing_set.num_points, -1
644
+ non_spatial_axes_shape = tuple(
645
+ size
646
+ for key, size in pset_values.sizes.items()
647
+ if key not in pointing_set.spatial_coords
648
+ )
649
+ raveled_pset_data = pset_values.data.reshape(
650
+ *non_spatial_axes_shape,
651
+ pointing_set.num_points,
478
652
  )
479
- if value_key not in self.data_dict:
653
+
654
+ if value_key not in self.data_1d.data_vars:
480
655
  # Initialize the map data array if it doesn't exist (values start at 0)
481
- output_shape = (self.num_points, *raveled_pset_data.shape[1:])
482
- self.data_dict[value_key] = np.zeros(output_shape)
656
+ output_shape = (*raveled_pset_data.shape[:-1], self.num_points)
657
+ self.data_1d[value_key] = xr.DataArray(
658
+ np.zeros(output_shape),
659
+ dims=pointing_set.unwrapped_dims_dict[value_key],
660
+ )
661
+
662
+ # Make coordinates for the map data array if they don't exist
663
+ self.data_1d.coords.update(
664
+ {
665
+ dim: pointing_set.data[dim]
666
+ for dim in self.data_1d[value_key].dims
667
+ if dim not in self.data_1d.coords
668
+ }
669
+ )
483
670
 
484
671
  if index_match_method is IndexMatchMethod.PUSH:
672
+ # Bin the values at the matched indices. There may be multiple
673
+ # pointing set pixels that correspond to the same sky map pixel.
485
674
  pointing_projected_values = map_utils.bin_single_array_at_indices(
486
675
  value_array=raveled_pset_data,
487
- projection_grid_shape=(
488
- len(self.sky_grid.az_bin_midpoints),
489
- len(self.sky_grid.el_bin_midpoints),
490
- ),
676
+ projection_grid_shape=self.binning_grid_shape,
491
677
  projection_indices=matched_indices_push,
492
678
  )
679
+ elif index_match_method is IndexMatchMethod.PULL:
680
+ # We know that there will only be one value per sky map pixel,
681
+ # so we can use the matched indices directly
682
+ pointing_projected_values = raveled_pset_data[..., matched_indices_pull]
493
683
  else:
494
684
  raise NotImplementedError(
495
- "The 'pull' method of index matching is not yet implemented."
685
+ "Only PUSH and PULL index matching methods are supported."
496
686
  )
497
- self.data_dict[value_key] += pointing_projected_values
687
+
688
+ self.data_1d[value_key] += pointing_projected_values
689
+
690
+
691
+ class RectangularSkyMap(AbstractSkyMap):
692
+ """
693
+ Map which tiles the sky with a 2D rectangular grid of azimuth/elevation pixels.
694
+
695
+ Parameters
696
+ ----------
697
+ spacing_deg : float
698
+ The spacing of the rectangular grid in degrees.
699
+ spice_frame : geometry.SpiceFrame
700
+ The reference Spice frame of the map.
701
+
702
+ Notes
703
+ -----
704
+ Internally, the map is stored as a 1D array of pixels, and all data arrays
705
+ are stored with the final (-1) axis as the only spatial axis, representing the
706
+ pixel index in the 1D array (See Figs 1-2, which demonstrate the 1D pixel index
707
+ corresponding to the 2D grid of coordinates).
708
+
709
+ ^ |15, 75|45, 75|75, 75|105, 75|...|255, 75|285, 75|315, 75|345, 75|
710
+ | |15, 45|45, 45|75, 45|105, 45|...|255, 45|285, 45|315, 45|345, 45|
711
+ | |15, 15|45, 15|75, 15|105, 15|...|255, 15|285, 15|315, 15|345, 15|
712
+ | |15, -15|45, -15|75, -15|105, -15|...|255, -15|285, -15|315, -15|345, -15|
713
+ | |15, -45|45, -45|75, -45|105, -45|...|255, -45|285, -45|315, -45|345, -45|
714
+ | |15, -75|45, -75|75, -75|105, -75|...|255, -75|285, -75|315, -75|345, -75|
715
+ |
716
+ ---------------------------------------------------------------> Azimuth (degrees)
717
+ Elevation (degrees)
718
+
719
+ Fig. 1: Example of a rectangular grid of pixels in azimuth and elevation coordinates
720
+ in degrees, with a spacing of 30 degrees. There will be 12 azimuth bins and 6
721
+ elevation bins in this example, resulting in 72 pixels in the map.
722
+
723
+ A multidimentional value (e.g. counts, with energy levels at each pixel)
724
+ will be stored as a 2D array with the first axis as the energy dimension and the
725
+ second axis as the pixel index.
726
+
727
+ ^ |5|11|17|23|29|35|41|47|53|59|65|71|
728
+ | |4|10|16|22|28|34|40|46|52|58|64|70|
729
+ | |3|9 |15|21|27|33|39|45|51|57|63|69|
730
+ | |2|8 |14|20|26|32|38|44|50|56|62|68|
731
+ | |1|7 |13|19|25|31|37|43|49|55|61|67|
732
+ | |0|6 |12|18|24|30|36|42|48|54|60|66|
733
+ ---------------------------------------> Azimuth
734
+ Elevation
735
+
736
+ Fig. 2: The 1D indices of the pixels in Fig. 1.
737
+ Note that the indices are raveled from the 2D grid of (az, el) such that as one
738
+ increases in pixel index, elevation increments first, then azimuth.
739
+ """
740
+
741
+ def __init__(
742
+ self,
743
+ spacing_deg: float,
744
+ spice_frame: geometry.SpiceFrame,
745
+ ):
746
+ # Define the core properties of the map:
747
+ self.tiling_type = SkyTilingType.RECTANGULAR # Type of tiling of the sky
748
+
749
+ # The reference Spice frame of the map, in which angles are defined
750
+ self.spice_reference_frame = spice_frame
751
+
752
+ # Angular spacing of the map grid (degrees) defines the number, size of pixels.
753
+ self.spacing_deg = spacing_deg
754
+ self.sky_grid = spatial_utils.AzElSkyGrid(
755
+ spacing_deg=self.spacing_deg,
756
+ )
757
+ # The shape of the map (num_az_bins, num_el_bins) is used to bin the data
758
+ self.binning_grid_shape = self.sky_grid.grid_shape
759
+
760
+ self.non_spatial_coords = {}
761
+ self.spatial_coords = {
762
+ CoordNames.AZIMUTH_L1C.value: xr.DataArray(
763
+ self.sky_grid.az_bin_midpoints,
764
+ dims=[CoordNames.AZIMUTH_L1C.value],
765
+ attrs={"units": "degrees"},
766
+ ),
767
+ CoordNames.ELEVATION_L1C.value: xr.DataArray(
768
+ self.sky_grid.el_bin_midpoints,
769
+ dims=[CoordNames.ELEVATION_L1C.value],
770
+ attrs={"units": "degrees"},
771
+ ),
772
+ }
773
+
774
+ # Unwrap the az, el grids to 1D array of points tiling the sky
775
+ az_points = self.sky_grid.az_grid.ravel()
776
+ el_points = self.sky_grid.el_grid.ravel()
777
+
778
+ # Stack so axis 0 is different pixels, and axis 1 is (az, el) of the pixel
779
+ self.az_el_points = np.column_stack((az_points, el_points))
780
+ self.num_points = self.az_el_points.shape[0]
781
+
782
+ # Calculate solid angles of each pixel in the map grid in units of steradians
783
+ self.solid_angle_grid = spatial_utils.build_solid_angle_map(
784
+ spacing_deg=self.spacing_deg,
785
+ )
786
+ self.solid_angle_points = self.solid_angle_grid.ravel()
787
+
788
+ # Initialize xarray Dataset to store map data projected from pointing sets
789
+ self.data_1d: xr.Dataset = xr.Dataset(
790
+ coords={
791
+ CoordNames.GENERIC_PIXEL.value: np.arange(self.num_points),
792
+ }
793
+ )
498
794
 
499
795
  def __repr__(self) -> str:
500
796
  """
@@ -506,14 +802,82 @@ class RectangularSkyMap(AbstractSkyMap):
506
802
  String representation of the RectangularSkyMap.
507
803
  """
508
804
  return (
509
- "RectangularSkyMap\n\t(reference_frame="
805
+ f"{self.__class__.__name__}\n\t(reference_frame="
510
806
  f"{self.spice_reference_frame.name} ({self.spice_reference_frame.value}), "
511
807
  f"spacing_deg={self.spacing_deg}, num_points={self.num_points})"
512
808
  )
513
809
 
514
810
 
515
- # TODO:
516
- # Add pulling index matching in match_pset_coords_to_indices
811
+ class HealpixSkyMap(AbstractSkyMap):
812
+ """
813
+ Map which tiles the sky with a Healpix tessellation of equal-area pixels.
814
+
815
+ Parameters
816
+ ----------
817
+ nside : int
818
+ The nside parameter of the Healpix tessellation.
819
+ spice_frame : geometry.SpiceFrame
820
+ The reference Spice frame of the map.
821
+ nested : bool, optional
822
+ Whether the Healpix tessellation is nested. Default is False.
823
+ """
824
+
825
+ def __init__(
826
+ self, nside: int, spice_frame: geometry.SpiceFrame, nested: bool = False
827
+ ):
828
+ # Define the core properties of the map:
829
+ self.tiling_type = SkyTilingType.HEALPIX
830
+ self.spice_reference_frame = spice_frame
831
+
832
+ # Tile the sky with a Healpix tessellation. Defined by nside, nested parameters.
833
+ self.nside = nside
834
+ self.nested = nested
835
+
836
+ # Calculate how many pixels cover the sky and the approximate resolution (deg)
837
+ self.num_points = hp.nside2npix(nside)
838
+ self.approx_resolution = np.rad2deg(hp.nside2resol(nside, arcmin=False))
839
+ # Define binning_grid_shape for consistency with RectangularSkyMap
840
+ self.binning_grid_shape = (self.num_points,)
841
+ self.spatial_coords = {
842
+ CoordNames.HEALPIX_INDEX.value: xr.DataArray(
843
+ np.arange(self.num_points),
844
+ dims=[CoordNames.HEALPIX_INDEX.value],
845
+ )
846
+ }
847
+
848
+ # The centers of each pixel in the Healpix tessellation in azimuth (az) and
849
+ # elevation (el) coordinates (degrees) within the map's Spice frame.
850
+ pixel_az, pixel_el = hp.pix2ang(
851
+ nside=nside, ipix=np.arange(self.num_points), nest=nested, lonlat=True
852
+ )
853
+ # Stack so axis 0 is different pixels, and axis 1 is (az, el) of the pixel
854
+ self.az_el_points = np.column_stack((pixel_az, pixel_el))
855
+
856
+ # Tracks Per-Pixel Solid Angle in steradians.
857
+ self.solid_angle = hp.nside2pixarea(nside, degrees=False)
858
+
859
+ # Solid angle is equal at all pixels, but define
860
+ # solid_angle_points to be consistent with RectangularSkyMap
861
+ self.solid_angle_points = np.full(self.num_points, self.solid_angle)
862
+
863
+ # Initialize xarray Dataset to store map data projected from pointing sets
864
+ self.data_1d: xr.Dataset = xr.Dataset(
865
+ coords={
866
+ CoordNames.GENERIC_PIXEL.value: np.arange(self.num_points),
867
+ }
868
+ )
517
869
 
518
- # TODO:
519
- # Check units of time which will be read in. Do we need to add j2000ns_to_j2000s?
870
+ def __repr__(self) -> str:
871
+ """
872
+ Return a string representation of the HealpixSkyMap.
873
+
874
+ Returns
875
+ -------
876
+ str
877
+ String representation of the HealpixSkyMap.
878
+ """
879
+ return (
880
+ f"{self.__class__.__name__}\n\t(reference_frame="
881
+ f"{self.spice_reference_frame.name} ({self.spice_reference_frame.value}), "
882
+ f"nside={self.nside}, num_points={self.num_points})"
883
+ )