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
@@ -13,6 +13,9 @@ from imap_processing.ultra.l1b.ultra_l1b_extended import (
13
13
  determine_species,
14
14
  get_coincidence_positions,
15
15
  get_ctof,
16
+ get_de_az_el,
17
+ get_de_energy_kev,
18
+ get_de_velocity,
16
19
  get_energy_pulse_height,
17
20
  get_energy_ssd,
18
21
  get_front_x_position,
@@ -21,12 +24,11 @@ from imap_processing.ultra.l1b.ultra_l1b_extended import (
21
24
  get_ph_tof_and_back_positions,
22
25
  get_ssd_back_position_and_tof_offset,
23
26
  get_ssd_tof,
24
- get_unit_vector,
25
27
  )
26
28
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
27
29
 
28
30
 
29
- def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
31
+ def calculate_de(de_dataset: xr.Dataset, name: str, data_version: str) -> xr.Dataset:
30
32
  """
31
33
  Create dataset with defined datatypes for Direct Event Data.
32
34
 
@@ -36,6 +38,8 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
36
38
  L1a dataset containing direct event data.
37
39
  name : str
38
40
  Name of the l1a dataset.
41
+ data_version : str
42
+ Version of the data.
39
43
 
40
44
  Returns
41
45
  -------
@@ -45,6 +49,11 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
45
49
  de_dict = {}
46
50
  sensor = parse_filename_like(name)["sensor"][0:2]
47
51
 
52
+ # Drop events with invalid start type.
53
+ de_dataset = de_dataset.where(
54
+ de_dataset["START_TYPE"] != np.iinfo(np.int64).min, drop=True
55
+ )
56
+
48
57
  # Instantiate arrays
49
58
  yf = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
50
59
  xb = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
@@ -55,17 +64,13 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
55
64
  tof = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
56
65
  etof = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
57
66
  ctof = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
67
+ magnitude_v = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
58
68
  energy = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
59
- # TODO: Confirm with Ultra team what fill values and dtype we want.
60
69
  species_bin = np.full(len(de_dataset["epoch"]), "UNKNOWN", dtype="U10")
61
70
  t2 = np.full(len(de_dataset["epoch"]), np.nan, dtype=np.float32)
62
71
 
63
- # Drop events with invalid start type.
64
- de_dataset = de_dataset.where(
65
- de_dataset["START_TYPE"] != np.iinfo(np.int64).min, drop=True
66
- )
67
72
  # Define epoch.
68
- de_dict["epoch"] = de_dataset["epoch"]
73
+ de_dict["epoch"] = de_dataset["epoch"].data
69
74
 
70
75
  xf = get_front_x_position(
71
76
  de_dataset["START_TYPE"].data,
@@ -97,7 +102,9 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
97
102
  etof[ph_indices], xc[ph_indices] = get_coincidence_positions(
98
103
  de_dataset.isel(epoch=ph_indices), t2[ph_indices], f"ultra{sensor}"
99
104
  )
100
- ctof[ph_indices], _ = get_ctof(tof[ph_indices], r[ph_indices], "PH")
105
+ ctof[ph_indices], magnitude_v[ph_indices] = get_ctof(
106
+ tof[ph_indices], r[ph_indices], "PH"
107
+ )
101
108
 
102
109
  # SSD
103
110
  ssd_indices = np.nonzero(np.isin(de_dataset["STOP_TYPE"], StopType.SSD.value))[0]
@@ -118,7 +125,9 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
118
125
  species_bin[ssd_indices] = determine_species(
119
126
  tof[ssd_indices], r[ssd_indices], "SSD"
120
127
  )
121
- ctof[ssd_indices], _ = get_ctof(tof[ssd_indices], r[ssd_indices], "SSD")
128
+ ctof[ssd_indices], magnitude_v[ssd_indices] = get_ctof(
129
+ tof[ssd_indices], r[ssd_indices], "SSD"
130
+ )
122
131
 
123
132
  # Combine ph_yb and ssd_yb along with their indices
124
133
  de_dict["x_front"] = xf.astype(np.float32)
@@ -129,6 +138,7 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
129
138
  de_dict["tof_start_stop"] = tof
130
139
  de_dict["tof_stop_coin"] = etof
131
140
  de_dict["tof_corrected"] = ctof
141
+ de_dict["velocity_magnitude"] = magnitude_v
132
142
  de_dict["front_back_distance"] = d
133
143
  de_dict["path_length"] = r
134
144
 
@@ -137,61 +147,46 @@ def calculate_de(de_dataset: xr.Dataset, name: str) -> xr.Dataset:
137
147
  "start_type",
138
148
  "event_type",
139
149
  "de_event_met",
150
+ "event_times",
140
151
  ]
141
- dataset_keys = ["COIN_TYPE", "START_TYPE", "STOP_TYPE", "SHCOARSE"]
152
+ dataset_keys = ["COIN_TYPE", "START_TYPE", "STOP_TYPE", "SHCOARSE", "EVENTTIMES"]
142
153
 
143
154
  de_dict.update(
144
155
  {key: de_dataset[dataset_key] for key, dataset_key in zip(keys, dataset_keys)}
145
156
  )
146
157
 
147
- vx_ultra, vy_ultra, vz_ultra = get_unit_vector(
158
+ v = get_de_velocity(
148
159
  (de_dict["x_front"], de_dict["y_front"]),
149
160
  (de_dict["x_back"], de_dict["y_back"]),
150
161
  de_dict["front_back_distance"],
151
162
  de_dict["tof_start_stop"],
152
163
  )
164
+ de_dict["direct_event_velocity"] = v.astype(np.float32)
153
165
 
154
- de_dict["vx_ultra"] = vx_ultra.astype(np.float32)
155
- de_dict["vy_ultra"] = vy_ultra.astype(np.float32)
156
- de_dict["vz_ultra"] = vz_ultra.astype(np.float32)
166
+ de_dict["tof_energy"] = get_de_energy_kev(v, species_bin)
167
+ de_dict["azimuth"], de_dict["elevation"] = get_de_az_el(v)
157
168
  de_dict["energy"] = energy
158
169
  de_dict["species"] = species_bin
159
170
 
160
171
  # Annotated Events.
161
- position = np.stack(
162
- (de_dict["vx_ultra"], de_dict["vy_ultra"], de_dict["vz_ultra"]), axis=-1
163
- )
164
-
165
172
  ultra_frame = getattr(SpiceFrame, f"IMAP_ULTRA_{sensor}")
166
173
  sc_velocity, sc_dps_velocity, helio_velocity = get_annotated_particle_velocity(
167
- de_dataset.data_vars["EVENTTIMES"],
168
- position,
174
+ de_dataset.data_vars["EVENTTIMES"].values,
175
+ de_dict["direct_event_velocity"],
169
176
  ultra_frame,
170
177
  SpiceFrame.IMAP_DPS,
171
178
  SpiceFrame.IMAP_SPACECRAFT,
172
179
  )
173
180
 
174
- de_dict["vx_sc"], de_dict["vy_sc"], de_dict["vz_sc"] = (
175
- sc_velocity[:, 0],
176
- sc_velocity[:, 1],
177
- sc_velocity[:, 2],
178
- )
179
- de_dict["vx_dps_sc"], de_dict["vy_dps_sc"], de_dict["vz_dps_sc"] = (
180
- sc_dps_velocity[:, 0],
181
- sc_dps_velocity[:, 1],
182
- sc_dps_velocity[:, 2],
183
- )
184
- de_dict["vx_dps_helio"], de_dict["vy_dps_helio"], de_dict["vz_dps_helio"] = (
185
- helio_velocity[:, 0],
186
- helio_velocity[:, 1],
187
- helio_velocity[:, 2],
188
- )
181
+ de_dict["velocity_sc"] = sc_velocity
182
+ de_dict["velocity_dps_sc"] = sc_dps_velocity
183
+ de_dict["velocity_dps_helio"] = helio_velocity
189
184
 
190
185
  # TODO: TBD.
191
186
  de_dict["event_efficiency"] = np.full(
192
187
  len(de_dataset["epoch"]), np.nan, dtype=np.float32
193
188
  )
194
189
 
195
- dataset = create_dataset(de_dict, name, "l1b")
190
+ dataset = create_dataset(de_dict, name, "l1b", data_version)
196
191
 
197
192
  return dataset
@@ -1,21 +1,38 @@
1
1
  """Calculate Extended Spin."""
2
2
 
3
- import numpy as np
4
3
  import xarray as xr
5
4
 
5
+ from imap_processing.ultra.l1b.ultra_l1b_culling import (
6
+ flag_attitude,
7
+ flag_spin,
8
+ get_energy_histogram,
9
+ get_spin,
10
+ )
6
11
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
7
12
 
8
13
 
9
- def calculate_extendedspin(rates_dataset: xr.Dataset, name: str) -> xr.Dataset:
14
+ def calculate_extendedspin(
15
+ hk_dataset: xr.Dataset,
16
+ rates_dataset: xr.Dataset,
17
+ de_dataset: xr.Dataset,
18
+ name: str,
19
+ data_version: str,
20
+ ) -> xr.Dataset:
10
21
  """
11
22
  Create dataset with defined datatypes for Extended Spin Data.
12
23
 
13
24
  Parameters
14
25
  ----------
26
+ hk_dataset : xarray.Dataset
27
+ Dataset containing l1a hk data.
15
28
  rates_dataset : xarray.Dataset
16
- Dataset containing rates data.
29
+ Dataset containing l1a rates data.
30
+ de_dataset : xarray.Dataset
31
+ Dataset containing l1b de data.
17
32
  name : str
18
33
  Name of the dataset.
34
+ data_version : str
35
+ Version of the data.
19
36
 
20
37
  Returns
21
38
  -------
@@ -23,23 +40,30 @@ def calculate_extendedspin(rates_dataset: xr.Dataset, name: str) -> xr.Dataset:
23
40
  Dataset containing the data.
24
41
  """
25
42
  extendedspin_dict = {}
43
+ rates_qf, spin, energy_midpoints, n_sigma_per_energy = flag_spin(
44
+ de_dataset["event_times"].values,
45
+ de_dataset["energy"].values,
46
+ )
47
+ spin_number = get_spin(de_dataset["event_times"].values)
48
+ count_rates, _, counts, _ = get_energy_histogram(
49
+ spin_number, de_dataset["energy"].values
50
+ )
51
+ attitude_qf, spin_rates, spin_period, spin_starttime = flag_attitude(
52
+ de_dataset["event_times"].values
53
+ )
26
54
 
27
- epoch = rates_dataset.coords["epoch"].values
28
-
29
- # Placeholder for calculations
30
- extendedspin_dict["epoch"] = epoch
31
- extendedspin_dict["spin_number"] = np.zeros(len(epoch), dtype=np.uint64)
32
- extendedspin_dict["spin_start_time"] = np.zeros(len(epoch), dtype=np.float64)
33
- extendedspin_dict["avg_spin_period"] = np.zeros(len(epoch), dtype=np.float64)
34
- extendedspin_dict["rate_start_pulses"] = np.zeros(len(epoch), dtype=np.float64)
35
- extendedspin_dict["rate_stop_pulses"] = np.zeros(len(epoch), dtype=np.float64)
36
- extendedspin_dict["rate_coin_pulses"] = np.zeros(len(epoch), dtype=np.float64)
37
- extendedspin_dict["rate_processed_events"] = np.zeros(len(epoch), dtype=np.float64)
38
- extendedspin_dict["rate_rejected_events"] = np.zeros(len(epoch), dtype=np.float64)
39
- extendedspin_dict["quality_hk"] = np.zeros(len(epoch), dtype=np.uint16)
40
- extendedspin_dict["quality_attitude"] = np.zeros(len(epoch), dtype=np.uint16)
41
- extendedspin_dict["quality_instruments"] = np.zeros(len(epoch), dtype=np.uint16)
42
-
43
- extendedspin_dataset = create_dataset(extendedspin_dict, name, "l1b")
55
+ # These will be the coordinates.
56
+ extendedspin_dict["spin_number"] = spin
57
+ extendedspin_dict["energy_bin_geometric_mean"] = energy_midpoints
58
+
59
+ extendedspin_dict["ena_rates"] = count_rates
60
+ extendedspin_dict["ena_rates_threshold"] = n_sigma_per_energy
61
+ extendedspin_dict["spin_start_time"] = spin_starttime
62
+ extendedspin_dict["spin_period"] = spin_period
63
+ extendedspin_dict["spin_rate"] = spin_rates
64
+ extendedspin_dict["quality_attitude"] = attitude_qf
65
+ extendedspin_dict["quality_ena_rates"] = rates_qf
66
+
67
+ extendedspin_dataset = create_dataset(extendedspin_dict, name, "l1b", data_version)
44
68
 
45
69
  return extendedspin_dataset
@@ -27,38 +27,37 @@ def ultra_l1b(data_dict: dict, data_version: str) -> list[xr.Dataset]:
27
27
  output_datasets = []
28
28
  instrument_id = 45 if any("45" in key for key in data_dict.keys()) else 90
29
29
 
30
- if f"imap_ultra_l1a_{instrument_id}sensor-rates" in data_dict:
30
+ if (
31
+ f"imap_ultra_l1a_{instrument_id}sensor-hk" in data_dict
32
+ and f"imap_ultra_l1a_{instrument_id}sensor-de" in data_dict
33
+ and f"imap_ultra_l1a_{instrument_id}sensor-rates" in data_dict
34
+ ):
35
+ de_dataset = calculate_de(
36
+ data_dict[f"imap_ultra_l1a_{instrument_id}sensor-de"],
37
+ f"imap_ultra_l1b_{instrument_id}sensor-de",
38
+ data_version,
39
+ )
31
40
  extendedspin_dataset = calculate_extendedspin(
41
+ data_dict[f"imap_ultra_l1a_{instrument_id}sensor-hk"],
32
42
  data_dict[f"imap_ultra_l1a_{instrument_id}sensor-rates"],
43
+ de_dataset,
33
44
  f"imap_ultra_l1b_{instrument_id}sensor-extendedspin",
45
+ data_version,
34
46
  )
35
- # TODO: move these to use ImapCdfAttributes().add_global_attribute()
36
- extendedspin_dataset.attrs["Data_version"] = data_version
37
-
38
47
  cullingmask_dataset = calculate_cullingmask(
39
- extendedspin_dataset, f"imap_ultra_l1b_{instrument_id}sensor-cullingmask"
48
+ extendedspin_dataset,
49
+ f"imap_ultra_l1b_{instrument_id}sensor-cullingmask",
50
+ data_version,
40
51
  )
41
- cullingmask_dataset.attrs["Data_version"] = data_version
42
-
43
52
  badtimes_dataset = calculate_badtimes(
44
- extendedspin_dataset, f"imap_ultra_l1b_{instrument_id}sensor-badtimes"
53
+ extendedspin_dataset,
54
+ cullingmask_dataset["spin_number"].values,
55
+ f"imap_ultra_l1b_{instrument_id}sensor-badtimes",
56
+ data_version,
45
57
  )
46
- badtimes_dataset.attrs["Data_version"] = data_version
47
-
48
58
  output_datasets.extend(
49
- [extendedspin_dataset, cullingmask_dataset, badtimes_dataset]
50
- )
51
- elif (
52
- f"imap_ultra_l1a_{instrument_id}sensor-aux" in data_dict
53
- and f"imap_ultra_l1a_{instrument_id}sensor-de" in data_dict
54
- ):
55
- de_dataset = calculate_de(
56
- data_dict[f"imap_ultra_l1a_{instrument_id}sensor-de"],
57
- f"imap_ultra_l1b_{instrument_id}sensor-de",
59
+ [de_dataset, extendedspin_dataset, cullingmask_dataset, badtimes_dataset]
58
60
  )
59
- de_dataset.attrs["Data_version"] = data_version
60
-
61
- output_datasets.append(de_dataset)
62
61
  else:
63
62
  raise ValueError("Data dictionary does not contain the expected keys.")
64
63
 
@@ -0,0 +1,190 @@
1
+ """Culls Events for ULTRA L1b."""
2
+
3
+ import numpy as np
4
+ from numpy.typing import NDArray
5
+
6
+ from imap_processing.quality_flags import ImapAttitudeUltraFlags, ImapRatesUltraFlags
7
+ from imap_processing.spice.spin import get_spin_data, interpolate_spin_data
8
+ from imap_processing.ultra.constants import UltraConstants
9
+
10
+
11
+ def get_spin(eventtimes_met: NDArray) -> NDArray:
12
+ """
13
+ Get spin number for each event.
14
+
15
+ Parameters
16
+ ----------
17
+ eventtimes_met : NDArray
18
+ Event Times in Mission Elapsed Time.
19
+
20
+ Returns
21
+ -------
22
+ spin_number : NDArray
23
+ Spin number at each event derived the from Universal Spin Table.
24
+ """
25
+ spin_df = interpolate_spin_data(eventtimes_met)
26
+ return spin_df["spin_number"].values
27
+
28
+
29
+ def get_energy_histogram(
30
+ spin_number: NDArray, energy: NDArray
31
+ ) -> tuple[NDArray, NDArray, NDArray, float]:
32
+ """
33
+ Compute a 2D histogram of the counts binned by energy and spin number.
34
+
35
+ Parameters
36
+ ----------
37
+ spin_number : NDArray
38
+ Spin number.
39
+ energy : NDArray
40
+ The particle energy.
41
+
42
+ Returns
43
+ -------
44
+ hist : NDArray
45
+ A 2D histogram array containing the
46
+ count rate per spin at each energy bin.
47
+ spin_edges : NDArray
48
+ Edges of the spin number bins.
49
+ counts : NDArray
50
+ A 2D histogram array containing the
51
+ counts per spin at each energy bin.
52
+ mean_duration : float
53
+ Mean duration of the spin.
54
+ """
55
+ spin_df = get_spin_data()
56
+
57
+ spin_edges = np.unique(spin_number)
58
+ spin_edges = np.append(spin_edges, spin_edges.max() + 1)
59
+
60
+ # Counts per spin at each energy bin.
61
+ hist, _ = np.histogramdd(
62
+ sample=(energy, spin_number),
63
+ bins=[UltraConstants.CULLING_ENERGY_BIN_EDGES, spin_edges],
64
+ )
65
+
66
+ counts = hist.copy()
67
+ total_spin_duration = 0
68
+
69
+ # Count rate per spin at each energy bin.
70
+ for i in range(hist.shape[1]):
71
+ spin_duration = spin_df.spin_period_sec[spin_df.spin_number == i]
72
+ hist[:, i] /= spin_duration.values[0]
73
+ total_spin_duration += spin_duration.sum()
74
+
75
+ mean_duration = total_spin_duration / hist.shape[1]
76
+
77
+ return hist, spin_edges, counts, mean_duration
78
+
79
+
80
+ def flag_attitude(eventtimes_met: NDArray) -> tuple[NDArray, NDArray, NDArray, NDArray]:
81
+ """
82
+ Flag data based on attitude.
83
+
84
+ Parameters
85
+ ----------
86
+ eventtimes_met : NDArray
87
+ Event Times in Mission Elapsed Time.
88
+
89
+ Returns
90
+ -------
91
+ quality_flags : NDArray
92
+ Quality flags.
93
+ spin_rates : NDArray
94
+ Spin rates.
95
+ spin_period : NDArray
96
+ Spin period.
97
+ spin_starttime : NDArray
98
+ Spin start time.
99
+ """
100
+ spins = np.unique(get_spin(eventtimes_met)) # Get unique spins
101
+ spin_df = get_spin_data() # Load spin data
102
+
103
+ spin_period = spin_df.loc[spin_df.spin_number.isin(spins), "spin_period_sec"]
104
+ spin_starttime = spin_df.loc[spin_df.spin_number.isin(spins), "spin_start_time"]
105
+ spin_rates = 60 / spin_period # 60 seconds in a minute
106
+ bad_spin_rate_indices = (spin_rates < UltraConstants.CULLING_RPM_MIN) | (
107
+ spin_rates > UltraConstants.CULLING_RPM_MAX
108
+ )
109
+
110
+ quality_flags = np.full(
111
+ spin_rates.shape, ImapAttitudeUltraFlags.NONE.value, dtype=np.uint16
112
+ )
113
+ quality_flags[bad_spin_rate_indices] |= ImapAttitudeUltraFlags.SPINRATE.value
114
+
115
+ return quality_flags, spin_rates, spin_period, spin_starttime
116
+
117
+
118
+ def get_n_sigma(count_rates: NDArray, mean_duration: float, sigma: int = 6) -> NDArray:
119
+ """
120
+ Calculate the threshold for the HIGHRATES flag.
121
+
122
+ Parameters
123
+ ----------
124
+ count_rates : NDArray
125
+ A 2D histogram array containing the
126
+ count rates per spin at each energy bin.
127
+ mean_duration : float
128
+ Mean duration of the spins.
129
+ sigma : int (default=6)
130
+ The number of sigma.
131
+
132
+ Returns
133
+ -------
134
+ threshold : NDArray
135
+ Threshold for applying HIGHRATES flag.
136
+ """
137
+ sigma_per_energy = np.std(count_rates, axis=1)
138
+ n_sigma_per_energy = sigma * sigma_per_energy
139
+ mean_per_energy = np.mean(count_rates, axis=1)
140
+ # Must have a HIGHRATES threshold of at least 3 counts per spin.
141
+ threshold = np.maximum(mean_per_energy + n_sigma_per_energy, 3 / mean_duration)
142
+
143
+ return threshold
144
+
145
+
146
+ def flag_spin(
147
+ eventtimes_met: NDArray, energy: NDArray, sigma: int = 6
148
+ ) -> tuple[NDArray, NDArray, NDArray, NDArray]:
149
+ """
150
+ Flag data based on counts and negative energies.
151
+
152
+ Parameters
153
+ ----------
154
+ eventtimes_met : NDArray
155
+ Event Times in Mission Elapsed Time.
156
+ energy : NDArray
157
+ Energy data.
158
+ sigma : int (default=6)
159
+ The number of sigma.
160
+
161
+ Returns
162
+ -------
163
+ quality_flags : NDArray
164
+ Quality flags.
165
+ spin : NDArray
166
+ Spin data.
167
+ energy_midpoints : NDArray
168
+ Energy midpoint data.
169
+ n_sigma_per_energy_reshape : NDArray
170
+ N sigma per energy.
171
+ """
172
+ spin = get_spin(eventtimes_met)
173
+ count_rates, spin_edges, counts, duration = get_energy_histogram(spin, energy)
174
+ quality_flags = np.full(
175
+ count_rates.shape, ImapRatesUltraFlags.NONE.value, dtype=np.uint16
176
+ )
177
+
178
+ # Zero counts/spin/energy level
179
+ quality_flags[counts == 0] |= ImapRatesUltraFlags.ZEROCOUNTS.value
180
+ threshold = get_n_sigma(count_rates, duration, sigma=sigma)
181
+
182
+ bin_edges = np.array(UltraConstants.CULLING_ENERGY_BIN_EDGES)
183
+ energy_midpoints = np.sqrt(bin_edges[:-1] * bin_edges[1:])
184
+ spin = np.unique(spin)
185
+
186
+ # Indices where the counts exceed the threshold
187
+ indices_n_sigma = count_rates > threshold[:, np.newaxis]
188
+ quality_flags[indices_n_sigma] |= ImapRatesUltraFlags.HIGHRATES.value
189
+
190
+ return quality_flags, spin, energy_midpoints, threshold
@@ -10,6 +10,7 @@ import xarray
10
10
  from numpy import ndarray
11
11
  from numpy.typing import NDArray
12
12
 
13
+ from imap_processing.spice.geometry import cartesian_to_spherical
13
14
  from imap_processing.ultra.constants import UltraConstants
14
15
  from imap_processing.ultra.l1b.lookup_utils import (
15
16
  get_back_position,
@@ -437,20 +438,14 @@ def get_coincidence_positions(
437
438
  return etof, xc_array * 100
438
439
 
439
440
 
440
- def get_unit_vector(
441
+ def get_de_velocity(
441
442
  front_position: tuple[NDArray, NDArray],
442
443
  back_position: tuple[NDArray, NDArray],
443
444
  d: np.ndarray,
444
445
  tof: np.ndarray,
445
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
446
+ ) -> NDArray:
446
447
  """
447
- Determine the particle velocity.
448
-
449
- The equation is: velocity = ((xf - xb), (yf - yb), d).
450
-
451
- Further description is available on pages 39 of
452
- IMAP-Ultra Flight Software Specification document
453
- (7523-9009_Rev_-.pdf).
448
+ Determine the direct event velocity.
454
449
 
455
450
  Parameters
456
451
  ----------
@@ -465,35 +460,30 @@ def get_unit_vector(
465
460
 
466
461
  Returns
467
462
  -------
468
- vhat_x : np.array
469
- Normalized component of the velocity vector in x direction.
470
- vhat_y : np.array
471
- Normalized component of the velocity vector in y direction.
472
- vhat_z : np.array
473
- Normalized component of the velocity vector in z direction.
463
+ velocities : np.ndarray
464
+ N x 3 array of velocity components (vx, vy, vz) in km/s.
474
465
  """
475
466
  if tof[tof < 0].any():
476
467
  logger.info("Negative tof values found.")
477
468
 
478
- delta_x = front_position[0] - back_position[0]
479
- delta_y = front_position[1] - back_position[1]
480
-
481
- v_x = delta_x / tof
482
- v_y = delta_y / tof
483
- v_z = d / tof
469
+ # distances in .1 mm
470
+ delta_v = np.empty((len(d), 3), dtype=np.float32)
471
+ delta_v[:, 0] = (front_position[0] - back_position[0]) * 0.1
472
+ delta_v[:, 1] = (front_position[1] - back_position[1]) * 0.1
473
+ delta_v[:, 2] = d * 0.1
484
474
 
485
- # Magnitude of the velocity vector
486
- magnitude_v = np.sqrt(v_x**2 + v_y**2 + v_z**2)
475
+ # Convert from 0.1mm/0.1ns to km/s.
476
+ v_x = delta_v[:, 0] / tof * 1e3
477
+ v_y = delta_v[:, 1] / tof * 1e3
478
+ v_z = delta_v[:, 2] / tof * 1e3
487
479
 
488
- vhat_x = -v_x / magnitude_v
489
- vhat_y = -v_y / magnitude_v
490
- vhat_z = -v_z / magnitude_v
480
+ v_x[tof < 0] = np.nan # used as fillvals
481
+ v_y[tof < 0] = np.nan
482
+ v_z[tof < 0] = np.nan
491
483
 
492
- vhat_x[tof < 0] = np.nan # used as fillvals
493
- vhat_y[tof < 0] = np.nan
494
- vhat_z[tof < 0] = np.nan
484
+ velocities = np.vstack((v_x, v_y, v_z)).T
495
485
 
496
- return vhat_x, vhat_y, vhat_z
486
+ return velocities
497
487
 
498
488
 
499
489
  def get_ssd_tof(de_dataset: xarray.Dataset, xf: np.ndarray) -> NDArray[np.float64]:
@@ -545,6 +535,37 @@ def get_ssd_tof(de_dataset: xarray.Dataset, xf: np.ndarray) -> NDArray[np.float6
545
535
  return np.asarray(tof, dtype=np.float64)
546
536
 
547
537
 
538
+ def get_de_energy_kev(v: np.ndarray, species: np.ndarray) -> NDArray:
539
+ """
540
+ Calculate the direct event energy.
541
+
542
+ Parameters
543
+ ----------
544
+ v : np.ndarray
545
+ N x 3 array of velocity components (vx, vy, vz) in km/s.
546
+ species : np.ndarray
547
+ Species of the particle.
548
+
549
+ Returns
550
+ -------
551
+ energy : np.ndarray
552
+ Energy of the direct event in keV.
553
+ """
554
+ vv = v * 1e3 # convert km/s to m/s
555
+ # Compute the sum of squares.
556
+ v2 = np.sum(vv**2, axis=1)
557
+
558
+ index_hydrogen = np.where(species == "H")
559
+ energy = np.full_like(v2, np.nan)
560
+
561
+ # 1/2 mv^2 in Joules, convert to keV
562
+ energy[index_hydrogen] = (
563
+ 0.5 * UltraConstants.MASS_H * v2[index_hydrogen] * UltraConstants.J_KEV
564
+ )
565
+
566
+ return energy
567
+
568
+
548
569
  def get_energy_pulse_height(
549
570
  stop_type: np.ndarray, energy: np.ndarray, xb: np.ndarray, yb: np.ndarray
550
571
  ) -> NDArray[np.float64]:
@@ -728,3 +749,33 @@ def determine_species(tof: np.ndarray, path_length: np.ndarray, type: str) -> ND
728
749
  ] = "H"
729
750
 
730
751
  return species_bin
752
+
753
+
754
+ def get_de_az_el(v: NDArray) -> tuple[NDArray, NDArray]:
755
+ """
756
+ Compute azimuth (phi) angles and elevation (theta).
757
+
758
+ Parameters
759
+ ----------
760
+ v : np.ndarray
761
+ A NumPy array with shape (n, 3) where each
762
+ row represents a vector
763
+ with x, y, z-components.
764
+
765
+ Returns
766
+ -------
767
+ spherical_coords : np.ndarray
768
+ A NumPy array with shape (n, 3), where each row contains
769
+ the spherical coordinates (r, azimuth, elevation):
770
+
771
+ - azimuth : angle in the xy-plane
772
+ In radians:
773
+ output range=[0, 2*pi].
774
+ - elevation : angle from the xy-plane
775
+ In radians:
776
+ output range=[-pi/2, pi/2].
777
+ """
778
+ # Compute azimuth (phi) angles and elevation (theta)
779
+ spherical_coords = cartesian_to_spherical(v, degrees=False)
780
+
781
+ return spherical_coords[:, 1], spherical_coords[:, 2]
@@ -6,7 +6,9 @@ import xarray as xr
6
6
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
7
7
 
8
8
 
9
- def calculate_histogram(histogram_dataset: xr.Dataset, name: str) -> xr.Dataset:
9
+ def calculate_histogram(
10
+ histogram_dataset: xr.Dataset, name: str, data_version: str
11
+ ) -> xr.Dataset:
10
12
  """
11
13
  Create dictionary with defined datatype for Histogram Data.
12
14
 
@@ -16,6 +18,8 @@ def calculate_histogram(histogram_dataset: xr.Dataset, name: str) -> xr.Dataset:
16
18
  Dataset containing histogram data.
17
19
  name : str
18
20
  Name of dataset.
21
+ data_version : str
22
+ Version of the data.
19
23
 
20
24
  Returns
21
25
  -------
@@ -31,6 +35,6 @@ def calculate_histogram(histogram_dataset: xr.Dataset, name: str) -> xr.Dataset:
31
35
  histogram_dict["epoch"] = epoch
32
36
  histogram_dict["sid"] = np.zeros(len(epoch), dtype=np.uint8)
33
37
 
34
- dataset = create_dataset(histogram_dict, name, "l1c")
38
+ dataset = create_dataset(histogram_dict, name, "l1c", data_version)
35
39
 
36
40
  return dataset