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,519 @@
1
+ """Define classes for handling pointing sets and maps for ENA data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import pathlib
7
+ from abc import ABC, abstractmethod
8
+ from enum import Enum
9
+
10
+ import numpy as np
11
+ import xarray as xr
12
+ from numpy.typing import NDArray
13
+
14
+ from imap_processing.cdf.utils import load_cdf
15
+ from imap_processing.ena_maps.utils import map_utils, spatial_utils
16
+ from imap_processing.spice import geometry
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SkyTilingType(Enum):
22
+ """Enumeration of the types of tiling used in the ENA maps."""
23
+
24
+ RECTANGULAR = "Rectangular"
25
+ HEALPIX = "Healpix"
26
+
27
+
28
+ class IndexMatchMethod(Enum):
29
+ """
30
+ Enumeration of the types of index matching methods used in the ENA sky maps.
31
+
32
+ Notes
33
+ -----
34
+ Index matching is the process of determining which pixels in a map grid correspond
35
+ to which pixels in a pointing set grid. The Ultra instrument team has determined
36
+ that they must support two methods of index matching for rectangular grid maps:
37
+
38
+ **Push Method**
39
+
40
+ The "push" method takes each pixel in a pointing set and transforms its coordinates
41
+ to the frame of the map, then determines into which pixel in the map grid the
42
+ transformed pointing set pixel falls.
43
+ This method ensures that all pointing set pixels (and thus all counts) are
44
+ captured in the map, but does not ensure that all pixels in the map receive data.
45
+
46
+ **Pull Method**
47
+
48
+ The "pull" method takes each pixel in the map grid and transforms its coordinates
49
+ to the frame of the pointing set, then determines into which pixel in the
50
+ pointing set grid the transformed map pixel falls.
51
+ This method ensures that all pixels in the map receive data, but can result in
52
+ some pointing set pixels not being captured in the map, and others being captured
53
+ multiple times.
54
+ """
55
+
56
+ PUSH = "Push"
57
+ PULL = "Pull"
58
+
59
+
60
+ def match_coords_to_indices(
61
+ input_object: PointingSet | AbstractSkyMap,
62
+ output_object: PointingSet | AbstractSkyMap,
63
+ event_time: float | None = None,
64
+ ) -> NDArray:
65
+ """
66
+ Find the output indices corresponding to each input coord between 2 spatial objects.
67
+
68
+ First, the pixel center coordinates of the input spatial object are
69
+ transformed from the Spice coordinate frame of the input object to their
70
+ corresponding coordinates in the Spice frame of the output object.
71
+ Then, the transformed pixel centers are matched to the 1D indices of the spatial
72
+ pixels in the output frame, either in an unwrapped rectangular grid or a Healpix
73
+ tessellation of the sky.
74
+
75
+ This function always "pushes" the pixels of the input object to corresponding pixels
76
+ in the output object's unwrapped rectangular grid or healpix tessellation;
77
+ however, by swapping the input and output objects, one can apply the "pull" method
78
+ of index matching.
79
+
80
+ At present, the allowable inputs are either:
81
+ - A PointingSet object and a SkyMap object, in either order of input/output.
82
+ The event time will be taken from the PointingSet object.
83
+ - Two SkyMap objects, in which case the event time must be specified.
84
+
85
+ Parameters
86
+ ----------
87
+ input_object : PointingSet | AbstractSkyMap
88
+ An object containing 1D spatial pixel centers in azimuth and elevation,
89
+ which will be matched to 1D indices of spatial pixels in the output frame.
90
+ Must contain the Spice frame in which the pixel centers are defined.
91
+ output_object : PointingSet | AbstractSkyMap
92
+ The object containing a grid or tessellation of spatial pixels
93
+ into which the input spatial pixel centers will 'land', and be matched to
94
+ corresponding pixel 1D indices in the output frame.
95
+ event_time : float, optional
96
+ Event time at which to transform the input spatial object to the output frame.
97
+ This can be manually specified, e.g., for converting between Maps which do not
98
+ contain an epoch value.
99
+ The default value is None, in which case the event time of the PointingSet
100
+ object is used.
101
+
102
+ Returns
103
+ -------
104
+ flat_indices_input_grid_output_frame : NDArray
105
+ 1D array of pixel indices of the output object corresponding to each pixel in
106
+ the input object. The length of the array is equal to the number of pixels in
107
+ the input object, and may contain 0, 1, or multiple occurrences of the same
108
+ output index.
109
+
110
+ Raises
111
+ ------
112
+ ValueError
113
+ If both input and output objects are PointingSet objects.
114
+ ValueError
115
+ If the event time is not specified and both objects are SkyMaps.
116
+ NotImplementedError
117
+ If the output tiling type is HEALPIX. Will be implemented in the future.
118
+ ValueError
119
+ If the tiling type of the output frame is not RECTANGULAR or HEALPIX.
120
+ """
121
+ if isinstance(input_object, PointingSet) and isinstance(output_object, PointingSet):
122
+ raise ValueError("Cannot match indices between two PointingSet objects.")
123
+
124
+ # If event_time is not specified, use event_time of the PointingSet, if present.
125
+ if event_time is None:
126
+ if isinstance(input_object, PointingSet):
127
+ event_time = input_object.data["epoch"].values
128
+ elif isinstance(output_object, PointingSet):
129
+ event_time = output_object.data["epoch"].values
130
+ else:
131
+ raise ValueError(
132
+ "Event time must be specified if both objects are SkyMaps."
133
+ )
134
+
135
+ # Az/El pixel center coords of the input object in its own frame
136
+ input_obj_az_el_input_frame = input_object.az_el_points
137
+
138
+ # Transform the input pixel centers to the output frame
139
+ input_obj_az_el_output_frame = geometry.frame_transform_az_el(
140
+ et=event_time,
141
+ az_el=input_obj_az_el_input_frame,
142
+ from_frame=input_object.spice_reference_frame,
143
+ to_frame=output_object.spice_reference_frame,
144
+ degrees=False,
145
+ )
146
+
147
+ # The way indices are matched depends on the tiling type of the 2nd object
148
+ if output_object.tiling_type is SkyTilingType.RECTANGULAR:
149
+ # To match to a rectangular grid, we need to digitize the transformed az, el
150
+ # pixel centers onto the bin edges of the output frame's grid, then
151
+ # use ravel_multi_index to get the 1D indices of the pixels in the output frame.
152
+ az_indices = (
153
+ np.digitize(
154
+ input_obj_az_el_output_frame[:, 0],
155
+ output_object.sky_grid.az_bin_edges,
156
+ )
157
+ - 1
158
+ )
159
+ el_indices = (
160
+ np.digitize(
161
+ input_obj_az_el_output_frame[:, 1],
162
+ output_object.sky_grid.el_bin_edges,
163
+ )
164
+ - 1
165
+ )
166
+ flat_indices_input_grid_output_frame = np.ravel_multi_index(
167
+ multi_index=(az_indices, el_indices),
168
+ dims=(
169
+ len(output_object.sky_grid.az_bin_midpoints),
170
+ len(output_object.sky_grid.el_bin_midpoints),
171
+ ),
172
+ )
173
+
174
+ elif output_object.tiling_type is SkyTilingType.HEALPIX:
175
+ # To match to a Healpix tessellation, we need to use the healpy function ang2pix
176
+ # 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
+ 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,
189
+ lonlat=True,
190
+ )
191
+ ```
192
+ """
193
+ raise NotImplementedError(
194
+ "Index matching for output tiling type Healpix is not yet implemented."
195
+ )
196
+
197
+ else:
198
+ raise ValueError(
199
+ "Tiling type of the output frame must be either RECTANGULAR or HEALPIX."
200
+ )
201
+
202
+ return flat_indices_input_grid_output_frame
203
+
204
+
205
+ # Define the pointing set classes
206
+ class PointingSet(ABC):
207
+ """
208
+ Abstract class to contain pointing set (PSET) data in the context of ENA sky maps.
209
+
210
+ Parameters
211
+ ----------
212
+ dataset : xr.Dataset
213
+ Dataset containing the pointing set data.
214
+ spice_reference_frame : geometry.SpiceFrame
215
+ The reference Spice frame of the pointing set.
216
+ """
217
+
218
+ @abstractmethod
219
+ def __init__(self, dataset: xr.Dataset, spice_reference_frame: geometry.SpiceFrame):
220
+ """Abstract method to initialize the pointing set object."""
221
+ self.spice_reference_frame = spice_reference_frame
222
+ self.num_points = 0
223
+ self.az_el_points = np.zeros((self.num_points, 2))
224
+ self.data = xr.Dataset()
225
+
226
+ def __repr__(self) -> str:
227
+ """
228
+ Return a string representation of the pointing set.
229
+
230
+ Returns
231
+ -------
232
+ str
233
+ String representation of the pointing set.
234
+ """
235
+ return (
236
+ f"{self.__class__} PointingSet"
237
+ f"(spice_reference_frame={self.spice_reference_frame})"
238
+ )
239
+
240
+
241
+ class UltraPointingSet(PointingSet):
242
+ """
243
+ PSET object specifically for ULTRA data, nominally at Level 1C.
244
+
245
+ Parameters
246
+ ----------
247
+ l1c_dataset : xr.Dataset | pathlib.Path | str
248
+ 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,
250
+ with data_vars indexed along the coordinates:
251
+ - '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.
256
+ spice_reference_frame : geometry.SpiceFrame
257
+ The reference Spice frame of the pointing set. Default is IMAP_DPS.
258
+
259
+ Raises
260
+ ------
261
+ 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.
264
+ ValueError
265
+ If multiple epochs are found in the dataset.
266
+ """
267
+
268
+ def __init__(
269
+ self,
270
+ l1c_dataset: xr.Dataset | pathlib.Path | str,
271
+ spice_reference_frame: geometry.SpiceFrame = geometry.SpiceFrame.IMAP_DPS,
272
+ ):
273
+ # Store the reference frame of the pointing set
274
+ self.spice_reference_frame = spice_reference_frame
275
+
276
+ # Read in the data and store the xarray dataset as data attr
277
+ if isinstance(l1c_dataset, (str, pathlib.Path)):
278
+ self.data = load_cdf(pathlib.Path(l1c_dataset))
279
+ elif isinstance(l1c_dataset, xr.Dataset):
280
+ self.data = l1c_dataset
281
+
282
+ # A PSET must have a single epoch
283
+ self.epoch = self.data["epoch"].values
284
+ if len(np.unique(self.epoch)) > 1:
285
+ raise ValueError("Multiple epochs found in the dataset.")
286
+
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
+ self.tiling_type = SkyTilingType.RECTANGULAR
291
+
292
+ # Ensure 1D axes grids are uniformly spaced,
293
+ # 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"])
296
+ if not np.allclose(az_bin_delta, az_bin_delta[0], atol=1e-10, rtol=0):
297
+ raise ValueError("Azimuth bin spacing is not uniform.")
298
+ if not np.allclose(el_bin_delta, el_bin_delta[0], atol=1e-10, rtol=0):
299
+ raise ValueError("Elevation bin spacing is not uniform.")
300
+ if not np.isclose(az_bin_delta[0], el_bin_delta[0], atol=1e-10, rtol=0):
301
+ raise ValueError(
302
+ "Azimuth and elevation bin spacing do not match: "
303
+ f"az {az_bin_delta[0]} != el {el_bin_delta[0]}."
304
+ )
305
+ self.spacing_deg = az_bin_delta[0]
306
+
307
+ # Build the azimuth and elevation grids with an AzElSkyGrid object
308
+ # and check that the 1D axes match the dataset's az and el.
309
+ self.sky_grid = spatial_utils.AzElSkyGrid(
310
+ spacing_deg=self.spacing_deg,
311
+ )
312
+
313
+ for dim, constructed_bins in zip(
314
+ ["azimuth", "elevation"],
315
+ [self.sky_grid.az_bin_midpoints, self.sky_grid.el_bin_midpoints],
316
+ ):
317
+ if not np.allclose(
318
+ sorted(np.rad2deg(constructed_bins)),
319
+ self.data[f"{dim}_bin_center"],
320
+ atol=1e-10,
321
+ rtol=0,
322
+ ):
323
+ raise ValueError(
324
+ f"{dim} bin centers do not match."
325
+ f"Constructed: {np.rad2deg(constructed_bins)}"
326
+ f"Dataset: {self.data[f'{dim}_bin_center']}"
327
+ )
328
+
329
+ # Unwrap the az, el grids to series of points tiling the sky and combine them
330
+ # into shape (number of points in tiling of the sky, 2) where
331
+ # column 0 (az_el_points[:, 0]) is the azimuth of that point and
332
+ # column 1 (az_el_points[:, 1]) is the elevation of that point.
333
+ self.az_el_points = np.column_stack(
334
+ (
335
+ self.sky_grid.az_grid.ravel(),
336
+ self.sky_grid.el_grid.ravel(),
337
+ )
338
+ )
339
+ self.num_points = self.az_el_points.shape[0]
340
+
341
+ # Also store the bin edges for the pointing set to allow for "pull" method
342
+ # of index matching (not yet implemented).
343
+ # These are 1D arrays of different lengths and cannot be stacked.
344
+ self.az_bin_edges = self.sky_grid.az_bin_edges
345
+ self.el_bin_edges = self.sky_grid.el_bin_edges
346
+
347
+ def __repr__(self) -> str:
348
+ """
349
+ Return a string representation of the UltraPointingSet.
350
+
351
+ Returns
352
+ -------
353
+ str
354
+ String representation of the UltraPointingSet.
355
+ """
356
+ return (
357
+ f"UltraPointingSet\n\t(spice_reference_frame="
358
+ f"{self.spice_reference_frame}, epoch={self.epoch}, "
359
+ f"num_points={self.num_points})"
360
+ )
361
+
362
+
363
+ # Define the Map classes
364
+ class AbstractSkyMap(ABC):
365
+ """Abstract base class to contain map data in the context of ENA sky maps."""
366
+
367
+ @abstractmethod
368
+ def __init__(self) -> None:
369
+ pass
370
+
371
+ def __repr__(self) -> str:
372
+ """
373
+ Return a string representation of the map.
374
+
375
+ Returns
376
+ -------
377
+ str
378
+ String representation of the map.
379
+ """
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]
421
+
422
+ # Initialize empty data dictionary to store map data
423
+ self.data_dict: dict[str, NDArray] = {}
424
+
425
+ def project_pset_values_to_map(
426
+ self,
427
+ pointing_set: PointingSet,
428
+ value_keys: list[str] | None = None,
429
+ index_match_method: IndexMatchMethod = IndexMatchMethod.PUSH,
430
+ ) -> None:
431
+ """
432
+ Project a pointing set's values to the map grid.
433
+
434
+ Here, the term "project" refers to the process of determining which pixels in
435
+ the map grid correspond to which pixels in the pointing set grid, and then
436
+ binning the values at those indices from the pointing set to the map.
437
+
438
+ Parameters
439
+ ----------
440
+ pointing_set : PointingSet
441
+ The pointing set containing the values to project to the map.
442
+ value_keys : list[tuple[str, IndexMatchMethod]] | None
443
+ The keys of the values to project to the map.
444
+ Ex.: ["counts", "flux"]
445
+ data_vars named each key must be present, and of the same dimensionality in
446
+ each pointing set which is to be projected to the map.
447
+ Default is None, in which case all data_vars in the pointing set are used.
448
+ index_match_method : IndexMatchMethod, optional
449
+ The method of index matching to use for all values.
450
+ Default is IndexMatchMethod.PUSH.
451
+
452
+ Raises
453
+ ------
454
+ ValueError
455
+ If a value key is not found in the pointing set.
456
+ """
457
+ if value_keys is None:
458
+ value_keys = list(pointing_set.data.data_vars.keys())
459
+
460
+ for value_key in value_keys:
461
+ if value_key not in pointing_set.data.data_vars:
462
+ raise ValueError(f"Value key {value_key} not found in pointing set.")
463
+
464
+ # Determine the indices of the sky map grid that correspond to
465
+ # each pixel in the pointing set.
466
+ if index_match_method is IndexMatchMethod.PUSH:
467
+ matched_indices_push = match_coords_to_indices(
468
+ input_object=pointing_set,
469
+ output_object=self,
470
+ )
471
+
472
+ for value_key in value_keys:
473
+ # If multiple spatial axes present
474
+ # (i.e (az, el) for rectangular coordinate PSET),
475
+ # 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
478
+ )
479
+ if value_key not in self.data_dict:
480
+ # 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)
483
+
484
+ if index_match_method is IndexMatchMethod.PUSH:
485
+ pointing_projected_values = map_utils.bin_single_array_at_indices(
486
+ 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
+ ),
491
+ projection_indices=matched_indices_push,
492
+ )
493
+ else:
494
+ raise NotImplementedError(
495
+ "The 'pull' method of index matching is not yet implemented."
496
+ )
497
+ self.data_dict[value_key] += pointing_projected_values
498
+
499
+ def __repr__(self) -> str:
500
+ """
501
+ Return a string representation of the RectangularSkyMap.
502
+
503
+ Returns
504
+ -------
505
+ str
506
+ String representation of the RectangularSkyMap.
507
+ """
508
+ return (
509
+ "RectangularSkyMap\n\t(reference_frame="
510
+ f"{self.spice_reference_frame.name} ({self.spice_reference_frame.value}), "
511
+ f"spacing_deg={self.spacing_deg}, num_points={self.num_points})"
512
+ )
513
+
514
+
515
+ # TODO:
516
+ # Add pulling index matching in match_pset_coords_to_indices
517
+
518
+ # TODO:
519
+ # Check units of time which will be read in. Do we need to add j2000ns_to_j2000s?
@@ -0,0 +1,145 @@
1
+ """Utilities for generating ENA maps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def bin_single_array_at_indices(
14
+ value_array: NDArray,
15
+ projection_grid_shape: tuple[int, int],
16
+ projection_indices: NDArray,
17
+ input_indices: NDArray | None = None,
18
+ ) -> NDArray:
19
+ """
20
+ Bin an array of values at the given indices.
21
+
22
+ Parameters
23
+ ----------
24
+ value_array : NDArray
25
+ Array of values to bin. The 0th axis must be the one and only spatial axis.
26
+ If other axes are present, they will be binned independently
27
+ along the 0th (spatial) axis.
28
+ projection_grid_shape : tuple[int]
29
+ The shape of the grid onto which values are projected
30
+ (rows, columns) if the grid is rectangular,
31
+ or just (number of bins,) if the grid is 1D.
32
+ projection_indices : NDArray
33
+ Ordered indices for projection grid, corresponding to indices in input grid.
34
+ 1 dimensional. May be non-unique, depending on the projection method.
35
+ input_indices : NDArray
36
+ Ordered indices for input grid, corresponding to indices in projection grid.
37
+ 1 dimensional. May be non-unique, depending on the projection method.
38
+ If None (default), an arange of the same length as the
39
+ 0th axis of value_array is used.
40
+
41
+ Returns
42
+ -------
43
+ NDArray
44
+ Binned values on the projection grid.
45
+
46
+ Raises
47
+ ------
48
+ ValueError
49
+ If the input and projection indices are not 1D arrays
50
+ with the same number of elements.
51
+ NotImplementedError
52
+ If the input value_array has dimensionality less than 1.
53
+ """
54
+ if input_indices is None:
55
+ input_indices = np.arange(value_array.shape[0])
56
+
57
+ # Both sets of indices must be 1D with the same number of elements
58
+ if input_indices.ndim != 1 or projection_indices.ndim != 1:
59
+ raise ValueError(
60
+ "Indices must be 1D arrays. "
61
+ "If using a rectangular grid, the indices must be unwrapped."
62
+ )
63
+ if input_indices.size != projection_indices.size:
64
+ raise ValueError(
65
+ "The number of input and projection indices must be the same. \n"
66
+ f"Received {input_indices.size} input indices and {projection_indices.size}"
67
+ " projection indices."
68
+ )
69
+
70
+ num_projection_indices = np.prod(projection_grid_shape)
71
+
72
+ if value_array.ndim == 1:
73
+ binned_values = np.bincount(
74
+ projection_indices,
75
+ weights=value_array[input_indices],
76
+ minlength=num_projection_indices,
77
+ )
78
+ elif value_array.ndim >= 2:
79
+ # Apply bincount to each row independently
80
+ binned_values = np.apply_along_axis(
81
+ lambda x: np.bincount(
82
+ projection_indices,
83
+ weights=x[input_indices, ...],
84
+ minlength=num_projection_indices,
85
+ ),
86
+ axis=0,
87
+ arr=value_array,
88
+ )
89
+ else:
90
+ raise NotImplementedError(
91
+ "Only 1+ Dimensional arrays are supported for binning. "
92
+ f"Received array with shape {value_array.shape}."
93
+ )
94
+ return binned_values
95
+
96
+
97
+ def bin_values_at_indices(
98
+ input_values_to_bin: dict[str, NDArray],
99
+ projection_grid_shape: tuple[int, int],
100
+ projection_indices: NDArray,
101
+ input_indices: NDArray | None = None,
102
+ ) -> dict[str, NDArray]:
103
+ """
104
+ Project values from input grid to projection grid based on matched indices.
105
+
106
+ Parameters
107
+ ----------
108
+ input_values_to_bin : dict[str, NDArray]
109
+ Dict matching variable names to arrays of values to bin.
110
+ The 0th axis of each array must be the one and only spatial axis,
111
+ which the indices correspond to and on which the values will be binned.
112
+ The other axes will be binned independently along this 0th axis.
113
+ projection_grid_shape : tuple[int, int]
114
+ The shape of the grid onto which values are projected (rows, columns).
115
+ This size of the resulting grid (rows * columns) will be the size of the
116
+ projected values contained in the output dictionary.
117
+ projection_indices : NDArray
118
+ Ordered indices for projection grid, corresponding to indices in input grid.
119
+ 1 dimensional. May be non-unique, depending on the projection method.
120
+ input_indices : NDArray
121
+ Ordered indices for input grid, corresponding to indices in projection grid.
122
+ 1 dimensional. May be non-unique, depending on the projection method.
123
+ If None (default), behavior is determined by bin_single_array_at_indices.
124
+
125
+ Returns
126
+ -------
127
+ dict[str, NDArray]
128
+ Dict matching the input variable names to the binned values
129
+ on the projection grid.
130
+
131
+ ValueError
132
+ If the input and projection indices are not 1D arrays
133
+ with the same number of elements.
134
+ """
135
+ binned_values_dict = {}
136
+ for value_name, value_array in input_values_to_bin.items():
137
+ logger.info(f"Binning {value_name}")
138
+ binned_values_dict[value_name] = bin_single_array_at_indices(
139
+ value_array=value_array,
140
+ projection_grid_shape=projection_grid_shape,
141
+ projection_indices=projection_indices,
142
+ input_indices=input_indices,
143
+ )
144
+
145
+ return binned_values_dict