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