imap-processing 0.11.0__py3-none-any.whl → 0.13.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 (415) hide show
  1. imap_processing/__init__.py +11 -11
  2. imap_processing/_version.py +2 -2
  3. imap_processing/ccsds/ccsds_data.py +1 -2
  4. imap_processing/ccsds/excel_to_xtce.py +66 -18
  5. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +24 -40
  6. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +934 -42
  7. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +1846 -128
  8. imap_processing/cdf/config/imap_glows_global_cdf_attrs.yaml +0 -5
  9. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +10 -11
  10. imap_processing/cdf/config/imap_hi_variable_attrs.yaml +17 -19
  11. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +27 -14
  12. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +106 -116
  13. imap_processing/cdf/config/imap_hit_l1b_variable_attrs.yaml +120 -145
  14. imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml +14 -0
  15. imap_processing/cdf/config/imap_idex_global_cdf_attrs.yaml +25 -9
  16. imap_processing/cdf/config/imap_idex_l1a_variable_attrs.yaml +6 -4
  17. imap_processing/cdf/config/imap_idex_l1b_variable_attrs.yaml +3 -3
  18. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +0 -12
  19. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +1 -1
  20. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +23 -20
  21. imap_processing/cdf/config/imap_mag_l1a_variable_attrs.yaml +361 -0
  22. imap_processing/cdf/config/imap_mag_l1b_variable_attrs.yaml +160 -0
  23. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +160 -0
  24. imap_processing/cdf/config/imap_spacecraft_global_cdf_attrs.yaml +18 -0
  25. imap_processing/cdf/config/imap_spacecraft_variable_attrs.yaml +40 -0
  26. imap_processing/cdf/config/imap_swapi_global_cdf_attrs.yaml +1 -5
  27. imap_processing/cdf/config/imap_swapi_variable_attrs.yaml +22 -0
  28. imap_processing/cdf/config/imap_swe_global_cdf_attrs.yaml +12 -4
  29. imap_processing/cdf/config/imap_swe_l1a_variable_attrs.yaml +16 -2
  30. imap_processing/cdf/config/imap_swe_l1b_variable_attrs.yaml +64 -52
  31. imap_processing/cdf/config/imap_swe_l2_variable_attrs.yaml +71 -47
  32. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +180 -19
  33. imap_processing/cdf/config/imap_ultra_l1a_variable_attrs.yaml +5045 -41
  34. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +80 -17
  35. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +32 -57
  36. imap_processing/cdf/utils.py +52 -38
  37. imap_processing/cli.py +477 -233
  38. imap_processing/codice/codice_l1a.py +466 -131
  39. imap_processing/codice/codice_l1b.py +51 -152
  40. imap_processing/codice/constants.py +1360 -569
  41. imap_processing/codice/decompress.py +2 -6
  42. imap_processing/ena_maps/ena_maps.py +1103 -146
  43. imap_processing/ena_maps/utils/coordinates.py +19 -0
  44. imap_processing/ena_maps/utils/map_utils.py +14 -17
  45. imap_processing/ena_maps/utils/spatial_utils.py +55 -52
  46. imap_processing/glows/l1a/glows_l1a.py +28 -99
  47. imap_processing/glows/l1a/glows_l1a_data.py +2 -2
  48. imap_processing/glows/l1b/glows_l1b.py +1 -4
  49. imap_processing/glows/l1b/glows_l1b_data.py +1 -3
  50. imap_processing/glows/l2/glows_l2.py +2 -5
  51. imap_processing/hi/l1a/hi_l1a.py +54 -29
  52. imap_processing/hi/l1a/histogram.py +0 -1
  53. imap_processing/hi/l1a/science_direct_event.py +6 -8
  54. imap_processing/hi/l1b/hi_l1b.py +111 -82
  55. imap_processing/hi/l1c/hi_l1c.py +416 -32
  56. imap_processing/hi/utils.py +58 -12
  57. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-sector-dt0-factors_20250219_v002.csv +81 -0
  58. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt0-factors_20250219_v002.csv +205 -0
  59. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt1-factors_20250219_v002.csv +205 -0
  60. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt2-factors_20250219_v002.csv +205 -0
  61. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-standard-dt3-factors_20250219_v002.csv +205 -0
  62. imap_processing/hit/ancillary/imap_hit_l1b-to-l2-summed-dt0-factors_20250219_v002.csv +68 -0
  63. imap_processing/hit/hit_utils.py +235 -5
  64. imap_processing/hit/l0/constants.py +20 -11
  65. imap_processing/hit/l0/decom_hit.py +21 -5
  66. imap_processing/hit/l1a/hit_l1a.py +71 -75
  67. imap_processing/hit/l1b/constants.py +321 -0
  68. imap_processing/hit/l1b/hit_l1b.py +377 -67
  69. imap_processing/hit/l2/constants.py +318 -0
  70. imap_processing/hit/l2/hit_l2.py +723 -0
  71. imap_processing/hit/packet_definitions/hit_packet_definitions.xml +1323 -71
  72. imap_processing/ialirt/l0/mag_l0_ialirt_data.py +155 -0
  73. imap_processing/ialirt/l0/parse_mag.py +374 -0
  74. imap_processing/ialirt/l0/process_swapi.py +69 -0
  75. imap_processing/ialirt/l0/process_swe.py +548 -0
  76. imap_processing/ialirt/packet_definitions/ialirt.xml +216 -208
  77. imap_processing/ialirt/packet_definitions/ialirt_codicehi.xml +1 -1
  78. imap_processing/ialirt/packet_definitions/ialirt_codicelo.xml +1 -1
  79. imap_processing/ialirt/packet_definitions/ialirt_mag.xml +115 -0
  80. imap_processing/ialirt/packet_definitions/ialirt_swapi.xml +14 -14
  81. imap_processing/ialirt/utils/grouping.py +114 -0
  82. imap_processing/ialirt/utils/time.py +29 -0
  83. imap_processing/idex/atomic_masses.csv +22 -0
  84. imap_processing/idex/decode.py +2 -2
  85. imap_processing/idex/idex_constants.py +33 -0
  86. imap_processing/idex/idex_l0.py +22 -8
  87. imap_processing/idex/idex_l1a.py +81 -51
  88. imap_processing/idex/idex_l1b.py +13 -39
  89. imap_processing/idex/idex_l2a.py +823 -0
  90. imap_processing/idex/idex_l2b.py +120 -0
  91. imap_processing/idex/idex_variable_unpacking_and_eu_conversion.csv +11 -11
  92. imap_processing/idex/packet_definitions/idex_housekeeping_packet_definition.xml +9130 -0
  93. imap_processing/lo/l0/lo_science.py +7 -2
  94. imap_processing/lo/l1a/lo_l1a.py +1 -5
  95. imap_processing/lo/l1b/lo_l1b.py +702 -29
  96. imap_processing/lo/l1b/tof_conversions.py +11 -0
  97. imap_processing/lo/l1c/lo_l1c.py +1 -4
  98. imap_processing/mag/constants.py +51 -0
  99. imap_processing/mag/imap_mag_sdc_configuration_v001.py +8 -0
  100. imap_processing/mag/l0/decom_mag.py +10 -3
  101. imap_processing/mag/l1a/mag_l1a.py +23 -19
  102. imap_processing/mag/l1a/mag_l1a_data.py +35 -10
  103. imap_processing/mag/l1b/mag_l1b.py +259 -50
  104. imap_processing/mag/l1c/interpolation_methods.py +388 -0
  105. imap_processing/mag/l1c/mag_l1c.py +621 -17
  106. imap_processing/mag/l2/mag_l2.py +140 -0
  107. imap_processing/mag/l2/mag_l2_data.py +288 -0
  108. imap_processing/quality_flags.py +1 -0
  109. imap_processing/spacecraft/packet_definitions/scid_x252.xml +538 -0
  110. imap_processing/spacecraft/quaternions.py +121 -0
  111. imap_processing/spice/geometry.py +19 -22
  112. imap_processing/spice/kernels.py +0 -276
  113. imap_processing/spice/pointing_frame.py +257 -0
  114. imap_processing/spice/repoint.py +149 -0
  115. imap_processing/spice/spin.py +38 -33
  116. imap_processing/spice/time.py +24 -0
  117. imap_processing/swapi/l1/swapi_l1.py +20 -12
  118. imap_processing/swapi/l2/swapi_l2.py +116 -5
  119. imap_processing/swapi/swapi_utils.py +32 -0
  120. imap_processing/swe/l1a/swe_l1a.py +44 -12
  121. imap_processing/swe/l1a/swe_science.py +13 -13
  122. imap_processing/swe/l1b/swe_l1b.py +898 -23
  123. imap_processing/swe/l2/swe_l2.py +75 -136
  124. imap_processing/swe/packet_definitions/swe_packet_definition.xml +1121 -1
  125. imap_processing/swe/utils/swe_constants.py +64 -0
  126. imap_processing/swe/utils/swe_utils.py +85 -28
  127. imap_processing/tests/ccsds/test_data/expected_output.xml +40 -1
  128. imap_processing/tests/ccsds/test_excel_to_xtce.py +24 -21
  129. imap_processing/tests/cdf/test_data/imap_instrument2_global_cdf_attrs.yaml +0 -2
  130. imap_processing/tests/cdf/test_utils.py +14 -16
  131. imap_processing/tests/codice/conftest.py +44 -33
  132. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-counters-aggregated_20241110193700_v0.0.0.cdf +0 -0
  133. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-counters-singles_20241110193700_v0.0.0.cdf +0 -0
  134. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-ialirt_20241110193700_v0.0.0.cdf +0 -0
  135. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-omni_20241110193700_v0.0.0.cdf +0 -0
  136. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-pha_20241110193700_v0.0.0.cdf +0 -0
  137. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-priorities_20241110193700_v0.0.0.cdf +0 -0
  138. imap_processing/tests/codice/data/validation/imap_codice_l1a_hi-sectored_20241110193700_v0.0.0.cdf +0 -0
  139. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-counters-aggregated_20241110193700_v0.0.0.cdf +0 -0
  140. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-counters-singles_20241110193700_v0.0.0.cdf +0 -0
  141. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-ialirt_20241110193700_v0.0.0.cdf +0 -0
  142. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-angular_20241110193700_v0.0.0.cdf +0 -0
  143. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-priority_20241110193700_v0.0.0.cdf +0 -0
  144. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-nsw-species_20241110193700_v0.0.0.cdf +0 -0
  145. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-pha_20241110193700_v0.0.0.cdf +0 -0
  146. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-angular_20241110193700_v0.0.0.cdf +0 -0
  147. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-priority_20241110193700_v0.0.0.cdf +0 -0
  148. imap_processing/tests/codice/data/validation/imap_codice_l1a_lo-sw-species_20241110193700_v0.0.0.cdf +0 -0
  149. imap_processing/tests/codice/test_codice_l1a.py +126 -53
  150. imap_processing/tests/codice/test_codice_l1b.py +6 -7
  151. imap_processing/tests/codice/test_decompress.py +4 -4
  152. imap_processing/tests/conftest.py +239 -27
  153. imap_processing/tests/ena_maps/conftest.py +51 -0
  154. imap_processing/tests/ena_maps/test_ena_maps.py +1068 -110
  155. imap_processing/tests/ena_maps/test_map_utils.py +66 -43
  156. imap_processing/tests/ena_maps/test_spatial_utils.py +17 -21
  157. imap_processing/tests/glows/conftest.py +10 -14
  158. imap_processing/tests/glows/test_glows_decom.py +4 -4
  159. imap_processing/tests/glows/test_glows_l1a_cdf.py +6 -27
  160. imap_processing/tests/glows/test_glows_l1a_data.py +6 -8
  161. imap_processing/tests/glows/test_glows_l1b.py +11 -11
  162. imap_processing/tests/glows/test_glows_l1b_data.py +5 -5
  163. imap_processing/tests/glows/test_glows_l2.py +2 -8
  164. imap_processing/tests/hi/conftest.py +1 -1
  165. imap_processing/tests/hi/data/l0/H45_diag_fee_20250208.bin +0 -0
  166. imap_processing/tests/hi/data/l0/H45_diag_fee_20250208_verify.csv +205 -0
  167. imap_processing/tests/hi/test_hi_l1b.py +22 -27
  168. imap_processing/tests/hi/test_hi_l1c.py +249 -18
  169. imap_processing/tests/hi/test_l1a.py +35 -7
  170. imap_processing/tests/hi/test_science_direct_event.py +3 -3
  171. imap_processing/tests/hi/test_utils.py +24 -2
  172. imap_processing/tests/hit/helpers/l1_validation.py +74 -73
  173. imap_processing/tests/hit/test_data/hskp_sample.ccsds +0 -0
  174. imap_processing/tests/hit/test_data/imap_hit_l0_raw_20100105_v001.pkts +0 -0
  175. imap_processing/tests/hit/test_decom_hit.py +5 -1
  176. imap_processing/tests/hit/test_hit_l1a.py +32 -36
  177. imap_processing/tests/hit/test_hit_l1b.py +300 -81
  178. imap_processing/tests/hit/test_hit_l2.py +716 -0
  179. imap_processing/tests/hit/test_hit_utils.py +184 -7
  180. imap_processing/tests/hit/validation_data/hit_l1b_standard_sample2_nsrl_v4_3decimals.csv +62 -62
  181. imap_processing/tests/hit/validation_data/hskp_sample_eu_3_6_2025.csv +89 -0
  182. imap_processing/tests/hit/validation_data/hskp_sample_raw.csv +89 -88
  183. imap_processing/tests/hit/validation_data/sci_sample_raw.csv +1 -1
  184. imap_processing/tests/ialirt/data/l0/461971383-404.bin +0 -0
  185. imap_processing/tests/ialirt/data/l0/461971384-405.bin +0 -0
  186. imap_processing/tests/ialirt/data/l0/461971385-406.bin +0 -0
  187. imap_processing/tests/ialirt/data/l0/461971386-407.bin +0 -0
  188. imap_processing/tests/ialirt/data/l0/461971387-408.bin +0 -0
  189. imap_processing/tests/ialirt/data/l0/461971388-409.bin +0 -0
  190. imap_processing/tests/ialirt/data/l0/461971389-410.bin +0 -0
  191. imap_processing/tests/ialirt/data/l0/461971390-411.bin +0 -0
  192. imap_processing/tests/ialirt/data/l0/461971391-412.bin +0 -0
  193. imap_processing/tests/ialirt/data/l0/sample_decoded_i-alirt_data.csv +383 -0
  194. imap_processing/tests/ialirt/unit/test_decom_ialirt.py +16 -81
  195. imap_processing/tests/ialirt/unit/test_grouping.py +81 -0
  196. imap_processing/tests/ialirt/unit/test_parse_mag.py +223 -0
  197. imap_processing/tests/ialirt/unit/test_process_codicehi.py +3 -3
  198. imap_processing/tests/ialirt/unit/test_process_codicelo.py +3 -10
  199. imap_processing/tests/ialirt/unit/test_process_ephemeris.py +4 -4
  200. imap_processing/tests/ialirt/unit/test_process_hit.py +3 -3
  201. imap_processing/tests/ialirt/unit/test_process_swapi.py +24 -16
  202. imap_processing/tests/ialirt/unit/test_process_swe.py +319 -6
  203. imap_processing/tests/ialirt/unit/test_time.py +16 -0
  204. imap_processing/tests/idex/conftest.py +127 -6
  205. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20231218_v001.pkts +0 -0
  206. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20241206_v001.pkts +0 -0
  207. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20250108_v001.pkts +0 -0
  208. imap_processing/tests/idex/test_data/impact_14_tof_high_data.txt +4508 -4508
  209. imap_processing/tests/idex/test_idex_l0.py +33 -11
  210. imap_processing/tests/idex/test_idex_l1a.py +92 -21
  211. imap_processing/tests/idex/test_idex_l1b.py +106 -27
  212. imap_processing/tests/idex/test_idex_l2a.py +399 -0
  213. imap_processing/tests/idex/test_idex_l2b.py +93 -0
  214. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_de_20241022_v002.cdf +0 -0
  215. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_spin_20241022_v002.cdf +0 -0
  216. imap_processing/tests/lo/test_lo_l1a.py +3 -3
  217. imap_processing/tests/lo/test_lo_l1b.py +515 -6
  218. imap_processing/tests/lo/test_lo_l1c.py +1 -1
  219. imap_processing/tests/lo/test_lo_science.py +7 -7
  220. imap_processing/tests/lo/test_star_sensor.py +1 -1
  221. imap_processing/tests/mag/conftest.py +120 -2
  222. imap_processing/tests/mag/test_mag_decom.py +5 -4
  223. imap_processing/tests/mag/test_mag_l1a.py +51 -7
  224. imap_processing/tests/mag/test_mag_l1b.py +40 -59
  225. imap_processing/tests/mag/test_mag_l1c.py +354 -19
  226. imap_processing/tests/mag/test_mag_l2.py +130 -0
  227. imap_processing/tests/mag/test_mag_validation.py +247 -26
  228. imap_processing/tests/mag/validation/L1b/T009/MAGScience-normal-(2,2)-8s-20250204-16h39.csv +17 -0
  229. imap_processing/tests/mag/validation/L1b/T009/mag-l1a-l1b-t009-magi-out.csv +16 -16
  230. imap_processing/tests/mag/validation/L1b/T009/mag-l1a-l1b-t009-mago-out.csv +16 -16
  231. imap_processing/tests/mag/validation/L1b/T010/MAGScience-normal-(2,2)-8s-20250206-12h05.csv +17 -0
  232. imap_processing/tests/mag/validation/L1b/T011/MAGScience-normal-(2,2)-8s-20250204-16h08.csv +17 -0
  233. imap_processing/tests/mag/validation/L1b/T011/mag-l1a-l1b-t011-magi-out.csv +16 -16
  234. imap_processing/tests/mag/validation/L1b/T011/mag-l1a-l1b-t011-mago-out.csv +16 -16
  235. imap_processing/tests/mag/validation/L1b/T012/MAGScience-normal-(2,2)-8s-20250204-16h08.csv +17 -0
  236. imap_processing/tests/mag/validation/L1b/T012/data.bin +0 -0
  237. imap_processing/tests/mag/validation/L1b/T012/field_like_all_ranges.txt +19200 -0
  238. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-cal.cdf +0 -0
  239. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-in.csv +17 -0
  240. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-magi-out.csv +17 -0
  241. imap_processing/tests/mag/validation/L1b/T012/mag-l1a-l1b-t012-mago-out.csv +17 -0
  242. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-magi-normal-in.csv +1217 -0
  243. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-magi-normal-out.csv +1857 -0
  244. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-mago-normal-in.csv +1217 -0
  245. imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-mago-normal-out.csv +1857 -0
  246. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-magi-normal-in.csv +1217 -0
  247. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-magi-normal-out.csv +1793 -0
  248. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-mago-normal-in.csv +1217 -0
  249. imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-mago-normal-out.csv +1793 -0
  250. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-burst-in.csv +2561 -0
  251. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-normal-in.csv +961 -0
  252. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-normal-out.csv +1539 -0
  253. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-mago-normal-in.csv +1921 -0
  254. imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-mago-normal-out.csv +2499 -0
  255. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-magi-normal-in.csv +865 -0
  256. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-magi-normal-out.csv +1196 -0
  257. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-mago-normal-in.csv +1729 -0
  258. imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-mago-normal-out.csv +3053 -0
  259. imap_processing/tests/mag/validation/L2/imap_mag_l1b_norm-mago_20251017_v002.cdf +0 -0
  260. imap_processing/tests/mag/validation/calibration/imap_mag_l1b-calibration_20240229_v001.cdf +0 -0
  261. imap_processing/tests/mag/validation/calibration/imap_mag_l2-calibration-matrices_20251017_v004.cdf +0 -0
  262. imap_processing/tests/mag/validation/calibration/imap_mag_l2-offsets-norm_20251017_20251017_v001.cdf +0 -0
  263. imap_processing/tests/spacecraft/data/SSR_2024_190_20_08_12_0483851794_2_DA_apid0594_1packet.pkts +0 -0
  264. imap_processing/tests/spacecraft/test_quaternions.py +71 -0
  265. imap_processing/tests/spice/test_data/fake_repoint_data.csv +5 -0
  266. imap_processing/tests/spice/test_data/fake_spin_data.csv +11 -11
  267. imap_processing/tests/spice/test_geometry.py +9 -12
  268. imap_processing/tests/spice/test_kernels.py +1 -200
  269. imap_processing/tests/spice/test_pointing_frame.py +185 -0
  270. imap_processing/tests/spice/test_repoint.py +121 -0
  271. imap_processing/tests/spice/test_spin.py +50 -9
  272. imap_processing/tests/spice/test_time.py +14 -0
  273. imap_processing/tests/swapi/lut/imap_swapi_esa-unit-conversion_20250211_v000.csv +73 -0
  274. imap_processing/tests/swapi/lut/imap_swapi_lut-notes_20250211_v000.csv +1025 -0
  275. imap_processing/tests/swapi/test_swapi_l1.py +13 -11
  276. imap_processing/tests/swapi/test_swapi_l2.py +180 -8
  277. imap_processing/tests/swe/l0_data/2024051010_SWE_HK_packet.bin +0 -0
  278. imap_processing/tests/swe/l0_data/2024051011_SWE_CEM_RAW_packet.bin +0 -0
  279. imap_processing/tests/swe/l0_validation_data/idle_export_eu.SWE_APP_HK_20240510_092742.csv +49 -0
  280. imap_processing/tests/swe/l0_validation_data/idle_export_eu.SWE_CEM_RAW_20240510_092742.csv +593 -0
  281. imap_processing/tests/swe/lut/checker-board-indices.csv +24 -0
  282. imap_processing/tests/swe/lut/imap_swe_esa-lut_20250301_v000.csv +385 -0
  283. imap_processing/tests/swe/lut/imap_swe_l1b-in-flight-cal_20240510_20260716_v000.csv +3 -0
  284. imap_processing/tests/swe/test_swe_l1a.py +20 -2
  285. imap_processing/tests/swe/test_swe_l1a_cem_raw.py +52 -0
  286. imap_processing/tests/swe/test_swe_l1a_hk.py +68 -0
  287. imap_processing/tests/swe/test_swe_l1a_science.py +3 -3
  288. imap_processing/tests/swe/test_swe_l1b.py +162 -24
  289. imap_processing/tests/swe/test_swe_l2.py +153 -91
  290. imap_processing/tests/test_cli.py +171 -88
  291. imap_processing/tests/test_utils.py +140 -17
  292. imap_processing/tests/ultra/data/l0/FM45_UltraFM45_Functional_2024-01-22T0105_20240122T010548.CCSDS +0 -0
  293. imap_processing/tests/ultra/data/l0/ultra45_raw_sc_ultraimgrates_20220530_00.csv +164 -0
  294. 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
  295. imap_processing/tests/ultra/data/mock_data.py +369 -0
  296. imap_processing/tests/ultra/unit/conftest.py +115 -89
  297. imap_processing/tests/ultra/unit/test_badtimes.py +4 -4
  298. imap_processing/tests/ultra/unit/test_cullingmask.py +8 -6
  299. imap_processing/tests/ultra/unit/test_de.py +14 -13
  300. imap_processing/tests/ultra/unit/test_decom_apid_880.py +27 -76
  301. imap_processing/tests/ultra/unit/test_decom_apid_881.py +54 -11
  302. imap_processing/tests/ultra/unit/test_decom_apid_883.py +12 -10
  303. imap_processing/tests/ultra/unit/test_decom_apid_896.py +202 -55
  304. imap_processing/tests/ultra/unit/test_lookup_utils.py +23 -1
  305. imap_processing/tests/ultra/unit/test_spacecraft_pset.py +77 -0
  306. imap_processing/tests/ultra/unit/test_ultra_l1a.py +98 -305
  307. imap_processing/tests/ultra/unit/test_ultra_l1b.py +60 -14
  308. imap_processing/tests/ultra/unit/test_ultra_l1b_annotated.py +2 -2
  309. imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py +26 -27
  310. imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +239 -70
  311. imap_processing/tests/ultra/unit/test_ultra_l1c.py +5 -5
  312. imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +114 -83
  313. imap_processing/tests/ultra/unit/test_ultra_l2.py +230 -0
  314. imap_processing/ultra/constants.py +1 -1
  315. imap_processing/ultra/l0/decom_tools.py +27 -39
  316. imap_processing/ultra/l0/decom_ultra.py +168 -204
  317. imap_processing/ultra/l0/ultra_utils.py +152 -136
  318. imap_processing/ultra/l1a/ultra_l1a.py +55 -271
  319. imap_processing/ultra/l1b/badtimes.py +1 -4
  320. imap_processing/ultra/l1b/cullingmask.py +2 -6
  321. imap_processing/ultra/l1b/de.py +116 -57
  322. imap_processing/ultra/l1b/extendedspin.py +20 -18
  323. imap_processing/ultra/l1b/lookup_utils.py +72 -9
  324. imap_processing/ultra/l1b/ultra_l1b.py +36 -16
  325. imap_processing/ultra/l1b/ultra_l1b_culling.py +66 -30
  326. imap_processing/ultra/l1b/ultra_l1b_extended.py +297 -94
  327. imap_processing/ultra/l1c/histogram.py +2 -6
  328. imap_processing/ultra/l1c/spacecraft_pset.py +84 -0
  329. imap_processing/ultra/l1c/ultra_l1c.py +8 -9
  330. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +206 -108
  331. imap_processing/ultra/l2/ultra_l2.py +299 -0
  332. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +526 -0
  333. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +526 -0
  334. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +526 -0
  335. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +526 -0
  336. imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +2 -2
  337. imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +2 -0
  338. imap_processing/ultra/packet_definitions/README.md +38 -0
  339. imap_processing/ultra/packet_definitions/ULTRA_SCI_COMBINED.xml +15302 -482
  340. imap_processing/ultra/utils/ultra_l1_utils.py +31 -12
  341. imap_processing/utils.py +69 -29
  342. {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/METADATA +10 -6
  343. imap_processing-0.13.0.dist-info/RECORD +578 -0
  344. imap_processing/cdf/config/imap_mag_l1_variable_attrs.yaml +0 -237
  345. imap_processing/hi/l1a/housekeeping.py +0 -27
  346. imap_processing/hi/l1b/hi_eng_unit_convert_table.csv +0 -154
  347. imap_processing/swe/l1b/swe_esa_lookup_table.csv +0 -1441
  348. imap_processing/swe/l1b/swe_l1b_science.py +0 -652
  349. imap_processing/tests/codice/data/imap_codice_l1a_hi-counters-aggregated_20240429_v001.cdf +0 -0
  350. imap_processing/tests/codice/data/imap_codice_l1a_hi-counters-singles_20240429_v001.cdf +0 -0
  351. imap_processing/tests/codice/data/imap_codice_l1a_hi-omni_20240429_v001.cdf +0 -0
  352. imap_processing/tests/codice/data/imap_codice_l1a_hi-sectored_20240429_v001.cdf +0 -0
  353. imap_processing/tests/codice/data/imap_codice_l1a_hskp_20100101_v001.cdf +0 -0
  354. imap_processing/tests/codice/data/imap_codice_l1a_lo-counters-aggregated_20240429_v001.cdf +0 -0
  355. imap_processing/tests/codice/data/imap_codice_l1a_lo-counters-singles_20240429_v001.cdf +0 -0
  356. imap_processing/tests/codice/data/imap_codice_l1a_lo-nsw-angular_20240429_v001.cdf +0 -0
  357. imap_processing/tests/codice/data/imap_codice_l1a_lo-nsw-priority_20240429_v001.cdf +0 -0
  358. imap_processing/tests/codice/data/imap_codice_l1a_lo-nsw-species_20240429_v001.cdf +0 -0
  359. imap_processing/tests/codice/data/imap_codice_l1a_lo-sw-angular_20240429_v001.cdf +0 -0
  360. imap_processing/tests/codice/data/imap_codice_l1a_lo-sw-priority_20240429_v001.cdf +0 -0
  361. imap_processing/tests/codice/data/imap_codice_l1a_lo-sw-species_20240429_v001.cdf +0 -0
  362. imap_processing/tests/codice/data/imap_codice_l1b_hi-counters-aggregated_20240429_v001.cdf +0 -0
  363. imap_processing/tests/codice/data/imap_codice_l1b_hi-counters-singles_20240429_v001.cdf +0 -0
  364. imap_processing/tests/codice/data/imap_codice_l1b_hi-omni_20240429_v001.cdf +0 -0
  365. imap_processing/tests/codice/data/imap_codice_l1b_hi-sectored_20240429_v001.cdf +0 -0
  366. imap_processing/tests/codice/data/imap_codice_l1b_hskp_20100101_v001.cdf +0 -0
  367. imap_processing/tests/codice/data/imap_codice_l1b_lo-counters-aggregated_20240429_v001.cdf +0 -0
  368. imap_processing/tests/codice/data/imap_codice_l1b_lo-counters-singles_20240429_v001.cdf +0 -0
  369. imap_processing/tests/codice/data/imap_codice_l1b_lo-nsw-angular_20240429_v001.cdf +0 -0
  370. imap_processing/tests/codice/data/imap_codice_l1b_lo-nsw-priority_20240429_v001.cdf +0 -0
  371. imap_processing/tests/codice/data/imap_codice_l1b_lo-nsw-species_20240429_v001.cdf +0 -0
  372. imap_processing/tests/codice/data/imap_codice_l1b_lo-sw-angular_20240429_v001.cdf +0 -0
  373. imap_processing/tests/codice/data/imap_codice_l1b_lo-sw-priority_20240429_v001.cdf +0 -0
  374. imap_processing/tests/codice/data/imap_codice_l1b_lo-sw-species_20240429_v001.cdf +0 -0
  375. imap_processing/tests/hi/data/l1/imap_hi_l1b_45sensor-de_20250415_v999.cdf +0 -0
  376. imap_processing/tests/hit/PREFLIGHT_raw_record_2023_256_15_59_04_apid1251.pkts +0 -0
  377. imap_processing/tests/hit/PREFLIGHT_raw_record_2023_256_15_59_04_apid1252.pkts +0 -0
  378. imap_processing/tests/hit/validation_data/hskp_sample_eu.csv +0 -89
  379. imap_processing/tests/hit/validation_data/sci_sample_raw1.csv +0 -29
  380. imap_processing/tests/idex/test_data/imap_idex_l0_raw_20231214_v001.pkts +0 -0
  381. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_de_20100101_v001.cdf +0 -0
  382. imap_processing/tests/lo/test_cdfs/imap_lo_l1a_spin_20100101_v001.cdf +0 -0
  383. imap_processing/tests/swe/test_swe_l1b_science.py +0 -84
  384. imap_processing/tests/ultra/test_data/mock_data.py +0 -161
  385. imap_processing/ultra/l1c/pset.py +0 -40
  386. imap_processing/ultra/lookup_tables/dps_sensitivity45.cdf +0 -0
  387. imap_processing-0.11.0.dist-info/RECORD +0 -488
  388. /imap_processing/idex/packet_definitions/{idex_packet_definition.xml → idex_science_packet_definition.xml} +0 -0
  389. /imap_processing/tests/ialirt/{test_data → data}/l0/20240827095047_SWE_IALIRT_packet.bin +0 -0
  390. /imap_processing/tests/ialirt/{test_data → data}/l0/BinLog CCSDS_FRAG_TLM_20240826_152323Z_IALIRT_data_for_SDC.bin +0 -0
  391. /imap_processing/tests/ialirt/{test_data → data}/l0/IALiRT Raw Packet Telemetry.txt +0 -0
  392. /imap_processing/tests/ialirt/{test_data → data}/l0/apid01152.tlm +0 -0
  393. /imap_processing/tests/ialirt/{test_data → data}/l0/eu_SWP_IAL_20240826_152033.csv +0 -0
  394. /imap_processing/tests/ialirt/{test_data → data}/l0/hi_fsw_view_1_ccsds.bin +0 -0
  395. /imap_processing/tests/ialirt/{test_data → data}/l0/hit_ialirt_sample.ccsds +0 -0
  396. /imap_processing/tests/ialirt/{test_data → data}/l0/hit_ialirt_sample.csv +0 -0
  397. /imap_processing/tests/ialirt/{test_data → data}/l0/idle_export_eu.SWE_IALIRT_20240827_093852.csv +0 -0
  398. /imap_processing/tests/ialirt/{test_data → data}/l0/imap_codice_l1a_hi-ialirt_20240523200000_v0.0.0.cdf +0 -0
  399. /imap_processing/tests/ialirt/{test_data → data}/l0/imap_codice_l1a_lo-ialirt_20241110193700_v0.0.0.cdf +0 -0
  400. /imap_processing/{mag/l1b → tests/spacecraft}/__init__.py +0 -0
  401. /imap_processing/{swe/l1b/engineering_unit_convert_table.csv → tests/swe/lut/imap_swe_eu-conversion_20240510_v000.csv} +0 -0
  402. /imap_processing/tests/ultra/{test_data → data}/l0/FM45_40P_Phi28p5_BeamCal_LinearScan_phi28.50_theta-0.00_20240207T102740.CCSDS +0 -0
  403. /imap_processing/tests/ultra/{test_data → data}/l0/FM45_7P_Phi0.0_BeamCal_LinearScan_phi0.04_theta-0.01_20230821T121304.CCSDS +0 -0
  404. /imap_processing/tests/ultra/{test_data → data}/l0/FM45_TV_Cycle6_Hot_Ops_Front212_20240124T063837.CCSDS +0 -0
  405. /imap_processing/tests/ultra/{test_data → data}/l0/Ultra45_EM_SwRI_Cal_Run7_ThetaScan_20220530T225054.CCSDS +0 -0
  406. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_auxdata_Ultra45_EM_SwRI_Cal_Run7_ThetaScan_20220530T225054.csv +0 -0
  407. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_enaphxtofhangimg_FM45_TV_Cycle6_Hot_Ops_Front212_20240124T063837.csv +0 -0
  408. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_ultraimgrates_Ultra45_EM_SwRI_Cal_Run7_ThetaScan_20220530T225054.csv +0 -0
  409. /imap_processing/tests/ultra/{test_data → data}/l0/ultra45_raw_sc_ultrarawimgevent_FM45_7P_Phi00_BeamCal_LinearScan_phi004_theta-001_20230821T121304.csv +0 -0
  410. /imap_processing/tests/ultra/{test_data → data}/l1/dps_exposure_helio_45_E1.cdf +0 -0
  411. /imap_processing/tests/ultra/{test_data → data}/l1/dps_exposure_helio_45_E12.cdf +0 -0
  412. /imap_processing/tests/ultra/{test_data → data}/l1/dps_exposure_helio_45_E24.cdf +0 -0
  413. {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/LICENSE +0 -0
  414. {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/WHEEL +0 -0
  415. {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,823 @@
1
+ """
2
+ Perform IDEX L2a Processing.
3
+
4
+ Examples
5
+ --------
6
+ .. code-block:: python
7
+
8
+ from imap_processing.idex.idex_l1a import PacketParser
9
+ from imap_processing.idex.idex_l1b import idex_l1b
10
+ from imap_processing.idex.idex_l2a import idex_l2a
11
+
12
+ l0_file = "imap_processing/tests/idex/imap_idex_l0_sci_20231214_v001.pkts"
13
+ l1a_data = PacketParser(l0_file)
14
+ l1b_data = idex_l1b(l1a_data)
15
+ l2a_data = idex_l2a(l1b_data)
16
+ write_cdf(l2a_data)
17
+ """
18
+
19
+ import logging
20
+ from enum import IntEnum
21
+
22
+ import numpy as np
23
+ import pandas as pd
24
+ import xarray as xr
25
+ from numpy.typing import NDArray
26
+ from scipy.integrate import quad
27
+ from scipy.optimize import curve_fit
28
+ from scipy.signal import butter, detrend, filtfilt, find_peaks
29
+ from scipy.stats import exponnorm
30
+
31
+ from imap_processing import imap_module_directory
32
+ from imap_processing.idex import idex_constants
33
+ from imap_processing.idex.idex_l1a import get_idex_attrs
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class BaselineNoiseTime(IntEnum):
39
+ """
40
+ Time range in microseconds that mark the baseline noise before a Dust impact.
41
+
42
+ Attributes
43
+ ----------
44
+ START: int
45
+ Beginning of the baseline noise window.
46
+ STOP: int
47
+ End of the baseline noise window.
48
+ """
49
+
50
+ START = -7
51
+ STOP = -5
52
+
53
+
54
+ def idex_l2a(l1b_dataset: xr.Dataset) -> xr.Dataset:
55
+ """
56
+ Will process IDEX l1b data to create l2a data products.
57
+
58
+ This will use fits to estimate the total impact charge for the Ion Grid and two
59
+ target signals.
60
+
61
+ Calculate mass scales for each event using the TOF high arrays (best quality of the
62
+ 3 gain stages).
63
+ The TOF peaks are fitted to EMG curves to determine total intensity, max amplitude,
64
+ and signal quality.
65
+
66
+ Parameters
67
+ ----------
68
+ l1b_dataset : xarray.Dataset
69
+ IDEX L1a dataset to process.
70
+
71
+ Returns
72
+ -------
73
+ l1b_dataset : xarray.Dataset
74
+ The``xarray`` dataset containing the science data and supporting metadata.
75
+ """
76
+ logger.info(
77
+ f"Running IDEX L2A processing on dataset: {l1b_dataset.attrs['Logical_source']}"
78
+ )
79
+
80
+ tof_high = l1b_dataset["TOF_High"]
81
+ hs_time = l1b_dataset["time_high_sample_rate"]
82
+ ls_time = l1b_dataset["time_low_sample_rate"]
83
+
84
+ # Load an array of known masses of ions
85
+ atomic_masses_path = f"{imap_module_directory}/idex/atomic_masses.csv"
86
+ atomic_masses = pd.read_csv(atomic_masses_path)
87
+ masses = atomic_masses["Mass"]
88
+ stretches, shifts, mass_scales = time_to_mass(tof_high.data, hs_time.data, masses)
89
+
90
+ mass_scales_da = xr.DataArray(
91
+ name="mass_scale",
92
+ data=mass_scales,
93
+ dims=("epoch", "time_high_sample_rate_index"),
94
+ )
95
+ snr = calculate_snr(tof_high, hs_time)
96
+ # Find peaks for each event. The peaks represent a TOF of an ion.
97
+ # Peaks_2d is a list of variable-length arrays
98
+ peaks_2d = [find_peaks(tof, prominence=0.01)[0] for tof in tof_high]
99
+ kappa = calculate_kappa(mass_scales, peaks_2d)
100
+
101
+ # Analyze peaks for estimating dust composition
102
+ peak_fits_params, area_under_fits, fit_chisqr, fit_redchi = xr.apply_ufunc(
103
+ analyze_peaks,
104
+ tof_high,
105
+ hs_time,
106
+ mass_scales_da,
107
+ np.arange(len(peaks_2d)),
108
+ kwargs={"peaks_2d": peaks_2d},
109
+ input_core_dims=[
110
+ ["time_high_sample_rate_index"],
111
+ ["time_high_sample_rate_index"],
112
+ ["time_high_sample_rate_index"],
113
+ [],
114
+ ],
115
+ output_core_dims=[
116
+ ["mass", "peak_fit_parameters"],
117
+ ["mass"],
118
+ [],
119
+ [],
120
+ ],
121
+ vectorize=True,
122
+ )
123
+
124
+ l2a_dataset = l1b_dataset.copy()
125
+
126
+ for waveform in ["Target_Low", "Target_High", "Ion_Grid"]:
127
+ # Get the dust mass estimates and fit results
128
+ fit_results = xr.apply_ufunc(
129
+ estimate_dust_mass,
130
+ ls_time,
131
+ l1b_dataset[waveform],
132
+ input_core_dims=[
133
+ ["time_low_sample_rate_index"],
134
+ ["time_low_sample_rate_index"],
135
+ ],
136
+ output_core_dims=[
137
+ ["fit_parameters"],
138
+ [],
139
+ [],
140
+ [],
141
+ ["time_low_sample_rate_index"],
142
+ ],
143
+ vectorize=True,
144
+ output_dtypes=[np.float64] * 6,
145
+ )
146
+ waveform_name = waveform.lower()
147
+ # Add variables
148
+ l2a_dataset[f"{waveform_name}_fit_parameters"] = fit_results[0]
149
+ l2a_dataset[f"{waveform_name}_fit_impact_charge"] = fit_results[1]
150
+ # TODO: convert charge to mass
151
+ l2a_dataset[f"{waveform_name}_fit_impact_mass_estimate"] = fit_results[1]
152
+ l2a_dataset[f"{waveform_name}_chi_squared"] = fit_results[2]
153
+ l2a_dataset[f"{waveform_name}_reduced_chi_squared"] = fit_results[3]
154
+ l2a_dataset[f"{waveform_name}_fit_results"] = fit_results[4]
155
+
156
+ l2a_dataset["tof_peak_fit_parameters"] = peak_fits_params
157
+ l2a_dataset["tof_peak_area_under_fit"] = area_under_fits
158
+ l2a_dataset["tof_peak_chi_square"] = fit_chisqr
159
+ l2a_dataset["tof_peak_reduced_chi_square"] = fit_redchi
160
+
161
+ l2a_dataset["tof_peak_kappa"] = xr.DataArray(kappa, dims=["epoch"])
162
+ l2a_dataset["tof_snr"] = xr.DataArray(snr, dims=["epoch"])
163
+ l2a_dataset["mass"] = mass_scales_da
164
+ # Update global attributes
165
+ idex_attrs = get_idex_attrs()
166
+ l2a_dataset.attrs = idex_attrs.get_global_attributes("imap_idex_l2a_sci")
167
+
168
+ logger.info("IDEX L2A science data processing completed.")
169
+ return l2a_dataset
170
+
171
+
172
+ def time_to_mass(
173
+ tof_high: np.ndarray, high_sampling_time: np.ndarray, masses: np.ndarray
174
+ ) -> tuple[NDArray, NDArray, NDArray]:
175
+ """
176
+ Calculate a mass scale for each TOF array in 'TOF_high'.
177
+
178
+ 1) Make a vector with all zeros and a length of 8189, same as the TOF length: t_i
179
+ 2) Calculate the times when each input mass should appear in the TOF data: t_calc
180
+ for each mass, calculate a time using this formula:
181
+
182
+ t_calc = t_offset + stretch_factor*sqrt(mass)
183
+
184
+ t_offset is the time offset (ns)
185
+ stretch factor (ns)
186
+
187
+ Then and set the value at the index of t_i that is closest to each of the
188
+ t_calcs to 1, the rest stay zero.
189
+ 3) Calculate the cross-correlation with the original TOF.
190
+ The max will give you the best lag (t_offset) for a given stretch_factor.
191
+ 4) Choose the stretch_factor that has the highest correlation
192
+
193
+ Parameters
194
+ ----------
195
+ tof_high : numpy.ndarray
196
+ The time of flight array for one dust event. Shape is
197
+ (epoch, high_time_sample_rate).
198
+ high_sampling_time : numpy.ndarray
199
+ The high sampling time array for one dust event. Shape is
200
+ (epoch, high_time_sample_rate).
201
+ masses : np.ndarray
202
+ Array of known masses of ions. Shape is (21,).
203
+
204
+ Returns
205
+ -------
206
+ numpy.ndarray
207
+ Best stretch value per event(adjusts scale).
208
+ numpy.ndarray
209
+ Best shift value per event (shifts scale left or right).
210
+ numpy.ndarray
211
+ Estimated mass for each time per event (after the time has been aligned using
212
+ the best t_offset and stretch_factor).
213
+ """
214
+ # Create an array of random stretches
215
+ # eventually, the stretch_factor used to create the highest correlation is used to
216
+ # align the time
217
+ min_stretch = 1400
218
+ random_stretches = np.linspace(min_stretch, min_stretch + 100, 10)
219
+
220
+ # Normalize time so start time is zero.
221
+ # This is necessary to find the correct time offset
222
+ time = high_sampling_time - high_sampling_time[:, 0:1]
223
+
224
+ # Start with a time offset of 0
225
+ t_offset = 0
226
+ shift = np.zeros((len(random_stretches), len(tof_high)))
227
+ correlation = np.zeros_like(shift)
228
+ # Step 1
229
+ t_i = np.zeros((len(random_stretches), len(tof_high[0])))
230
+ # Step 2
231
+ t_calc = t_offset + random_stretches[:, np.newaxis] * np.sqrt(np.array(masses))
232
+ for i in range(len(random_stretches)):
233
+ # Round every calculated time to the nearest int
234
+ t_calc_int = np.round(t_calc[i]).astype(int)
235
+ # Set values of t_i to 1 at the rounded calculated times if the time is less
236
+ # than the length of t_i
237
+ # E.g., if t_calc_int[0] = 5 then t_i[5] = 1.
238
+ t_i[i, t_calc_int[t_calc_int < len(t_i[0])]] = 1
239
+ # Step 3
240
+ # Cross-correlate t_i with TOF
241
+ # T_i simulates peaks at the times expected from the formula above,
242
+ # when this is cross correlated with the actual time of flight array with
243
+ # The measured peaks, we can measure the lags between them.
244
+ for j in range(len(tof_high)):
245
+ cross_correlation = np.correlate(t_i[i], tof_high[j], mode="full")
246
+ if np.all(cross_correlation == 0):
247
+ logger.warning(
248
+ "There are no correlations found between the TOF array "
249
+ "and the expected mass times array. The resulting mass scale "
250
+ "may be inaccurate."
251
+ )
252
+ # Find the lag corresponding to the maximum correlation
253
+ # Represents the time lag from where the arrays are most correlated
254
+
255
+ # When np.correlate mode is 'full', it returns the convolution at each
256
+ # point of overlap, with an output shape of (N+M-1,) where N and M are the
257
+ # lengths of the input arrays. The center point or zero lag is at index
258
+ # len(M) - 1. Positions before this are negative lags, and
259
+ # positions after are positive lags.
260
+ middle = len(t_i[0]) - 1
261
+ shift[i, j] = np.argmax(cross_correlation) - middle
262
+ correlation[i, j] = np.max(cross_correlation)
263
+
264
+ # Calculate the estimated mass for each time (after the time has been aligned using
265
+ # the best t_offset and stretch_factor and converted to seconds).
266
+ # Step 4
267
+ # Gets the best shift in seconds (shift is currently in number of samples)
268
+ best_shift = (
269
+ idex_constants.FM_SAMPLING_RATE
270
+ * shift[np.argmax(correlation, axis=0), np.arange(len(shift[0]))]
271
+ )
272
+ # Get the best stretch in seconds
273
+ best_stretch = (
274
+ idex_constants.NS_TO_S * random_stretches[np.argmax(correlation, axis=0)]
275
+ )
276
+
277
+ mass_scale = (
278
+ (time * idex_constants.US_TO_S - best_shift[:, np.newaxis])
279
+ / best_stretch[:, np.newaxis]
280
+ ) ** 2
281
+
282
+ return best_stretch, best_shift, mass_scale
283
+
284
+
285
+ def calculate_kappa(mass_scales: np.ndarray, peaks_2d: list) -> NDArray:
286
+ """
287
+ Calculate the kappa value for each mass scale.
288
+
289
+ Kappa represents the difference between the observed mass peaks and their
290
+ expected integer values in the calculated mass scale. The value ranges between zero
291
+ and one. A kappa value closer to zero indicates a better accuracy of the mass scale.
292
+
293
+ Parameters
294
+ ----------
295
+ mass_scales : xarray.DataArray
296
+ Array containing the masses at each time value for each dust event.
297
+ peaks_2d : list
298
+ A Nested list of tof peak indices.
299
+
300
+ Returns
301
+ -------
302
+ numpy.ndarray
303
+ Average distance from the assigned peak to the nearest integer value.
304
+ """
305
+ # Find the average deviation between each TOF peak's assigned mass value and its
306
+ # nearest decimal value per spectrum.
307
+ kappas = np.asarray(
308
+ [
309
+ np.mean(mass_scale[peaks] - np.round(mass_scale[peaks]))
310
+ for mass_scale, peaks in zip(mass_scales, peaks_2d)
311
+ ]
312
+ )
313
+ return kappas
314
+
315
+
316
+ def calculate_snr(tof_high: xr.DataArray, hs_time: xr.DataArray) -> NDArray:
317
+ """
318
+ Calculate the signal-to-noise ratio.
319
+
320
+ Parameters
321
+ ----------
322
+ tof_high : xarray.DataArray
323
+ The time of flight array.
324
+ hs_time : xarray.DataArray
325
+ The high sampling time array.
326
+
327
+ Returns
328
+ -------
329
+ numpy.ndarray
330
+ Signal-to-noise ratio at each event.
331
+ """
332
+ # Find indices where Time (High Sampling) is between -7 and -5 ns (no signal yet)
333
+ # To determine the baseline noise
334
+ baseline_noise = np.where(
335
+ np.logical_and(
336
+ hs_time >= BaselineNoiseTime.START, hs_time <= BaselineNoiseTime.STOP
337
+ ),
338
+ tof_high,
339
+ np.nan,
340
+ )
341
+ if np.all(np.isnan(baseline_noise)):
342
+ logger.warning(
343
+ "Unable to find baseline noise. "
344
+ f"There is no signal from {BaselineNoiseTime.START} to "
345
+ f"{BaselineNoiseTime.STOP} ns. Returning np.nan SNR values"
346
+ )
347
+ return np.full(len(hs_time), fill_value=np.nan)
348
+ # Get the max signal without baseline noise
349
+ tof_max = np.max(tof_high, axis=1) - np.nanmean(baseline_noise, axis=1)
350
+ tof_sigma = np.nanstd(baseline_noise, axis=1, ddof=1)
351
+ # Return snr ratio
352
+ return tof_max / tof_sigma
353
+
354
+
355
+ def analyze_peaks(
356
+ tof_high: xr.DataArray,
357
+ high_sampling_time: xr.DataArray,
358
+ mass_scale: xr.DataArray,
359
+ event_num: int,
360
+ peaks_2d: np.ndarray,
361
+ ) -> tuple[NDArray, NDArray, float, float]:
362
+ """
363
+ Fit an EMG curve to the Time of Flight data around each peak.
364
+
365
+ Parameters
366
+ ----------
367
+ tof_high : xarray.DataArray
368
+ The time of flight array.
369
+ high_sampling_time : xarray.DataArray
370
+ The high sampling time array.
371
+ mass_scale : xarray.DataArray
372
+ Time to mass scale.
373
+ event_num : int
374
+ Dust event number (for debugging purposes).
375
+ peaks_2d : numpy.ndarray
376
+ Nested list of peak indices.
377
+
378
+ Returns
379
+ -------
380
+ params: numpy.ndarray
381
+ Array of the EMG fit parameters (mu, sigma, lambda) at the corresponding mass.
382
+ Empty mass slots contain zeros.
383
+
384
+ area_under_emg : numpy.ndarray
385
+ Array of the area under the EMG curve at that mass. Empty mass slots
386
+ contain zeros.
387
+ """
388
+ # Initialize arrays to store EMG fit results
389
+ # fit_params: (500, 3) array where the first dimension is the estimated ion mass (
390
+ # 0-499)
391
+ # and the second is EMG fit parameters (mu, sigma, lambda) for peaks at that mass
392
+ # area_under_emg: (500) array storing the area under each EMG peak at
393
+ # corresponding mass.
394
+ fit_params = np.zeros((500, 3))
395
+ area_under_emg = np.zeros(500)
396
+ for peak in peaks_2d[event_num]:
397
+ # Take a slice of 5 samples on either side of the peak
398
+ start = max(0, peak - 5)
399
+ end = min(len(tof_high), peak + 6)
400
+
401
+ time_slice = high_sampling_time[start:end]
402
+ tof_slice = tof_high[start:end]
403
+
404
+ param, chisqr, redchi = fit_emg(time_slice, tof_slice, event_num)
405
+ if np.all(np.isnan(param)):
406
+ continue
407
+
408
+ area = calculate_area_under_emg(time_slice, param)
409
+ # extract the variables
410
+ k, mu, sigma = param
411
+ # Calculate lambda
412
+ lam = 1 / (k * sigma)
413
+ # Find the index where time is closest to mu
414
+ time_idx = np.argmin(np.abs(high_sampling_time.data - mu))
415
+ mass = mass_scale[time_idx]
416
+ # Round calculated mass to get the index
417
+ # If that index is already taken, keep increasing the index by one
418
+ # until we find an empty slot.
419
+ # This ensures we don't overwrite existing data when we have multiple peaks
420
+ # close to the same mass number
421
+ if mass < 0:
422
+ logger.warning(f"Warning: Calculated a negative mass: {mass}.")
423
+
424
+ mass = max(0, round(mass))
425
+ # Find the first index with non-zero fit parameters, starting from current mass
426
+ non_zero_idxs = np.nonzero(np.all(fit_params[mass:] != 0, axis=-1))[0]
427
+ # Determine index to use
428
+ # If no non-zero parameters found, use current mass index
429
+ # Otherwise, use the current mass plus offset to first non-zero index
430
+ idx = mass if not non_zero_idxs.size else mass + non_zero_idxs[0]
431
+
432
+ if idx < 500:
433
+ fit_params[idx] = np.array([mu, sigma, lam])
434
+ area_under_emg[idx] = area
435
+ else:
436
+ logger.warning(f"Unable to find a slot for mass: {mass}. Discarding value.")
437
+
438
+ return fit_params, area_under_emg, chisqr, redchi
439
+
440
+
441
+ def fit_emg(
442
+ peak_time: np.ndarray, peak_signal: np.ndarray, event_num: int
443
+ ) -> tuple[NDArray, float, float]:
444
+ """
445
+ Fit an exponentially modified gaussian function to the peak signal.
446
+
447
+ Scipy.stats.exponnorm.pdf uses parameters shape (k),
448
+ location (mu), and scale (sigma) where k = 1/(sigma*lambda)
449
+ with lambda being the exponential decay rate.
450
+
451
+ Parameters
452
+ ----------
453
+ peak_time : numpy.ndarray
454
+ TOF high +5 and -5 samples around peak.
455
+ peak_signal : numpy.ndarray
456
+ High sampling time array at +5 and -5 samples around peak.
457
+ event_num : int
458
+ Dust event number (for debugging purposes).
459
+
460
+ Returns
461
+ -------
462
+ param : numpy.ndarray
463
+ Fitted EMG optimal values for the parameters (popt) [k (shape parameter), mu,
464
+ sigma] if fit is successful, array of np.nans otherwise.
465
+ chisqr : float
466
+ Chi-square value if fit is successful, np.nan otherwise.
467
+ redchi : float
468
+ Reduced chi-square value if fit is successful, np.nan otherwise.
469
+ """
470
+ # Initial Guess for the parameters of the emg fit:
471
+ # center of gaussian
472
+ mu = peak_time[np.argmax(peak_signal)]
473
+ sigma = np.std(peak_time) / 10
474
+ # Decay rate
475
+ lam = 1 / (peak_time[-1] - peak_time[0])
476
+ # Calculate shape parameter K from lambda and sigma
477
+ k = 1 / (lam * sigma)
478
+ p0 = [k, mu, sigma]
479
+
480
+ try:
481
+ param, _ = curve_fit(
482
+ exponnorm.pdf, peak_time, peak_signal, p0=p0, maxfev=100_000
483
+ )
484
+
485
+ except RuntimeError as e:
486
+ logger.warning(
487
+ f"Failed to fit EMG curve: {e}\n"
488
+ f"Time range: {peak_time[0]:.2f} to {peak_time[-1]:.2f}\n"
489
+ f"Signal range: {min(peak_signal):.2f} to {max(peak_signal):.2f}\n"
490
+ f"Event number: {event_num}\n"
491
+ "Returning np.nan values."
492
+ )
493
+ return np.full(len(p0), np.nan), np.nan, np.nan
494
+
495
+ emg_fit = exponnorm.pdf(peak_time, *param)
496
+ chisqr, redchi = chi_square(peak_signal, emg_fit, len(p0))
497
+
498
+ return param, chisqr, redchi
499
+
500
+
501
+ def calculate_area_under_emg(time_slice: np.ndarray, param: np.ndarray) -> float:
502
+ """
503
+ Calculate the area under the emg fit which is equal to the impact charge.
504
+
505
+ Parameters
506
+ ----------
507
+ time_slice : numpy.ndarray
508
+ Time values around the peak.
509
+ param : numpy.ndarray
510
+ Optimal parameters (k, mu, sigma) for the emg curve fit.
511
+
512
+ Returns
513
+ -------
514
+ float
515
+ Total area under the emg curve.
516
+ """
517
+ # Extract EMG fit parameters: k, mu, sigma
518
+ k, mu, sigma = param
519
+ # Compute integral
520
+ area, _ = quad(exponnorm.pdf, time_slice[0], time_slice[-1], args=(k, mu, sigma))
521
+
522
+ return float(area)
523
+
524
+
525
+ def estimate_dust_mass(
526
+ low_sampling_time: xr.DataArray,
527
+ target_signal: xr.DataArray,
528
+ remove_noise: bool = True,
529
+ ) -> tuple[NDArray, float, float, float, NDArray]:
530
+ """
531
+ Filter and fit the target or ion grid signals to get the total dust impact charge.
532
+
533
+ Parameters
534
+ ----------
535
+ low_sampling_time : xarray.DataArray
536
+ The low sampling time array.
537
+ target_signal : xarray.DataArray
538
+ Target signal data.
539
+ remove_noise : bool
540
+ If true, attempt to remove background noise, otherwise fit on the unfiltered
541
+ signal.
542
+
543
+ Returns
544
+ -------
545
+ param : numpy.ndarray
546
+ Optimal target signal fit values for the parameters (popt)
547
+ [time_of_impact, constant_offset, amplitude, rise_time, discharge_time]
548
+ if fit successful. None otherwise.
549
+ sig_amp : float
550
+ Signal amplitude, calculated as difference between fitted maximum signal
551
+ and baseline mean if fit successful. None otherwise.
552
+ chi_squared : float
553
+ Sum of squared residuals from the fit.
554
+ reduced_chi_squared : float
555
+ Chi-squared per degree of freedom.
556
+ result : numpy.ndarray
557
+ The model values evaluated at each time point.
558
+ """
559
+ signal = np.array(target_signal.data)
560
+ time = np.array(low_sampling_time.data)
561
+ good_mask = np.logical_and(
562
+ time >= BaselineNoiseTime.START,
563
+ time <= BaselineNoiseTime.STOP,
564
+ )
565
+ if not np.any(good_mask):
566
+ logger.warning(
567
+ "Unable to find baseline noise. "
568
+ f"There is no signal from {BaselineNoiseTime.START} to "
569
+ f"{BaselineNoiseTime.STOP} ns."
570
+ )
571
+ if remove_noise:
572
+ # Remove noise due to "microphonics"
573
+ signal = remove_signal_noise(time, signal, good_mask)
574
+ # Time before image charge
575
+ pre = -2.0
576
+ # Get signal values where the time is before the image charge
577
+ signal_before_imapact = signal[time < pre]
578
+ # Center the baseline signal around zero
579
+ signal_baseline = signal_before_imapact - np.mean(signal_before_imapact)
580
+
581
+ # Initial Guess for the parameters of the ion grid signal
582
+ time_of_impact = 0.0 # Time of dust hit
583
+ constant_offset = 0.0 # Initial baseline
584
+ amplitude: float = np.max(signal) # Signal height
585
+ rise_time = 0.371 # How fast the signal rises (s)
586
+ discharge_time = 0.371 # How fast signal decays (s)
587
+
588
+ p0 = [time_of_impact, constant_offset, amplitude, rise_time, discharge_time]
589
+
590
+ try:
591
+ with np.errstate(invalid="ignore", over="ignore"):
592
+ param, _ = curve_fit(
593
+ fit_impact,
594
+ time,
595
+ signal,
596
+ p0=p0,
597
+ maxfev=100_000, # , epsfcn=1e-10
598
+ )
599
+ except RuntimeError as e:
600
+ logger.warning(
601
+ f"Failed to fit curve: {e}\n"
602
+ f"Time range: {time[0]:.2f} to {time[-1]:.2f}\n"
603
+ f"Signal range: {min(signal):.2f} to {max(signal):.2f}\n"
604
+ "Returning None."
605
+ )
606
+ return (
607
+ np.full(len(p0), np.nan),
608
+ np.nan,
609
+ np.nan,
610
+ np.nan,
611
+ np.full_like(time, np.nan),
612
+ )
613
+
614
+ impact_fit = fit_impact(time, *param)
615
+ # Calculate the resulting signal amplitude after removing baseline noise
616
+ sig_amp = max(impact_fit) - np.mean(signal_baseline)
617
+ chisqr, redchi = chi_square(signal, impact_fit, len(p0))
618
+
619
+ return param, float(sig_amp), chisqr, redchi, impact_fit
620
+
621
+
622
+ def fit_impact(
623
+ time: np.ndarray,
624
+ time_of_impact: float,
625
+ constant_offset: float,
626
+ amplitude: float,
627
+ rise_time: float,
628
+ discharge_time: float,
629
+ ) -> NDArray:
630
+ """
631
+ Fit function for the Ion Grid and two target signals.
632
+
633
+ Parameters
634
+ ----------
635
+ time : np.ndarray
636
+ Time values for the signal.
637
+ time_of_impact : float
638
+ Time of dust impact.
639
+ constant_offset : float
640
+ Initial baseline noise.
641
+ amplitude : float
642
+ Signal height.
643
+ rise_time : float
644
+ How fast the signal rises (s).
645
+ discharge_time : float
646
+ How fast the signal decays (s).
647
+
648
+ Returns
649
+ -------
650
+ np.ndarray
651
+ Function values calculated at the input time points.
652
+
653
+ Notes
654
+ -----
655
+ Impact charge fit function [1]_:
656
+ Y(t) = C₀ + H(t - t₀)[C₂(1 - e^(-(t-t₀)/τ₁))e^(-(t-t₀)/τ₂) - C₁]
657
+
658
+ References
659
+ ----------
660
+ .. [1] Horányi, M., et al. (2014), The Lunar Dust Experiment (LDEX) Onboard the
661
+ Lunar Atmosphere and Dust Environment Explorer (LADEE) mission, Space Sci. Rev.,
662
+ 185(1–4), 93–113, doi:10.1007/s11214-014-0118-7.
663
+ """
664
+ exponent_1 = 1.0 - np.exp(-(time - time_of_impact) / rise_time)
665
+ exponent_2 = np.exp(-(time - time_of_impact) / discharge_time)
666
+ return constant_offset + np.heaviside(time - time_of_impact, 0) * (
667
+ amplitude * exponent_1 * exponent_2
668
+ )
669
+
670
+
671
+ def remove_signal_noise(
672
+ time: np.ndarray, signal: np.ndarray, good_mask: np.ndarray
673
+ ) -> NDArray:
674
+ """
675
+ Remove linear, sine wave, and high frequency background noise from the input signal.
676
+
677
+ Parameters
678
+ ----------
679
+ time : np.ndarray
680
+ Time values for the signal.
681
+ signal : numpy.ndarray
682
+ Target or Ion Grid signal.
683
+ good_mask : numpy.ndarray
684
+ Boolean mask for the signal array to determine where the baseline noise is.
685
+
686
+ Returns
687
+ -------
688
+ numpy.ndarray
689
+ Signal with linear, sine wave, and high frequency background noise filtered out.
690
+ """
691
+ # Remove linear noise
692
+ signal = detrend(signal, type="linear")
693
+ # Remove sine wave Background
694
+ baseline_detrended = signal[good_mask]
695
+ # Approximate initial values for the fit
696
+ amplitude: float = max(baseline_detrended)
697
+ frequency = idex_constants.TARGET_NOISE_FREQUENCY
698
+ # Horizontal wave shift
699
+ phase_shift = 45
700
+ # Minimize function
701
+ p0 = [amplitude, frequency, phase_shift]
702
+ # Fit a sign wave to the baseline noise with initial best guesses of
703
+ # amplitude, period, and phase shift
704
+ try:
705
+ # Set epsfcn to 1e-10 to mimic what lmfit minimize does
706
+ param, _ = curve_fit(
707
+ sine_fit,
708
+ time[good_mask],
709
+ baseline_detrended,
710
+ p0=p0,
711
+ maxfev=100_000,
712
+ epsfcn=1e-10,
713
+ )
714
+ # Remove the sine wave background from the signal
715
+ signal -= sine_fit(time, *param)
716
+ except RuntimeError as e:
717
+ logger.warning(f"Failed to fit background noise sine wave : {e}\n")
718
+
719
+ # Use the butterworth filter to smooth remaining noise and remove noise above
720
+ # desired cutoff
721
+ signal = butter_lowpass_filter(time, signal)
722
+ return signal
723
+
724
+
725
+ def sine_fit(time: np.ndarray, a: float, f: float, p: float) -> NDArray:
726
+ """
727
+ Generate a sine wave with given amplitude, angular frequency, and phase.
728
+
729
+ Parameters
730
+ ----------
731
+ time : numpy.ndarray
732
+ Time points at which to evaluate the sine wave, in seconds.
733
+ a : float
734
+ Amplitude of the sine wave.
735
+ f : float
736
+ Angular frequency of the sine wave.
737
+ p : float
738
+ Phase shift of the sine wave in radians.
739
+
740
+ Returns
741
+ -------
742
+ numpy.ndarray
743
+ Sine wave values calculated at the input time points.
744
+ """
745
+ return a * np.sin(f * time + p)
746
+
747
+
748
+ def butter_lowpass_filter(
749
+ time: np.ndarray,
750
+ signal: np.ndarray,
751
+ cutoff: float = idex_constants.TARGET_HIGH_FREQUENCY_CUTOFF,
752
+ ) -> NDArray:
753
+ """
754
+ Apply a Butterworth low-pass filter to remove high frequency noise from the signal.
755
+
756
+ Parameters
757
+ ----------
758
+ time : numpy.ndarray
759
+ Time values for the signal.
760
+ signal : numpy.ndarray
761
+ Target or Ion Grid signal.
762
+ cutoff : float
763
+ Frequency cutoff in Mhz (time is in microseconds).
764
+
765
+ Returns
766
+ -------
767
+ numpy.ndarray
768
+ Filtered signal.
769
+ """
770
+ sample_period = time[1] - time[0]
771
+ # sampling frequency
772
+ fs = (time[-1] - time[0]) / sample_period # Hz
773
+ # Calculate nyquist frequency
774
+ # It is the highest frequency for the sampling frequency
775
+ nyq = 0.5 * fs
776
+ # sine wave can be approx represented as quadratic
777
+ order = 2
778
+ # Normalize the nyquist frequency. It is expected to be between 0 and 1
779
+ normal_cutoff = cutoff / nyq
780
+ # Get the filter coefficients
781
+ b, a = butter(order, normal_cutoff, btype="low", analog=False)
782
+ y = filtfilt(b, a, signal)
783
+ return y
784
+
785
+
786
+ def chi_square(
787
+ observed: np.ndarray, expected: np.ndarray, num_params: int
788
+ ) -> tuple[float, float]:
789
+ """
790
+ Calculate the chi-square and reduced chi-square statistics.
791
+
792
+ This implementation follows the approach used in lmfit.minimize()'s
793
+ _calculate_statistics() method, which calculates chi-square as the sum of squared
794
+ residuals:
795
+
796
+ chisqr = (residual**2).sum()
797
+
798
+ And reduced chi-square as the chi-square divided by degrees of freedom:
799
+
800
+ ndata = len(residual)
801
+ nfree = ndata - number_of_parameters
802
+ redchi = chisqr / max(1, nfree)
803
+
804
+ Parameters
805
+ ----------
806
+ observed : numpy.ndarray
807
+ The observed signal.
808
+ expected : numpy.ndarray
809
+ The expected signal calculated with the fit parameters.
810
+ num_params : int
811
+ The number of parameters used in the fit.
812
+
813
+ Returns
814
+ -------
815
+ chisqr : float
816
+ The chi-square value.
817
+ redchi : float
818
+ The reduced chi-square value.
819
+ """
820
+ residuals = observed - expected
821
+ chisqr = float(np.sum(residuals**2))
822
+ redchi = chisqr / max(1, (len(observed) - num_params))
823
+ return chisqr, redchi