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.
- imap_processing/__init__.py +11 -11
- imap_processing/_version.py +2 -2
- imap_processing/ccsds/ccsds_data.py +1 -2
- imap_processing/ccsds/excel_to_xtce.py +66 -18
- imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +24 -40
- imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +934 -42
- imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +1846 -128
- 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 +27 -14
- 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 +25 -9
- imap_processing/cdf/config/imap_idex_l1a_variable_attrs.yaml +6 -4
- imap_processing/cdf/config/imap_idex_l1b_variable_attrs.yaml +3 -3
- 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 +23 -20
- 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_swapi_variable_attrs.yaml +22 -0
- 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 +64 -52
- imap_processing/cdf/config/imap_swe_l2_variable_attrs.yaml +71 -47
- imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +180 -19
- imap_processing/cdf/config/imap_ultra_l1a_variable_attrs.yaml +5045 -41
- imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +80 -17
- imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +32 -57
- imap_processing/cdf/utils.py +52 -38
- imap_processing/cli.py +477 -233
- imap_processing/codice/codice_l1a.py +466 -131
- imap_processing/codice/codice_l1b.py +51 -152
- imap_processing/codice/constants.py +1360 -569
- imap_processing/codice/decompress.py +2 -6
- imap_processing/ena_maps/ena_maps.py +1103 -146
- 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 +55 -52
- imap_processing/glows/l1a/glows_l1a.py +28 -99
- imap_processing/glows/l1a/glows_l1a_data.py +2 -2
- imap_processing/glows/l1b/glows_l1b.py +1 -4
- imap_processing/glows/l1b/glows_l1b_data.py +1 -3
- imap_processing/glows/l2/glows_l2.py +2 -5
- imap_processing/hi/l1a/hi_l1a.py +54 -29
- 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 +111 -82
- imap_processing/hi/l1c/hi_l1c.py +416 -32
- imap_processing/hi/utils.py +58 -12
- imap_processing/hit/ancillary/imap_hit_l1b-to-l2-sector-dt0-factors_20250219_v002.csv +81 -0
- 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 +235 -5
- imap_processing/hit/l0/constants.py +20 -11
- imap_processing/hit/l0/decom_hit.py +21 -5
- imap_processing/hit/l1a/hit_l1a.py +71 -75
- imap_processing/hit/l1b/constants.py +321 -0
- imap_processing/hit/l1b/hit_l1b.py +377 -67
- imap_processing/hit/l2/constants.py +318 -0
- imap_processing/hit/l2/hit_l2.py +723 -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 +374 -0
- imap_processing/ialirt/l0/process_swapi.py +69 -0
- imap_processing/ialirt/l0/process_swe.py +548 -0
- imap_processing/ialirt/packet_definitions/ialirt.xml +216 -208
- imap_processing/ialirt/packet_definitions/ialirt_codicehi.xml +1 -1
- imap_processing/ialirt/packet_definitions/ialirt_codicelo.xml +1 -1
- imap_processing/ialirt/packet_definitions/ialirt_mag.xml +115 -0
- imap_processing/ialirt/packet_definitions/ialirt_swapi.xml +14 -14
- 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 +33 -0
- imap_processing/idex/idex_l0.py +22 -8
- imap_processing/idex/idex_l1a.py +81 -51
- imap_processing/idex/idex_l1b.py +13 -39
- imap_processing/idex/idex_l2a.py +823 -0
- imap_processing/idex/idex_l2b.py +120 -0
- imap_processing/idex/idex_variable_unpacking_and_eu_conversion.csv +11 -11
- imap_processing/idex/packet_definitions/idex_housekeeping_packet_definition.xml +9130 -0
- imap_processing/lo/l0/lo_science.py +7 -2
- imap_processing/lo/l1a/lo_l1a.py +1 -5
- imap_processing/lo/l1b/lo_l1b.py +702 -29
- imap_processing/lo/l1b/tof_conversions.py +11 -0
- imap_processing/lo/l1c/lo_l1c.py +1 -4
- imap_processing/mag/constants.py +51 -0
- imap_processing/mag/imap_mag_sdc_configuration_v001.py +8 -0
- imap_processing/mag/l0/decom_mag.py +10 -3
- imap_processing/mag/l1a/mag_l1a.py +23 -19
- imap_processing/mag/l1a/mag_l1a_data.py +35 -10
- imap_processing/mag/l1b/mag_l1b.py +259 -50
- imap_processing/mag/l1c/interpolation_methods.py +388 -0
- imap_processing/mag/l1c/mag_l1c.py +621 -17
- imap_processing/mag/l2/mag_l2.py +140 -0
- imap_processing/mag/l2/mag_l2_data.py +288 -0
- imap_processing/quality_flags.py +1 -0
- imap_processing/spacecraft/packet_definitions/scid_x252.xml +538 -0
- imap_processing/spacecraft/quaternions.py +121 -0
- imap_processing/spice/geometry.py +19 -22
- imap_processing/spice/kernels.py +0 -276
- imap_processing/spice/pointing_frame.py +257 -0
- imap_processing/spice/repoint.py +149 -0
- imap_processing/spice/spin.py +38 -33
- imap_processing/spice/time.py +24 -0
- imap_processing/swapi/l1/swapi_l1.py +20 -12
- imap_processing/swapi/l2/swapi_l2.py +116 -5
- imap_processing/swapi/swapi_utils.py +32 -0
- imap_processing/swe/l1a/swe_l1a.py +44 -12
- imap_processing/swe/l1a/swe_science.py +13 -13
- imap_processing/swe/l1b/swe_l1b.py +898 -23
- imap_processing/swe/l2/swe_l2.py +75 -136
- imap_processing/swe/packet_definitions/swe_packet_definition.xml +1121 -1
- imap_processing/swe/utils/swe_constants.py +64 -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 +24 -21
- imap_processing/tests/cdf/test_data/imap_instrument2_global_cdf_attrs.yaml +0 -2
- imap_processing/tests/cdf/test_utils.py +14 -16
- imap_processing/tests/codice/conftest.py +44 -33
- 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 +126 -53
- imap_processing/tests/codice/test_codice_l1b.py +6 -7
- imap_processing/tests/codice/test_decompress.py +4 -4
- imap_processing/tests/conftest.py +239 -27
- imap_processing/tests/ena_maps/conftest.py +51 -0
- imap_processing/tests/ena_maps/test_ena_maps.py +1068 -110
- imap_processing/tests/ena_maps/test_map_utils.py +66 -43
- imap_processing/tests/ena_maps/test_spatial_utils.py +17 -21
- imap_processing/tests/glows/conftest.py +10 -14
- imap_processing/tests/glows/test_glows_decom.py +4 -4
- imap_processing/tests/glows/test_glows_l1a_cdf.py +6 -27
- imap_processing/tests/glows/test_glows_l1a_data.py +6 -8
- imap_processing/tests/glows/test_glows_l1b.py +11 -11
- imap_processing/tests/glows/test_glows_l1b_data.py +5 -5
- imap_processing/tests/glows/test_glows_l2.py +2 -8
- imap_processing/tests/hi/conftest.py +1 -1
- 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 +22 -27
- imap_processing/tests/hi/test_hi_l1c.py +249 -18
- imap_processing/tests/hi/test_l1a.py +35 -7
- imap_processing/tests/hi/test_science_direct_event.py +3 -3
- imap_processing/tests/hi/test_utils.py +24 -2
- imap_processing/tests/hit/helpers/l1_validation.py +74 -73
- 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 +5 -1
- imap_processing/tests/hit/test_hit_l1a.py +32 -36
- imap_processing/tests/hit/test_hit_l1b.py +300 -81
- imap_processing/tests/hit/test_hit_l2.py +716 -0
- imap_processing/tests/hit/test_hit_utils.py +184 -7
- imap_processing/tests/hit/validation_data/hit_l1b_standard_sample2_nsrl_v4_3decimals.csv +62 -62
- 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/hit/validation_data/sci_sample_raw.csv +1 -1
- imap_processing/tests/ialirt/data/l0/461971383-404.bin +0 -0
- imap_processing/tests/ialirt/data/l0/461971384-405.bin +0 -0
- imap_processing/tests/ialirt/data/l0/461971385-406.bin +0 -0
- imap_processing/tests/ialirt/data/l0/461971386-407.bin +0 -0
- imap_processing/tests/ialirt/data/l0/461971387-408.bin +0 -0
- imap_processing/tests/ialirt/data/l0/461971388-409.bin +0 -0
- imap_processing/tests/ialirt/data/l0/461971389-410.bin +0 -0
- imap_processing/tests/ialirt/data/l0/461971390-411.bin +0 -0
- imap_processing/tests/ialirt/data/l0/461971391-412.bin +0 -0
- imap_processing/tests/ialirt/data/l0/sample_decoded_i-alirt_data.csv +383 -0
- imap_processing/tests/ialirt/unit/test_decom_ialirt.py +16 -81
- imap_processing/tests/ialirt/unit/test_grouping.py +81 -0
- imap_processing/tests/ialirt/unit/test_parse_mag.py +223 -0
- imap_processing/tests/ialirt/unit/test_process_codicehi.py +3 -3
- imap_processing/tests/ialirt/unit/test_process_codicelo.py +3 -10
- imap_processing/tests/ialirt/unit/test_process_ephemeris.py +4 -4
- imap_processing/tests/ialirt/unit/test_process_hit.py +3 -3
- imap_processing/tests/ialirt/unit/test_process_swapi.py +24 -16
- imap_processing/tests/ialirt/unit/test_process_swe.py +319 -6
- imap_processing/tests/ialirt/unit/test_time.py +16 -0
- imap_processing/tests/idex/conftest.py +127 -6
- imap_processing/tests/idex/test_data/imap_idex_l0_raw_20231218_v001.pkts +0 -0
- imap_processing/tests/idex/test_data/imap_idex_l0_raw_20241206_v001.pkts +0 -0
- imap_processing/tests/idex/test_data/imap_idex_l0_raw_20250108_v001.pkts +0 -0
- imap_processing/tests/idex/test_data/impact_14_tof_high_data.txt +4508 -4508
- imap_processing/tests/idex/test_idex_l0.py +33 -11
- imap_processing/tests/idex/test_idex_l1a.py +92 -21
- imap_processing/tests/idex/test_idex_l1b.py +106 -27
- imap_processing/tests/idex/test_idex_l2a.py +399 -0
- imap_processing/tests/idex/test_idex_l2b.py +93 -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_l1a.py +3 -3
- imap_processing/tests/lo/test_lo_l1b.py +515 -6
- imap_processing/tests/lo/test_lo_l1c.py +1 -1
- imap_processing/tests/lo/test_lo_science.py +7 -7
- imap_processing/tests/lo/test_star_sensor.py +1 -1
- imap_processing/tests/mag/conftest.py +120 -2
- imap_processing/tests/mag/test_mag_decom.py +5 -4
- imap_processing/tests/mag/test_mag_l1a.py +51 -7
- imap_processing/tests/mag/test_mag_l1b.py +40 -59
- imap_processing/tests/mag/test_mag_l1c.py +354 -19
- imap_processing/tests/mag/test_mag_l2.py +130 -0
- imap_processing/tests/mag/test_mag_validation.py +247 -26
- 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/L1c/T013/mag-l1b-l1c-t013-magi-normal-in.csv +1217 -0
- imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-magi-normal-out.csv +1857 -0
- imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-mago-normal-in.csv +1217 -0
- imap_processing/tests/mag/validation/L1c/T013/mag-l1b-l1c-t013-mago-normal-out.csv +1857 -0
- imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-magi-normal-in.csv +1217 -0
- imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-magi-normal-out.csv +1793 -0
- imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-mago-normal-in.csv +1217 -0
- imap_processing/tests/mag/validation/L1c/T014/mag-l1b-l1c-t014-mago-normal-out.csv +1793 -0
- imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-burst-in.csv +2561 -0
- imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-normal-in.csv +961 -0
- imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-magi-normal-out.csv +1539 -0
- imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-mago-normal-in.csv +1921 -0
- imap_processing/tests/mag/validation/L1c/T015/mag-l1b-l1c-t015-mago-normal-out.csv +2499 -0
- imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-magi-normal-in.csv +865 -0
- imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-magi-normal-out.csv +1196 -0
- imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-mago-normal-in.csv +1729 -0
- imap_processing/tests/mag/validation/L1c/T016/mag-l1b-l1c-t016-mago-normal-out.csv +3053 -0
- imap_processing/tests/mag/validation/L2/imap_mag_l1b_norm-mago_20251017_v002.cdf +0 -0
- imap_processing/tests/mag/validation/calibration/imap_mag_l1b-calibration_20240229_v001.cdf +0 -0
- imap_processing/tests/mag/validation/calibration/imap_mag_l2-calibration-matrices_20251017_v004.cdf +0 -0
- imap_processing/tests/mag/validation/calibration/imap_mag_l2-offsets-norm_20251017_20251017_v001.cdf +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_data/fake_spin_data.csv +11 -11
- imap_processing/tests/spice/test_geometry.py +9 -12
- imap_processing/tests/spice/test_kernels.py +1 -200
- imap_processing/tests/spice/test_pointing_frame.py +185 -0
- imap_processing/tests/spice/test_repoint.py +121 -0
- imap_processing/tests/spice/test_spin.py +50 -9
- imap_processing/tests/spice/test_time.py +14 -0
- imap_processing/tests/swapi/lut/imap_swapi_esa-unit-conversion_20250211_v000.csv +73 -0
- imap_processing/tests/swapi/lut/imap_swapi_lut-notes_20250211_v000.csv +1025 -0
- imap_processing/tests/swapi/test_swapi_l1.py +13 -11
- imap_processing/tests/swapi/test_swapi_l2.py +180 -8
- 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/lut/checker-board-indices.csv +24 -0
- imap_processing/tests/swe/lut/imap_swe_esa-lut_20250301_v000.csv +385 -0
- imap_processing/tests/swe/lut/imap_swe_l1b-in-flight-cal_20240510_20260716_v000.csv +3 -0
- imap_processing/tests/swe/test_swe_l1a.py +20 -2
- 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_l1a_science.py +3 -3
- imap_processing/tests/swe/test_swe_l1b.py +162 -24
- imap_processing/tests/swe/test_swe_l2.py +153 -91
- imap_processing/tests/test_cli.py +171 -88
- imap_processing/tests/test_utils.py +140 -17
- 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 +369 -0
- imap_processing/tests/ultra/unit/conftest.py +115 -89
- imap_processing/tests/ultra/unit/test_badtimes.py +4 -4
- imap_processing/tests/ultra/unit/test_cullingmask.py +8 -6
- imap_processing/tests/ultra/unit/test_de.py +14 -13
- imap_processing/tests/ultra/unit/test_decom_apid_880.py +27 -76
- imap_processing/tests/ultra/unit/test_decom_apid_881.py +54 -11
- imap_processing/tests/ultra/unit/test_decom_apid_883.py +12 -10
- imap_processing/tests/ultra/unit/test_decom_apid_896.py +202 -55
- imap_processing/tests/ultra/unit/test_lookup_utils.py +23 -1
- imap_processing/tests/ultra/unit/test_spacecraft_pset.py +77 -0
- imap_processing/tests/ultra/unit/test_ultra_l1a.py +98 -305
- imap_processing/tests/ultra/unit/test_ultra_l1b.py +60 -14
- imap_processing/tests/ultra/unit/test_ultra_l1b_annotated.py +2 -2
- imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py +26 -27
- imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +239 -70
- imap_processing/tests/ultra/unit/test_ultra_l1c.py +5 -5
- imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +114 -83
- imap_processing/tests/ultra/unit/test_ultra_l2.py +230 -0
- imap_processing/ultra/constants.py +1 -1
- imap_processing/ultra/l0/decom_tools.py +27 -39
- imap_processing/ultra/l0/decom_ultra.py +168 -204
- imap_processing/ultra/l0/ultra_utils.py +152 -136
- imap_processing/ultra/l1a/ultra_l1a.py +55 -271
- imap_processing/ultra/l1b/badtimes.py +1 -4
- imap_processing/ultra/l1b/cullingmask.py +2 -6
- imap_processing/ultra/l1b/de.py +116 -57
- imap_processing/ultra/l1b/extendedspin.py +20 -18
- imap_processing/ultra/l1b/lookup_utils.py +72 -9
- imap_processing/ultra/l1b/ultra_l1b.py +36 -16
- imap_processing/ultra/l1b/ultra_l1b_culling.py +66 -30
- imap_processing/ultra/l1b/ultra_l1b_extended.py +297 -94
- imap_processing/ultra/l1c/histogram.py +2 -6
- imap_processing/ultra/l1c/spacecraft_pset.py +84 -0
- imap_processing/ultra/l1c/ultra_l1c.py +8 -9
- imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +206 -108
- imap_processing/ultra/l2/ultra_l2.py +299 -0
- imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +526 -0
- imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +526 -0
- imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +526 -0
- imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +526 -0
- imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +2 -2
- imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +2 -0
- imap_processing/ultra/packet_definitions/README.md +38 -0
- imap_processing/ultra/packet_definitions/ULTRA_SCI_COMBINED.xml +15302 -482
- imap_processing/ultra/utils/ultra_l1_utils.py +31 -12
- imap_processing/utils.py +69 -29
- {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/METADATA +10 -6
- imap_processing-0.13.0.dist-info/RECORD +578 -0
- imap_processing/cdf/config/imap_mag_l1_variable_attrs.yaml +0 -237
- imap_processing/hi/l1a/housekeeping.py +0 -27
- imap_processing/hi/l1b/hi_eng_unit_convert_table.csv +0 -154
- imap_processing/swe/l1b/swe_esa_lookup_table.csv +0 -1441
- imap_processing/swe/l1b/swe_l1b_science.py +0 -652
- 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/swe/test_swe_l1b_science.py +0 -84
- imap_processing/tests/ultra/test_data/mock_data.py +0 -161
- imap_processing/ultra/l1c/pset.py +0 -40
- imap_processing/ultra/lookup_tables/dps_sensitivity45.cdf +0 -0
- imap_processing-0.11.0.dist-info/RECORD +0 -488
- /imap_processing/idex/packet_definitions/{idex_packet_definition.xml → idex_science_packet_definition.xml} +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/20240827095047_SWE_IALIRT_packet.bin +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/BinLog CCSDS_FRAG_TLM_20240826_152323Z_IALIRT_data_for_SDC.bin +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/IALiRT Raw Packet Telemetry.txt +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/apid01152.tlm +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/eu_SWP_IAL_20240826_152033.csv +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/hi_fsw_view_1_ccsds.bin +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/hit_ialirt_sample.ccsds +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/hit_ialirt_sample.csv +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/idle_export_eu.SWE_IALIRT_20240827_093852.csv +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/imap_codice_l1a_hi-ialirt_20240523200000_v0.0.0.cdf +0 -0
- /imap_processing/tests/ialirt/{test_data → data}/l0/imap_codice_l1a_lo-ialirt_20241110193700_v0.0.0.cdf +0 -0
- /imap_processing/{mag/l1b → tests/spacecraft}/__init__.py +0 -0
- /imap_processing/{swe/l1b/engineering_unit_convert_table.csv → tests/swe/lut/imap_swe_eu-conversion_20240510_v000.csv} +0 -0
- /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.13.0.dist-info}/LICENSE +0 -0
- {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/WHEEL +0 -0
- {imap_processing-0.11.0.dist-info → imap_processing-0.13.0.dist-info}/entry_points.txt +0 -0
|
@@ -2,21 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
6
|
-
import pathlib
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from enum import Enum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TypeVar
|
|
9
11
|
|
|
12
|
+
import astropy_healpix.healpy as hp
|
|
10
13
|
import numpy as np
|
|
11
14
|
import xarray as xr
|
|
12
15
|
from numpy.typing import NDArray
|
|
13
16
|
|
|
14
17
|
from imap_processing.cdf.utils import load_cdf
|
|
15
18
|
from imap_processing.ena_maps.utils import map_utils, spatial_utils
|
|
19
|
+
|
|
20
|
+
# The coordinate names can vary between L1C and L2 data (e.g. azimuth vs longitude),
|
|
21
|
+
# so we define an enum to handle the coordinate names.
|
|
22
|
+
from imap_processing.ena_maps.utils.coordinates import CoordNames
|
|
16
23
|
from imap_processing.spice import geometry
|
|
24
|
+
from imap_processing.spice.time import ttj2000ns_to_et
|
|
17
25
|
|
|
18
26
|
logger = logging.getLogger(__name__)
|
|
19
27
|
|
|
28
|
+
# Set the maximum recursion depth for the conversion from Healpix to rectangular SkyMap.
|
|
29
|
+
MAX_SUBDIV_RECURSION_DEPTH = 8
|
|
30
|
+
|
|
20
31
|
|
|
21
32
|
class SkyTilingType(Enum):
|
|
22
33
|
"""Enumeration of the types of tiling used in the ENA maps."""
|
|
@@ -60,7 +71,7 @@ class IndexMatchMethod(Enum):
|
|
|
60
71
|
def match_coords_to_indices(
|
|
61
72
|
input_object: PointingSet | AbstractSkyMap,
|
|
62
73
|
output_object: PointingSet | AbstractSkyMap,
|
|
63
|
-
|
|
74
|
+
event_et: float | None = None,
|
|
64
75
|
) -> NDArray:
|
|
65
76
|
"""
|
|
66
77
|
Find the output indices corresponding to each input coord between 2 spatial objects.
|
|
@@ -92,10 +103,11 @@ def match_coords_to_indices(
|
|
|
92
103
|
The object containing a grid or tessellation of spatial pixels
|
|
93
104
|
into which the input spatial pixel centers will 'land', and be matched to
|
|
94
105
|
corresponding pixel 1D indices in the output frame.
|
|
95
|
-
|
|
106
|
+
event_et : float, optional
|
|
96
107
|
Event time at which to transform the input spatial object to the output frame.
|
|
97
108
|
This can be manually specified, e.g., for converting between Maps which do not
|
|
98
109
|
contain an epoch value.
|
|
110
|
+
If specified, must be in SPICE compatible ET.
|
|
99
111
|
The default value is None, in which case the event time of the PointingSet
|
|
100
112
|
object is used.
|
|
101
113
|
|
|
@@ -121,12 +133,14 @@ def match_coords_to_indices(
|
|
|
121
133
|
if isinstance(input_object, PointingSet) and isinstance(output_object, PointingSet):
|
|
122
134
|
raise ValueError("Cannot match indices between two PointingSet objects.")
|
|
123
135
|
|
|
124
|
-
# If
|
|
125
|
-
|
|
136
|
+
# If event_et is not specified, use epoch of the PointingSet, if present.
|
|
137
|
+
# The epoch will be in units of terrestrial time (TT) J2000 nanoseconds,
|
|
138
|
+
# which must be converted to ephemeris time (ET) for SPICE.
|
|
139
|
+
if event_et is None:
|
|
126
140
|
if isinstance(input_object, PointingSet):
|
|
127
|
-
|
|
141
|
+
event_et = ttj2000ns_to_et(input_object.epoch)
|
|
128
142
|
elif isinstance(output_object, PointingSet):
|
|
129
|
-
|
|
143
|
+
event_et = ttj2000ns_to_et(output_object.epoch)
|
|
130
144
|
else:
|
|
131
145
|
raise ValueError(
|
|
132
146
|
"Event time must be specified if both objects are SkyMaps."
|
|
@@ -137,11 +151,11 @@ def match_coords_to_indices(
|
|
|
137
151
|
|
|
138
152
|
# Transform the input pixel centers to the output frame
|
|
139
153
|
input_obj_az_el_output_frame = geometry.frame_transform_az_el(
|
|
140
|
-
et=
|
|
154
|
+
et=event_et,
|
|
141
155
|
az_el=input_obj_az_el_input_frame,
|
|
142
156
|
from_frame=input_object.spice_reference_frame,
|
|
143
157
|
to_frame=output_object.spice_reference_frame,
|
|
144
|
-
degrees=
|
|
158
|
+
degrees=True,
|
|
145
159
|
)
|
|
146
160
|
|
|
147
161
|
# The way indices are matched depends on the tiling type of the 2nd object
|
|
@@ -174,54 +188,142 @@ def match_coords_to_indices(
|
|
|
174
188
|
elif output_object.tiling_type is SkyTilingType.HEALPIX:
|
|
175
189
|
# To match to a Healpix tessellation, we need to use the healpy function ang2pix
|
|
176
190
|
# which directly returns the index on the output frame's Healpix tessellation.
|
|
177
|
-
"""
|
|
178
|
-
Leaving this as a placeholder for now, so we don't yet
|
|
179
|
-
need to add a healpy dependency. It will look something like the
|
|
180
|
-
following code, much simpler than the rectangular case:
|
|
181
|
-
|
|
182
|
-
```python
|
|
183
|
-
import healpy as hp
|
|
184
191
|
flat_indices_input_grid_output_frame = hp.ang2pix(
|
|
185
|
-
nside=
|
|
186
|
-
theta=
|
|
187
|
-
phi=
|
|
188
|
-
nest=
|
|
192
|
+
nside=output_object.nside,
|
|
193
|
+
theta=input_obj_az_el_output_frame[:, 0], # Lon in degrees
|
|
194
|
+
phi=input_obj_az_el_output_frame[:, 1], # Lat in degrees
|
|
195
|
+
nest=output_object.nested,
|
|
189
196
|
lonlat=True,
|
|
190
197
|
)
|
|
191
|
-
```
|
|
192
|
-
"""
|
|
193
|
-
raise NotImplementedError(
|
|
194
|
-
"Index matching for output tiling type Healpix is not yet implemented."
|
|
195
|
-
)
|
|
196
|
-
|
|
197
198
|
else:
|
|
198
199
|
raise ValueError(
|
|
199
200
|
"Tiling type of the output frame must be either RECTANGULAR or HEALPIX."
|
|
201
|
+
f"Received: {output_object.tiling_type}"
|
|
200
202
|
)
|
|
201
203
|
|
|
202
204
|
return flat_indices_input_grid_output_frame
|
|
203
205
|
|
|
204
206
|
|
|
207
|
+
# Define a TypeVar type to dynamically hint the return type of the base PointingSet
|
|
208
|
+
# class classmethod
|
|
209
|
+
T = TypeVar("T", bound="PointingSet")
|
|
210
|
+
|
|
211
|
+
|
|
205
212
|
# Define the pointing set classes
|
|
206
213
|
class PointingSet(ABC):
|
|
207
214
|
"""
|
|
208
215
|
Abstract class to contain pointing set (PSET) data in the context of ENA sky maps.
|
|
209
216
|
|
|
217
|
+
Any spatial axes - (azimuth, elevation) for Rectangularly gridded tilings or
|
|
218
|
+
(pixel index) for Healpix - must be stored in the last axis/axes of each data array.
|
|
219
|
+
|
|
210
220
|
Parameters
|
|
211
221
|
----------
|
|
212
|
-
dataset : xr.Dataset
|
|
213
|
-
Dataset containing the pointing set data.
|
|
222
|
+
dataset : xr.Dataset | str | Path
|
|
223
|
+
Dataset or path to CDF file containing the pointing set data.
|
|
214
224
|
spice_reference_frame : geometry.SpiceFrame
|
|
215
225
|
The reference Spice frame of the pointing set.
|
|
216
226
|
"""
|
|
217
227
|
|
|
228
|
+
# The minimum set of class attributes for any PointingSet to function with
|
|
229
|
+
# a SkyMap using only the PUSH method of projecting are defined here.
|
|
230
|
+
|
|
231
|
+
# ======== Attributes that are set in the ABC __init__ method ========
|
|
232
|
+
# The xarray.Dataset containing the data from the PSET CDF
|
|
233
|
+
data: xr.Dataset
|
|
234
|
+
# The spice frame that the az_el_points are expressed in
|
|
235
|
+
spice_reference_frame: geometry.SpiceFrame
|
|
236
|
+
|
|
237
|
+
# ======== Attributes required to be set in a subclass ========
|
|
238
|
+
# Azimuth and elevation coordinates of each spatial pixel. The ndarray should
|
|
239
|
+
# have the shape (n, 2) where n is the number of spatial pixels
|
|
240
|
+
az_el_points: np.ndarray
|
|
241
|
+
# Tuple containing the names of each spatial coordinate of the xarray.Dataset
|
|
242
|
+
# stored in the data attribute
|
|
243
|
+
spatial_coords: tuple[str, ...]
|
|
244
|
+
|
|
218
245
|
@abstractmethod
|
|
219
|
-
def __init__(
|
|
246
|
+
def __init__(
|
|
247
|
+
self,
|
|
248
|
+
dataset: xr.Dataset | str | Path,
|
|
249
|
+
spice_reference_frame: geometry.SpiceFrame = geometry.SpiceFrame.IMAP_DPS,
|
|
250
|
+
):
|
|
220
251
|
"""Abstract method to initialize the pointing set object."""
|
|
221
252
|
self.spice_reference_frame = spice_reference_frame
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
253
|
+
|
|
254
|
+
if isinstance(dataset, (str, Path)):
|
|
255
|
+
dataset = load_cdf(dataset)
|
|
256
|
+
self.data = dataset
|
|
257
|
+
|
|
258
|
+
# A PSET must have a single epoch
|
|
259
|
+
if len(np.unique(self.data["epoch"].values)) > 1:
|
|
260
|
+
raise ValueError("Multiple epochs found in the dataset.")
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def num_points(self) -> int:
|
|
264
|
+
"""
|
|
265
|
+
The number of spatial pixels in the pointing set.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
num_points: int
|
|
270
|
+
The number of spatial pixels in the pointing set.
|
|
271
|
+
"""
|
|
272
|
+
return self.az_el_points.shape[0]
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def epoch(self) -> int:
|
|
276
|
+
"""
|
|
277
|
+
The singular epoch value from the xarray.Dataset.
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
-------
|
|
281
|
+
epoch: int
|
|
282
|
+
The epoch value [J2000 TT ns] of the pointing set.
|
|
283
|
+
"""
|
|
284
|
+
return self.data["epoch"].values[0]
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def unwrapped_dims_dict(self) -> dict[str, tuple[str, ...]]:
|
|
288
|
+
"""
|
|
289
|
+
Get dimensions of each variable in the pointing set, with only 1 spatial dim.
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
unwrapped_dims_dict : dict[str, tuple[str, ...]]
|
|
294
|
+
Dictionary of variable names and their dimensions, with only 1 spatial dim.
|
|
295
|
+
The generic pixel dimension is always included.
|
|
296
|
+
E.g.: {"counts": ("epoch", "energy", "pixel")} .
|
|
297
|
+
"""
|
|
298
|
+
variable_dims = {}
|
|
299
|
+
for var_name in self.data.data_vars:
|
|
300
|
+
pset_dims = self.data[var_name].dims
|
|
301
|
+
non_spatial_dims = tuple(
|
|
302
|
+
dim for dim in pset_dims if dim not in self.spatial_coords
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
variable_dims[var_name] = (
|
|
306
|
+
*non_spatial_dims,
|
|
307
|
+
CoordNames.GENERIC_PIXEL.value,
|
|
308
|
+
)
|
|
309
|
+
return variable_dims
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def non_spatial_coords(self) -> dict[str, xr.DataArray]:
|
|
313
|
+
"""
|
|
314
|
+
Get the non-spatial coordinates of the pointing set.
|
|
315
|
+
|
|
316
|
+
Returns
|
|
317
|
+
-------
|
|
318
|
+
non_spatial_coords : dict[str, xr.DataArray]
|
|
319
|
+
Dictionary of coordinate names and their data arrays.
|
|
320
|
+
E.g.: {"epoch": [12345,], "energy": [100, 200, 300]} .
|
|
321
|
+
"""
|
|
322
|
+
non_spatial_coords = {}
|
|
323
|
+
for coord_name in self.data.coords:
|
|
324
|
+
if coord_name not in self.spatial_coords:
|
|
325
|
+
non_spatial_coords[coord_name] = self.data[coord_name]
|
|
326
|
+
return non_spatial_coords
|
|
225
327
|
|
|
226
328
|
def __repr__(self) -> str:
|
|
227
329
|
"""
|
|
@@ -233,66 +335,58 @@ class PointingSet(ABC):
|
|
|
233
335
|
String representation of the pointing set.
|
|
234
336
|
"""
|
|
235
337
|
return (
|
|
236
|
-
f"{self.__class__} PointingSet"
|
|
338
|
+
f"{self.__class__.__name__} PointingSet"
|
|
237
339
|
f"(spice_reference_frame={self.spice_reference_frame})"
|
|
238
340
|
)
|
|
239
341
|
|
|
240
342
|
|
|
241
|
-
class
|
|
343
|
+
class RectangularPointingSet(PointingSet):
|
|
242
344
|
"""
|
|
243
|
-
|
|
345
|
+
Pointing set object for rectangularly tiled data. Currently used in testing.
|
|
244
346
|
|
|
245
347
|
Parameters
|
|
246
348
|
----------
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
Currently, the dataset is expected to be in a rectangular grid,
|
|
349
|
+
dataset : xr.Dataset | str | Path
|
|
350
|
+
Dataset or path to CDF file containing the pointing set data.
|
|
351
|
+
Currently, the dataset is expected to be tiled in a rectangular grid,
|
|
250
352
|
with data_vars indexed along the coordinates:
|
|
251
353
|
- 'epoch' : time value (1 value per PSET)
|
|
252
|
-
- '
|
|
253
|
-
- '
|
|
254
|
-
Some data_vars may additionally be indexed by energy bin;
|
|
255
|
-
however, only the spatial axes are used in this class.
|
|
354
|
+
- 'longitude' : (number of longitude/az bins in L1C)
|
|
355
|
+
- 'latitude' : (number of latitude/el bins in L1C)
|
|
256
356
|
spice_reference_frame : geometry.SpiceFrame
|
|
257
357
|
The reference Spice frame of the pointing set. Default is IMAP_DPS.
|
|
258
358
|
|
|
259
359
|
Raises
|
|
260
360
|
------
|
|
261
361
|
ValueError
|
|
262
|
-
If the
|
|
263
|
-
Or if the
|
|
362
|
+
If the longitude/az or latitude/el bin centers don't match the constructed grid.
|
|
363
|
+
Or if the longitude or latitude bin spacing is not uniform.
|
|
264
364
|
ValueError
|
|
265
365
|
If multiple epochs are found in the dataset.
|
|
266
366
|
"""
|
|
267
367
|
|
|
368
|
+
# In addition to the required attributes defined in the base PointingSet
|
|
369
|
+
# class, the following attributes are required for a RectangularPointingSet
|
|
370
|
+
# to be projected using the PULL method.
|
|
371
|
+
tiling_type: SkyTilingType = SkyTilingType.RECTANGULAR
|
|
372
|
+
sky_grid: spatial_utils.AzElSkyGrid
|
|
373
|
+
|
|
268
374
|
def __init__(
|
|
269
375
|
self,
|
|
270
|
-
|
|
376
|
+
dataset: xr.Dataset | str | Path,
|
|
271
377
|
spice_reference_frame: geometry.SpiceFrame = geometry.SpiceFrame.IMAP_DPS,
|
|
272
378
|
):
|
|
273
|
-
|
|
274
|
-
self.spice_reference_frame = spice_reference_frame
|
|
275
|
-
|
|
276
|
-
# Read in the data and store the xarray dataset as data attr
|
|
277
|
-
if isinstance(l1c_dataset, (str, pathlib.Path)):
|
|
278
|
-
self.data = load_cdf(pathlib.Path(l1c_dataset))
|
|
279
|
-
elif isinstance(l1c_dataset, xr.Dataset):
|
|
280
|
-
self.data = l1c_dataset
|
|
281
|
-
|
|
282
|
-
# A PSET must have a single epoch
|
|
283
|
-
self.epoch = self.data["epoch"].values
|
|
284
|
-
if len(np.unique(self.epoch)) > 1:
|
|
285
|
-
raise ValueError("Multiple epochs found in the dataset.")
|
|
379
|
+
super().__init__(dataset, spice_reference_frame)
|
|
286
380
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
381
|
+
self.spatial_coords = (
|
|
382
|
+
CoordNames.AZIMUTH_L1C.value,
|
|
383
|
+
CoordNames.ELEVATION_L1C.value,
|
|
384
|
+
)
|
|
291
385
|
|
|
292
386
|
# Ensure 1D axes grids are uniformly spaced,
|
|
293
387
|
# then set spacing based on data's azimuth bin spacing.
|
|
294
|
-
az_bin_delta = np.diff(self.data[
|
|
295
|
-
el_bin_delta = np.diff(self.data[
|
|
388
|
+
az_bin_delta = np.diff(self.data[CoordNames.AZIMUTH_L1C.value])
|
|
389
|
+
el_bin_delta = np.diff(self.data[CoordNames.ELEVATION_L1C.value])
|
|
296
390
|
if not np.allclose(az_bin_delta, az_bin_delta[0], atol=1e-10, rtol=0):
|
|
297
391
|
raise ValueError("Azimuth bin spacing is not uniform.")
|
|
298
392
|
if not np.allclose(el_bin_delta, el_bin_delta[0], atol=1e-10, rtol=0):
|
|
@@ -302,28 +396,28 @@ class UltraPointingSet(PointingSet):
|
|
|
302
396
|
"Azimuth and elevation bin spacing do not match: "
|
|
303
397
|
f"az {az_bin_delta[0]} != el {el_bin_delta[0]}."
|
|
304
398
|
)
|
|
305
|
-
|
|
399
|
+
spacing_deg = az_bin_delta[0]
|
|
306
400
|
|
|
307
|
-
# Build the azimuth and elevation grids with an AzElSkyGrid object
|
|
401
|
+
# Build the az/azimuth and el/elevation grids with an AzElSkyGrid object
|
|
308
402
|
# and check that the 1D axes match the dataset's az and el.
|
|
309
403
|
self.sky_grid = spatial_utils.AzElSkyGrid(
|
|
310
|
-
spacing_deg=
|
|
404
|
+
spacing_deg=spacing_deg,
|
|
311
405
|
)
|
|
312
406
|
|
|
313
407
|
for dim, constructed_bins in zip(
|
|
314
|
-
[
|
|
408
|
+
[CoordNames.AZIMUTH_L1C.value, CoordNames.ELEVATION_L1C.value],
|
|
315
409
|
[self.sky_grid.az_bin_midpoints, self.sky_grid.el_bin_midpoints],
|
|
316
410
|
):
|
|
317
411
|
if not np.allclose(
|
|
318
|
-
sorted(
|
|
319
|
-
self.data[
|
|
412
|
+
sorted(constructed_bins),
|
|
413
|
+
self.data[dim],
|
|
320
414
|
atol=1e-10,
|
|
321
415
|
rtol=0,
|
|
322
416
|
):
|
|
323
417
|
raise ValueError(
|
|
324
418
|
f"{dim} bin centers do not match."
|
|
325
|
-
f"Constructed: {
|
|
326
|
-
f"Dataset: {self.data[
|
|
419
|
+
f"Constructed: {constructed_bins}"
|
|
420
|
+
f"Dataset: {self.data[dim]}"
|
|
327
421
|
)
|
|
328
422
|
|
|
329
423
|
# Unwrap the az, el grids to series of points tiling the sky and combine them
|
|
@@ -336,91 +430,263 @@ class UltraPointingSet(PointingSet):
|
|
|
336
430
|
self.sky_grid.el_grid.ravel(),
|
|
337
431
|
)
|
|
338
432
|
)
|
|
339
|
-
self.num_points = self.az_el_points.shape[0]
|
|
340
433
|
|
|
341
|
-
# Also store the bin edges for the pointing set to allow for "pull" method
|
|
342
|
-
# of index matching (not yet implemented).
|
|
343
|
-
# These are 1D arrays of different lengths and cannot be stacked.
|
|
344
|
-
self.az_bin_edges = self.sky_grid.az_bin_edges
|
|
345
|
-
self.el_bin_edges = self.sky_grid.el_bin_edges
|
|
346
434
|
|
|
347
|
-
|
|
435
|
+
class HealpixPointingSet(PointingSet, ABC):
|
|
436
|
+
"""
|
|
437
|
+
Abstract base class for Healpix pointing sets.
|
|
438
|
+
|
|
439
|
+
Defines additional properties and absract properties that are required
|
|
440
|
+
for a PointingSet instance to be used with the match_coords_to_indices
|
|
441
|
+
function.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
tiling_type: SkyTilingType = SkyTilingType.HEALPIX
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def nside(self) -> int:
|
|
348
448
|
"""
|
|
349
|
-
|
|
449
|
+
Number of pixels on the side of one of the 12 top-level healpix tiles.
|
|
350
450
|
|
|
351
451
|
Returns
|
|
352
452
|
-------
|
|
353
|
-
|
|
354
|
-
|
|
453
|
+
npix: int
|
|
454
|
+
The number of pixels on the side of one of the 12 ‘top-level’ healpix
|
|
455
|
+
tiles.
|
|
355
456
|
"""
|
|
356
|
-
return (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
457
|
+
return hp.npix_to_nside(self.num_points)
|
|
458
|
+
|
|
459
|
+
@property
|
|
460
|
+
@abstractmethod
|
|
461
|
+
def nested(self) -> bool:
|
|
462
|
+
"""Abstract property for getting nested boolean."""
|
|
463
|
+
raise NotImplementedError
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class UltraPointingSet(HealpixPointingSet):
|
|
467
|
+
"""
|
|
468
|
+
Pointing set object specifically for Healpix-tiled ULTRA data, nominally at Level1C.
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
dataset : xr.Dataset | str | Path
|
|
473
|
+
Dataset or path to CDF file containing the pointing set data.
|
|
474
|
+
Currently, the dataset is expected to be tiled in a HEALPix tessellation,
|
|
475
|
+
with data_vars indexed along the coordinates:
|
|
476
|
+
- 'epoch' : time value (1 value per PSET, from the mean of the PSET)
|
|
477
|
+
- 'energy' : (number of energy bins in L1C)
|
|
478
|
+
- 'healpix_index' : HEALPix pixel index
|
|
479
|
+
Only the 'healpix_index' coordinate is used in this class for projection.
|
|
480
|
+
spice_reference_frame : geometry.SpiceFrame
|
|
481
|
+
The reference Spice frame of the pointing set. Default is IMAP_DPS.
|
|
482
|
+
|
|
483
|
+
Raises
|
|
484
|
+
------
|
|
485
|
+
ValueError
|
|
486
|
+
If the longitude/az or latitude/el bin centers don't match the constructed grid.
|
|
487
|
+
Or if the longitude or latitude bin spacing is not uniform.
|
|
488
|
+
ValueError
|
|
489
|
+
If multiple epochs are found in the dataset.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
def __init__(
|
|
493
|
+
self,
|
|
494
|
+
dataset: xr.Dataset | str | Path,
|
|
495
|
+
spice_reference_frame: geometry.SpiceFrame = geometry.SpiceFrame.IMAP_DPS,
|
|
496
|
+
):
|
|
497
|
+
super().__init__(dataset, spice_reference_frame)
|
|
498
|
+
|
|
499
|
+
# Set the spatial coordinates and number of points
|
|
500
|
+
self.spatial_coords = (CoordNames.HEALPIX_INDEX.value,)
|
|
501
|
+
|
|
502
|
+
# Tracks Per-Pixel Solid Angle in steradians.
|
|
503
|
+
self.solid_angle = hp.nside2pixarea(self.nside, degrees=False)
|
|
504
|
+
|
|
505
|
+
# Get the azimuth and elevation coordinates of the healpix pixel centers (deg)
|
|
506
|
+
azimuth_pixel_center, elevation_pixel_center = hp.pix2ang(
|
|
507
|
+
nside=self.nside,
|
|
508
|
+
ipix=np.arange(self.num_points),
|
|
509
|
+
nest=self.nested,
|
|
510
|
+
lonlat=True,
|
|
360
511
|
)
|
|
361
512
|
|
|
513
|
+
# Verify that the azimuth and elevation of the healpix pixel centers
|
|
514
|
+
# match the data's azimuth and elevation bin centers.
|
|
515
|
+
# NOTE: They can have different names in the L1C dataset
|
|
516
|
+
# (e.g. "longitude"/"latitude" vs "azimuth"/"elevation").
|
|
517
|
+
for dim, constructed_bins in zip(
|
|
518
|
+
[CoordNames.AZIMUTH_L1C.value, CoordNames.ELEVATION_L1C.value],
|
|
519
|
+
[azimuth_pixel_center, elevation_pixel_center],
|
|
520
|
+
):
|
|
521
|
+
if not np.allclose(
|
|
522
|
+
self.data[dim],
|
|
523
|
+
constructed_bins,
|
|
524
|
+
atol=1e-10,
|
|
525
|
+
rtol=0,
|
|
526
|
+
):
|
|
527
|
+
raise ValueError(
|
|
528
|
+
f"{dim} pixel centers do not match the data's {dim} bin centers."
|
|
529
|
+
f"Constructed: {constructed_bins}"
|
|
530
|
+
f"Dataset: {self.data[dim]}"
|
|
531
|
+
)
|
|
362
532
|
|
|
363
|
-
#
|
|
364
|
-
|
|
365
|
-
|
|
533
|
+
# The coordinates of the healpix pixel centers are stored as a 2D array
|
|
534
|
+
# of shape (num_points, 2) where column 0 is the lon/az
|
|
535
|
+
# and column 1 is the lat/el.
|
|
536
|
+
self.az_el_points = np.column_stack(
|
|
537
|
+
(azimuth_pixel_center, elevation_pixel_center)
|
|
538
|
+
)
|
|
366
539
|
|
|
367
|
-
@
|
|
368
|
-
def
|
|
369
|
-
|
|
540
|
+
@property
|
|
541
|
+
def num_points(self) -> int:
|
|
542
|
+
"""
|
|
543
|
+
Override the base class property to get the number from the dataset.
|
|
544
|
+
|
|
545
|
+
Returns
|
|
546
|
+
-------
|
|
547
|
+
num_points: int
|
|
548
|
+
The number of healpix pixels in the pointing set.
|
|
549
|
+
"""
|
|
550
|
+
return self.data[CoordNames.HEALPIX_INDEX.value].size
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def nested(self) -> bool:
|
|
554
|
+
"""
|
|
555
|
+
Whether the healpix tessellation is nested.
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
-------
|
|
559
|
+
nested: bool
|
|
560
|
+
Whether the healpix tessellation is nested.
|
|
561
|
+
"""
|
|
562
|
+
return bool(
|
|
563
|
+
self.data[CoordNames.HEALPIX_INDEX.value].attrs.get("nested", False)
|
|
564
|
+
)
|
|
370
565
|
|
|
371
566
|
def __repr__(self) -> str:
|
|
372
567
|
"""
|
|
373
|
-
Return a string representation of the
|
|
568
|
+
Return a string representation of the UltraPointingSet.
|
|
374
569
|
|
|
375
570
|
Returns
|
|
376
571
|
-------
|
|
377
572
|
str
|
|
378
|
-
String representation of the
|
|
573
|
+
String representation of the UltraPointingSet.
|
|
379
574
|
"""
|
|
380
|
-
return
|
|
575
|
+
return (
|
|
576
|
+
f"UltraPointingSet\n\t(spice_reference_frame="
|
|
577
|
+
f"{self.spice_reference_frame}, epoch={self.epoch}, "
|
|
578
|
+
f"num_points={self.num_points})"
|
|
579
|
+
)
|
|
381
580
|
|
|
382
581
|
|
|
383
|
-
class
|
|
582
|
+
class HiPointingSet(PointingSet):
|
|
384
583
|
"""
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
NOTE: Internally, the map is stored as a 1D array of pixels.
|
|
584
|
+
PointingSet object specific to Hi L1C PSet data.
|
|
388
585
|
|
|
389
586
|
Parameters
|
|
390
587
|
----------
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
spice_frame : geometry.SpiceFrame
|
|
394
|
-
The reference Spice frame of the map.
|
|
588
|
+
dataset : xarray.Dataset
|
|
589
|
+
Hi L1C pointing set data loaded in an xarray.DataArray.
|
|
395
590
|
"""
|
|
396
591
|
|
|
397
|
-
def __init__(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
self.spacing_deg = spacing_deg
|
|
405
|
-
self.spice_reference_frame = spice_frame
|
|
406
|
-
self.sky_grid = spatial_utils.AzElSkyGrid(
|
|
407
|
-
spacing_deg=self.spacing_deg,
|
|
592
|
+
def __init__(self, dataset: xr.Dataset):
|
|
593
|
+
super().__init__(dataset, spice_reference_frame=geometry.SpiceFrame.ECLIPJ2000)
|
|
594
|
+
self.az_el_points = np.column_stack(
|
|
595
|
+
(
|
|
596
|
+
np.squeeze(self.data["hae_longitude"]),
|
|
597
|
+
np.squeeze(self.data["hae_latitude"]),
|
|
598
|
+
)
|
|
408
599
|
)
|
|
600
|
+
self.spatial_coords = ("spin_angle_bin",)
|
|
409
601
|
|
|
410
|
-
# Solid angles of each pixel in the map grid in units of steradians
|
|
411
|
-
self.solid_angle_grid = spatial_utils.build_solid_angle_map(
|
|
412
|
-
spacing_deg=self.spacing_deg,
|
|
413
|
-
)
|
|
414
602
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
603
|
+
# Define the Map classes
|
|
604
|
+
class AbstractSkyMap(ABC):
|
|
605
|
+
"""
|
|
606
|
+
Abstract base class to contain map data in the context of ENA sky maps.
|
|
607
|
+
|
|
608
|
+
Data values are stored internally in an xarray Dataset, in the .data_1d attribute.
|
|
609
|
+
where the final (-1) axis is the only spatial dimension.
|
|
610
|
+
If the map is rectangular, this axis is the raveled 2D grid.
|
|
611
|
+
If the map is Healpix, this axis is the 1D array of Healpix pixel indices.
|
|
612
|
+
|
|
613
|
+
The data can be also accessed via the to_dataset method, which rewraps the data to
|
|
614
|
+
a 2D grid shape if the map is rectangular and formats the data as an xarray
|
|
615
|
+
Dataset with the correct dims and coords.
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
@abstractmethod
|
|
619
|
+
def __init__(self) -> None:
|
|
620
|
+
self.tiling_type: SkyTilingType
|
|
621
|
+
self.sky_grid: spatial_utils.AzElSkyGrid
|
|
622
|
+
self.num_points: int
|
|
623
|
+
self.non_spatial_coords: dict[str, xr.DataArray | NDArray]
|
|
624
|
+
self.spatial_coords: dict[str, xr.DataArray | NDArray]
|
|
625
|
+
self.binning_grid_shape: tuple[int, ...]
|
|
626
|
+
self.data_1d: xr.Dataset
|
|
627
|
+
|
|
628
|
+
# Initialize values to be used by the instrument code to push/pull
|
|
629
|
+
self.values_to_push_project: list[str] = []
|
|
630
|
+
self.values_to_pull_project: list[str] = []
|
|
631
|
+
|
|
632
|
+
def to_dataset(self) -> xr.Dataset:
|
|
633
|
+
"""
|
|
634
|
+
Get the SkyMap data as a formatted xarray Dataset.
|
|
635
|
+
|
|
636
|
+
Returns
|
|
637
|
+
-------
|
|
638
|
+
xr.Dataset
|
|
639
|
+
The SkyMap data as a formatted xarray Dataset with dims and coords.
|
|
640
|
+
If the SkyMap is empty, an empty xarray Dataset is returned.
|
|
641
|
+
If the SkyMap is Rectangular, the data is rewrapped to a 2D grid of
|
|
642
|
+
lon/lat (AKA az/el) coordinates.
|
|
643
|
+
If the SkyMap is Healpix, the data is unchanged from the data_1d, but
|
|
644
|
+
the pixel coordinate is renamed to CoordNames.HEALPIX_INDEX.value.
|
|
645
|
+
"""
|
|
646
|
+
if len(self.data_1d.data_vars) == 0:
|
|
647
|
+
# If the map is empty, return an empty xarray Dataset,
|
|
648
|
+
# with the unaltered spatial coords of the map
|
|
649
|
+
return xr.Dataset(
|
|
650
|
+
{},
|
|
651
|
+
coords={**self.spatial_coords},
|
|
652
|
+
)
|
|
421
653
|
|
|
422
|
-
|
|
423
|
-
|
|
654
|
+
if self.tiling_type is SkyTilingType.HEALPIX:
|
|
655
|
+
# return the data_1d as is, but with the pixel coordinate
|
|
656
|
+
# renamed to CoordNames.HEALPIX_INDEX.value
|
|
657
|
+
return self.data_1d.rename(
|
|
658
|
+
{CoordNames.GENERIC_PIXEL.value: CoordNames.HEALPIX_INDEX.value}
|
|
659
|
+
)
|
|
660
|
+
elif self.tiling_type is SkyTilingType.RECTANGULAR:
|
|
661
|
+
# Rewrap each data array in the data_1d to the original 2D grid shape
|
|
662
|
+
rewrapped_data = {}
|
|
663
|
+
for key in self.data_1d.data_vars:
|
|
664
|
+
# drop pixel dim from the end, and add the spatial coords as dims
|
|
665
|
+
rewrapped_dims = [
|
|
666
|
+
dim
|
|
667
|
+
for dim in self.data_1d[key].dims
|
|
668
|
+
if dim != CoordNames.GENERIC_PIXEL.value
|
|
669
|
+
]
|
|
670
|
+
rewrapped_dims.extend(self.spatial_coords.keys())
|
|
671
|
+
rewrapped_data[key] = xr.DataArray(
|
|
672
|
+
spatial_utils.rewrap_even_spaced_az_el_grid(
|
|
673
|
+
self.data_1d[key].values,
|
|
674
|
+
self.binning_grid_shape,
|
|
675
|
+
),
|
|
676
|
+
dims=rewrapped_dims,
|
|
677
|
+
)
|
|
678
|
+
# Add the output coordinates to the rewrapped data, excluding the pixel
|
|
679
|
+
self.non_spatial_coords.update(
|
|
680
|
+
{
|
|
681
|
+
key: self.data_1d[key].coords[key]
|
|
682
|
+
for key in self.data_1d[key].coords
|
|
683
|
+
if key != CoordNames.GENERIC_PIXEL.value
|
|
684
|
+
}
|
|
685
|
+
)
|
|
686
|
+
return xr.Dataset(
|
|
687
|
+
rewrapped_data,
|
|
688
|
+
coords={**self.non_spatial_coords, **self.spatial_coords},
|
|
689
|
+
)
|
|
424
690
|
|
|
425
691
|
def project_pset_values_to_map(
|
|
426
692
|
self,
|
|
@@ -440,7 +706,7 @@ class RectangularSkyMap(AbstractSkyMap):
|
|
|
440
706
|
pointing_set : PointingSet
|
|
441
707
|
The pointing set containing the values to project to the map.
|
|
442
708
|
value_keys : list[tuple[str, IndexMatchMethod]] | None
|
|
443
|
-
The keys of the values to project to the map.
|
|
709
|
+
The keys of the values in the PointingSet to project to the map.
|
|
444
710
|
Ex.: ["counts", "flux"]
|
|
445
711
|
data_vars named each key must be present, and of the same dimensionality in
|
|
446
712
|
each pointing set which is to be projected to the map.
|
|
@@ -456,45 +722,345 @@ class RectangularSkyMap(AbstractSkyMap):
|
|
|
456
722
|
"""
|
|
457
723
|
if value_keys is None:
|
|
458
724
|
value_keys = list(pointing_set.data.data_vars.keys())
|
|
459
|
-
|
|
460
725
|
for value_key in value_keys:
|
|
461
726
|
if value_key not in pointing_set.data.data_vars:
|
|
462
727
|
raise ValueError(f"Value key {value_key} not found in pointing set.")
|
|
463
728
|
|
|
464
|
-
# Determine the indices of the sky map grid that correspond to
|
|
465
|
-
# each pixel in the pointing set.
|
|
466
729
|
if index_match_method is IndexMatchMethod.PUSH:
|
|
730
|
+
# Determine the indices of the sky map grid that correspond to
|
|
731
|
+
# each pixel in the pointing set.
|
|
467
732
|
matched_indices_push = match_coords_to_indices(
|
|
468
733
|
input_object=pointing_set,
|
|
469
734
|
output_object=self,
|
|
470
735
|
)
|
|
736
|
+
elif index_match_method is IndexMatchMethod.PULL:
|
|
737
|
+
# Determine the indices of the pointing set grid that correspond to
|
|
738
|
+
# each pixel in the sky map.
|
|
739
|
+
matched_indices_pull = match_coords_to_indices(
|
|
740
|
+
input_object=self,
|
|
741
|
+
output_object=pointing_set,
|
|
742
|
+
)
|
|
743
|
+
else:
|
|
744
|
+
raise NotImplementedError(
|
|
745
|
+
"Only PUSH and PULL index matching methods are supported."
|
|
746
|
+
)
|
|
471
747
|
|
|
472
748
|
for value_key in value_keys:
|
|
749
|
+
pset_values = pointing_set.data[value_key]
|
|
750
|
+
|
|
473
751
|
# If multiple spatial axes present
|
|
474
752
|
# (i.e (az, el) for rectangular coordinate PSET),
|
|
475
753
|
# flatten them in the values array to match the raveled indices
|
|
476
|
-
|
|
477
|
-
|
|
754
|
+
non_spatial_axes_shape = tuple(
|
|
755
|
+
size
|
|
756
|
+
for key, size in pset_values.sizes.items()
|
|
757
|
+
if key not in pointing_set.spatial_coords
|
|
758
|
+
)
|
|
759
|
+
raveled_pset_data = pset_values.data.reshape(
|
|
760
|
+
*non_spatial_axes_shape,
|
|
761
|
+
pointing_set.num_points,
|
|
478
762
|
)
|
|
479
|
-
|
|
763
|
+
|
|
764
|
+
if value_key not in self.data_1d.data_vars:
|
|
480
765
|
# Initialize the map data array if it doesn't exist (values start at 0)
|
|
481
|
-
output_shape = (
|
|
482
|
-
self.
|
|
766
|
+
output_shape = (*raveled_pset_data.shape[:-1], self.num_points)
|
|
767
|
+
self.data_1d[value_key] = xr.DataArray(
|
|
768
|
+
np.zeros(output_shape),
|
|
769
|
+
dims=pointing_set.unwrapped_dims_dict[value_key],
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# Make coordinates for the map data array if they don't exist
|
|
773
|
+
self.data_1d.coords.update(
|
|
774
|
+
{
|
|
775
|
+
dim: pointing_set.data[dim]
|
|
776
|
+
for dim in self.data_1d[value_key].dims
|
|
777
|
+
if dim not in self.data_1d.coords
|
|
778
|
+
}
|
|
779
|
+
)
|
|
483
780
|
|
|
484
781
|
if index_match_method is IndexMatchMethod.PUSH:
|
|
782
|
+
# Bin the values at the matched indices. There may be multiple
|
|
783
|
+
# pointing set pixels that correspond to the same sky map pixel.
|
|
485
784
|
pointing_projected_values = map_utils.bin_single_array_at_indices(
|
|
486
785
|
value_array=raveled_pset_data,
|
|
487
|
-
projection_grid_shape=
|
|
488
|
-
len(self.sky_grid.az_bin_midpoints),
|
|
489
|
-
len(self.sky_grid.el_bin_midpoints),
|
|
490
|
-
),
|
|
786
|
+
projection_grid_shape=self.binning_grid_shape,
|
|
491
787
|
projection_indices=matched_indices_push,
|
|
492
788
|
)
|
|
789
|
+
elif index_match_method is IndexMatchMethod.PULL:
|
|
790
|
+
# We know that there will only be one value per sky map pixel,
|
|
791
|
+
# so we can use the matched indices directly
|
|
792
|
+
pointing_projected_values = raveled_pset_data[..., matched_indices_pull]
|
|
493
793
|
else:
|
|
494
794
|
raise NotImplementedError(
|
|
495
|
-
"
|
|
795
|
+
"Only PUSH and PULL index matching methods are supported."
|
|
496
796
|
)
|
|
497
|
-
|
|
797
|
+
|
|
798
|
+
# TODO: we may need to allow for unweighted/weighted means here by
|
|
799
|
+
# dividing pointing_projected_values by some binned weights.
|
|
800
|
+
# For unweighted means, we could use the number of pointing set pixels
|
|
801
|
+
# that correspond to each map pixel as the weights.
|
|
802
|
+
self.data_1d[value_key] += pointing_projected_values
|
|
803
|
+
|
|
804
|
+
@classmethod
|
|
805
|
+
def from_json(cls, json_path: str | Path) -> RectangularSkyMap | HealpixSkyMap:
|
|
806
|
+
"""
|
|
807
|
+
Create a SkyMap object from a JSON configuration file.
|
|
808
|
+
|
|
809
|
+
Parameters
|
|
810
|
+
----------
|
|
811
|
+
json_path : str | Path
|
|
812
|
+
Path to the JSON configuration file.
|
|
813
|
+
|
|
814
|
+
Returns
|
|
815
|
+
-------
|
|
816
|
+
RectangularSkyMap | HealpixSkyMap
|
|
817
|
+
An instance of a SkyMap object with the specified properties.
|
|
818
|
+
"""
|
|
819
|
+
with open(json_path) as f:
|
|
820
|
+
properties = json.load(f)
|
|
821
|
+
return cls.from_dict(properties)
|
|
822
|
+
|
|
823
|
+
@classmethod
|
|
824
|
+
def from_dict(cls, properties: dict) -> RectangularSkyMap | HealpixSkyMap:
|
|
825
|
+
"""
|
|
826
|
+
Create a SkyMap object from a dictionary of properties.
|
|
827
|
+
|
|
828
|
+
Parameters
|
|
829
|
+
----------
|
|
830
|
+
properties : dict
|
|
831
|
+
Dictionary containing the map properties. The required keys are:
|
|
832
|
+
- "spice_reference_frame" : str
|
|
833
|
+
The reference Spice frame of the map as a string. The available
|
|
834
|
+
options are defined in the spice geometry module:
|
|
835
|
+
`imap_processing.geometry.spice.SpiceFrame`. Example: "ECLIPJ2000".
|
|
836
|
+
- "sky_tiling_type" : str
|
|
837
|
+
The type of sky tiling, either "HEALPIX" or "RECTANGULAR".
|
|
838
|
+
- if "HEALPIX":
|
|
839
|
+
- "nside" : int
|
|
840
|
+
The nside parameter for the Healpix tessellation.
|
|
841
|
+
- "nested" : bool
|
|
842
|
+
Whether the Healpix tessellation is nested or not.
|
|
843
|
+
- if "RECTANGULAR":
|
|
844
|
+
- "spacing_deg" : float
|
|
845
|
+
The spacing of the rectangular grid in degrees.
|
|
846
|
+
- "values_to_push_project" : list[str], optional
|
|
847
|
+
The names of the variables to project to the map with the PUSH method.
|
|
848
|
+
NOTE: The projection is done by the instrument code, so this value can
|
|
849
|
+
only be used to inform that code. No values are projected automatically.
|
|
850
|
+
- "values_to_pull_project" : list[str], optional
|
|
851
|
+
The names of the variables to project to the map with the PULL method.
|
|
852
|
+
See the above note for more details.
|
|
853
|
+
|
|
854
|
+
See example dictionary in notes section.
|
|
855
|
+
|
|
856
|
+
Returns
|
|
857
|
+
-------
|
|
858
|
+
RectangularSkyMap | HealpixSkyMap
|
|
859
|
+
An instance of a SkyMap object with the specified properties.
|
|
860
|
+
|
|
861
|
+
Raises
|
|
862
|
+
------
|
|
863
|
+
ValueError
|
|
864
|
+
If the sky tiling type is not recognized.
|
|
865
|
+
|
|
866
|
+
Notes
|
|
867
|
+
-----
|
|
868
|
+
Example dictionary:
|
|
869
|
+
|
|
870
|
+
```python
|
|
871
|
+
properties = {
|
|
872
|
+
"spice_reference_frame": "ECLIPJ2000",
|
|
873
|
+
"sky_tiling_type": "HEALPIX",
|
|
874
|
+
"nside": 32,
|
|
875
|
+
"nested": False,
|
|
876
|
+
"values_to_push_project": ['counts', 'flux'],
|
|
877
|
+
"values_to_pull_project": []
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
"""
|
|
881
|
+
sky_tiling_type = SkyTilingType[properties["sky_tiling_type"].upper()]
|
|
882
|
+
spice_reference_frame = geometry.SpiceFrame[properties["spice_reference_frame"]]
|
|
883
|
+
|
|
884
|
+
skymap: RectangularSkyMap | HealpixSkyMap # Mypy gets confused by if/elif types
|
|
885
|
+
if sky_tiling_type is SkyTilingType.HEALPIX:
|
|
886
|
+
skymap = HealpixSkyMap(
|
|
887
|
+
nside=properties["nside"],
|
|
888
|
+
nested=properties["nested"],
|
|
889
|
+
spice_frame=spice_reference_frame,
|
|
890
|
+
)
|
|
891
|
+
elif sky_tiling_type is SkyTilingType.RECTANGULAR:
|
|
892
|
+
skymap = RectangularSkyMap(
|
|
893
|
+
spacing_deg=properties["spacing_deg"],
|
|
894
|
+
spice_frame=spice_reference_frame,
|
|
895
|
+
)
|
|
896
|
+
else:
|
|
897
|
+
raise ValueError(
|
|
898
|
+
f"Unknown sky tiling type: {sky_tiling_type}. "
|
|
899
|
+
f"Must be one of: {SkyTilingType.__members__.keys()}"
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
# Store requested variables to push/pull, which will be done by the instrument
|
|
903
|
+
# code which creates and uses the SkyMap object.
|
|
904
|
+
skymap.values_to_push_project = properties.get("values_to_push_project", [])
|
|
905
|
+
skymap.values_to_pull_project = properties.get("values_to_pull_project", [])
|
|
906
|
+
return skymap
|
|
907
|
+
|
|
908
|
+
def to_dict(self) -> dict:
|
|
909
|
+
"""
|
|
910
|
+
Convert the SkyMap object to a dictionary of properties.
|
|
911
|
+
|
|
912
|
+
Returns
|
|
913
|
+
-------
|
|
914
|
+
dict
|
|
915
|
+
Dictionary containing the map properties.
|
|
916
|
+
"""
|
|
917
|
+
if isinstance(self, HealpixSkyMap):
|
|
918
|
+
map_properties_dict = {
|
|
919
|
+
"sky_tiling_type": "HEALPIX",
|
|
920
|
+
"spice_reference_frame": self.spice_reference_frame.name,
|
|
921
|
+
"nside": self.nside,
|
|
922
|
+
"nested": self.nested,
|
|
923
|
+
}
|
|
924
|
+
elif isinstance(self, RectangularSkyMap):
|
|
925
|
+
map_properties_dict = {
|
|
926
|
+
"sky_tiling_type": "RECTANGULAR",
|
|
927
|
+
"spice_reference_frame": self.spice_reference_frame.name,
|
|
928
|
+
"spacing_deg": self.spacing_deg,
|
|
929
|
+
}
|
|
930
|
+
else:
|
|
931
|
+
raise ValueError(
|
|
932
|
+
f"Unknown SkyMap type: {self.__class__.__name__}. "
|
|
933
|
+
f"Must be one of: {AbstractSkyMap.__subclasses__()}"
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
map_properties_dict["values_to_push_project"] = (
|
|
937
|
+
self.values_to_push_project if self.values_to_push_project else []
|
|
938
|
+
)
|
|
939
|
+
map_properties_dict["values_to_pull_project"] = (
|
|
940
|
+
self.values_to_pull_project if self.values_to_pull_project else []
|
|
941
|
+
)
|
|
942
|
+
return map_properties_dict
|
|
943
|
+
|
|
944
|
+
def to_json(self, json_path: str | Path) -> None:
|
|
945
|
+
"""
|
|
946
|
+
Save the SkyMap object to a JSON configuration file.
|
|
947
|
+
|
|
948
|
+
Parameters
|
|
949
|
+
----------
|
|
950
|
+
json_path : str | Path
|
|
951
|
+
Path to the JSON file where the properties will be saved.
|
|
952
|
+
"""
|
|
953
|
+
with open(json_path, "w") as f:
|
|
954
|
+
json.dump(self.to_dict(), f, indent=4)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
class RectangularSkyMap(AbstractSkyMap):
|
|
958
|
+
"""
|
|
959
|
+
Map which tiles the sky with a 2D rectangular grid of azimuth/elevation pixels.
|
|
960
|
+
|
|
961
|
+
Parameters
|
|
962
|
+
----------
|
|
963
|
+
spacing_deg : float
|
|
964
|
+
The spacing of the rectangular grid in degrees.
|
|
965
|
+
spice_frame : geometry.SpiceFrame
|
|
966
|
+
The reference Spice frame of the map.
|
|
967
|
+
|
|
968
|
+
Notes
|
|
969
|
+
-----
|
|
970
|
+
Internally, the map is stored as a 1D array of pixels, and all data arrays
|
|
971
|
+
are stored with the final (-1) axis as the only spatial axis, representing the
|
|
972
|
+
pixel index in the 1D array (See Figs 1-2, which demonstrate the 1D pixel index
|
|
973
|
+
corresponding to the 2D grid of coordinates).
|
|
974
|
+
|
|
975
|
+
^ |15, 75|45, 75|75, 75|105, 75|...|255, 75|285, 75|315, 75|345, 75|
|
|
976
|
+
| |15, 45|45, 45|75, 45|105, 45|...|255, 45|285, 45|315, 45|345, 45|
|
|
977
|
+
| |15, 15|45, 15|75, 15|105, 15|...|255, 15|285, 15|315, 15|345, 15|
|
|
978
|
+
| |15, -15|45, -15|75, -15|105, -15|...|255, -15|285, -15|315, -15|345, -15|
|
|
979
|
+
| |15, -45|45, -45|75, -45|105, -45|...|255, -45|285, -45|315, -45|345, -45|
|
|
980
|
+
| |15, -75|45, -75|75, -75|105, -75|...|255, -75|285, -75|315, -75|345, -75|
|
|
981
|
+
|
|
|
982
|
+
---------------------------------------------------------------> Azimuth (degrees)
|
|
983
|
+
Elevation (degrees)
|
|
984
|
+
|
|
985
|
+
Fig. 1: Example of a rectangular grid of pixels in azimuth and elevation coordinates
|
|
986
|
+
in degrees, with a spacing of 30 degrees. There will be 12 azimuth bins and 6
|
|
987
|
+
elevation bins in this example, resulting in 72 pixels in the map.
|
|
988
|
+
|
|
989
|
+
A multidimensional value (e.g. counts, with energy levels at each pixel)
|
|
990
|
+
will be stored as a 2D array with the first axis as the energy dimension and the
|
|
991
|
+
second axis as the pixel index.
|
|
992
|
+
|
|
993
|
+
^ |5|11|17|23|29|35|41|47|53|59|65|71|
|
|
994
|
+
| |4|10|16|22|28|34|40|46|52|58|64|70|
|
|
995
|
+
| |3|9 |15|21|27|33|39|45|51|57|63|69|
|
|
996
|
+
| |2|8 |14|20|26|32|38|44|50|56|62|68|
|
|
997
|
+
| |1|7 |13|19|25|31|37|43|49|55|61|67|
|
|
998
|
+
| |0|6 |12|18|24|30|36|42|48|54|60|66|
|
|
999
|
+
---------------------------------------> Azimuth
|
|
1000
|
+
Elevation
|
|
1001
|
+
|
|
1002
|
+
Fig. 2: The 1D indices of the pixels in Fig. 1.
|
|
1003
|
+
Note that the indices are raveled from the 2D grid of (az, el) such that as one
|
|
1004
|
+
increases in pixel index, elevation increments first, then azimuth.
|
|
1005
|
+
"""
|
|
1006
|
+
|
|
1007
|
+
def __init__(
|
|
1008
|
+
self,
|
|
1009
|
+
spacing_deg: float,
|
|
1010
|
+
spice_frame: geometry.SpiceFrame,
|
|
1011
|
+
):
|
|
1012
|
+
# Define the core properties of the map:
|
|
1013
|
+
self.tiling_type = SkyTilingType.RECTANGULAR # Type of tiling of the sky
|
|
1014
|
+
|
|
1015
|
+
# The reference Spice frame of the map, in which angles are defined
|
|
1016
|
+
self.spice_reference_frame = spice_frame
|
|
1017
|
+
|
|
1018
|
+
# Initialize values to be used by the instrument code to push/pull
|
|
1019
|
+
self.values_to_push_project: list[str] = []
|
|
1020
|
+
self.values_to_pull_project: list[str] = []
|
|
1021
|
+
|
|
1022
|
+
# Angular spacing of the map grid (degrees) defines the number, size of pixels.
|
|
1023
|
+
self.spacing_deg = spacing_deg
|
|
1024
|
+
self.sky_grid = spatial_utils.AzElSkyGrid(
|
|
1025
|
+
spacing_deg=self.spacing_deg,
|
|
1026
|
+
)
|
|
1027
|
+
# The shape of the map (num_az_bins, num_el_bins) is used to bin the data
|
|
1028
|
+
self.binning_grid_shape = self.sky_grid.grid_shape
|
|
1029
|
+
|
|
1030
|
+
self.non_spatial_coords = {}
|
|
1031
|
+
self.spatial_coords = {
|
|
1032
|
+
CoordNames.AZIMUTH_L1C.value: xr.DataArray(
|
|
1033
|
+
self.sky_grid.az_bin_midpoints,
|
|
1034
|
+
dims=[CoordNames.AZIMUTH_L1C.value],
|
|
1035
|
+
attrs={"units": "degrees"},
|
|
1036
|
+
),
|
|
1037
|
+
CoordNames.ELEVATION_L1C.value: xr.DataArray(
|
|
1038
|
+
self.sky_grid.el_bin_midpoints,
|
|
1039
|
+
dims=[CoordNames.ELEVATION_L1C.value],
|
|
1040
|
+
attrs={"units": "degrees"},
|
|
1041
|
+
),
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
# Unwrap the az, el grids to 1D array of points tiling the sky
|
|
1045
|
+
az_points = self.sky_grid.az_grid.ravel()
|
|
1046
|
+
el_points = self.sky_grid.el_grid.ravel()
|
|
1047
|
+
|
|
1048
|
+
# Stack so axis 0 is different pixels, and axis 1 is (az, el) of the pixel
|
|
1049
|
+
self.az_el_points = np.column_stack((az_points, el_points))
|
|
1050
|
+
self.num_points = self.az_el_points.shape[0]
|
|
1051
|
+
|
|
1052
|
+
# Calculate solid angles of each pixel in the map grid in units of steradians
|
|
1053
|
+
self.solid_angle_grid = spatial_utils.build_solid_angle_map(
|
|
1054
|
+
spacing_deg=self.spacing_deg,
|
|
1055
|
+
)
|
|
1056
|
+
self.solid_angle_points = self.solid_angle_grid.ravel()
|
|
1057
|
+
|
|
1058
|
+
# Initialize xarray Dataset to store map data projected from pointing sets
|
|
1059
|
+
self.data_1d: xr.Dataset = xr.Dataset(
|
|
1060
|
+
coords={
|
|
1061
|
+
CoordNames.GENERIC_PIXEL.value: np.arange(self.num_points),
|
|
1062
|
+
}
|
|
1063
|
+
)
|
|
498
1064
|
|
|
499
1065
|
def __repr__(self) -> str:
|
|
500
1066
|
"""
|
|
@@ -506,14 +1072,405 @@ class RectangularSkyMap(AbstractSkyMap):
|
|
|
506
1072
|
String representation of the RectangularSkyMap.
|
|
507
1073
|
"""
|
|
508
1074
|
return (
|
|
509
|
-
"
|
|
1075
|
+
f"{self.__class__.__name__}\n\t(reference_frame="
|
|
510
1076
|
f"{self.spice_reference_frame.name} ({self.spice_reference_frame.value}), "
|
|
511
1077
|
f"spacing_deg={self.spacing_deg}, num_points={self.num_points})"
|
|
512
1078
|
)
|
|
513
1079
|
|
|
514
1080
|
|
|
515
|
-
|
|
516
|
-
|
|
1081
|
+
class HealpixSkyMap(AbstractSkyMap):
|
|
1082
|
+
"""
|
|
1083
|
+
Map which tiles the sky with a Healpix tessellation of equal-area pixels.
|
|
1084
|
+
|
|
1085
|
+
Parameters
|
|
1086
|
+
----------
|
|
1087
|
+
nside : int
|
|
1088
|
+
The nside parameter of the Healpix tessellation.
|
|
1089
|
+
spice_frame : geometry.SpiceFrame
|
|
1090
|
+
The reference Spice frame of the map.
|
|
1091
|
+
nested : bool, optional
|
|
1092
|
+
Whether the Healpix tessellation is nested. Default is False.
|
|
1093
|
+
"""
|
|
1094
|
+
|
|
1095
|
+
def __init__(
|
|
1096
|
+
self, nside: int, spice_frame: geometry.SpiceFrame, nested: bool = False
|
|
1097
|
+
):
|
|
1098
|
+
# Define the core properties of the map:
|
|
1099
|
+
self.tiling_type = SkyTilingType.HEALPIX
|
|
1100
|
+
self.spice_reference_frame = spice_frame
|
|
1101
|
+
|
|
1102
|
+
# Initialize values to be used by the instrument code to push/pull
|
|
1103
|
+
self.values_to_push_project: list[str] = []
|
|
1104
|
+
self.values_to_pull_project: list[str] = []
|
|
1105
|
+
|
|
1106
|
+
# Tile the sky with a Healpix tessellation. Defined by nside, nested parameters.
|
|
1107
|
+
self.nside = nside
|
|
1108
|
+
self.nested = nested
|
|
1109
|
+
|
|
1110
|
+
# Calculate how many pixels cover the sky and the approximate resolution (deg)
|
|
1111
|
+
self.num_points = hp.nside2npix(nside)
|
|
1112
|
+
self.approx_resolution = np.rad2deg(hp.nside2resol(nside, arcmin=False))
|
|
1113
|
+
# Define binning_grid_shape for consistency with RectangularSkyMap
|
|
1114
|
+
self.binning_grid_shape = (self.num_points,)
|
|
1115
|
+
self.spatial_coords = {
|
|
1116
|
+
CoordNames.HEALPIX_INDEX.value: xr.DataArray(
|
|
1117
|
+
np.arange(self.num_points),
|
|
1118
|
+
dims=[CoordNames.HEALPIX_INDEX.value],
|
|
1119
|
+
)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
# The centers of each pixel in the Healpix tessellation in azimuth (az) and
|
|
1123
|
+
# elevation (el) coordinates (degrees) within the map's Spice frame.
|
|
1124
|
+
pixel_az, pixel_el = hp.pix2ang(
|
|
1125
|
+
nside=nside, ipix=np.arange(self.num_points), nest=nested, lonlat=True
|
|
1126
|
+
)
|
|
1127
|
+
# Stack so axis 0 is different pixels, and axis 1 is (az, el) of the pixel
|
|
1128
|
+
self.az_el_points = np.column_stack((pixel_az, pixel_el))
|
|
1129
|
+
|
|
1130
|
+
# Tracks Per-Pixel Solid Angle in steradians.
|
|
1131
|
+
self.solid_angle = hp.nside2pixarea(nside, degrees=False)
|
|
1132
|
+
|
|
1133
|
+
# Solid angle is equal at all pixels, but define
|
|
1134
|
+
# solid_angle_points to be consistent with RectangularSkyMap
|
|
1135
|
+
self.solid_angle_points = np.full(self.num_points, self.solid_angle)
|
|
1136
|
+
|
|
1137
|
+
# Initialize xarray Dataset to store map data projected from pointing sets
|
|
1138
|
+
self.data_1d: xr.Dataset = xr.Dataset(
|
|
1139
|
+
coords={
|
|
1140
|
+
CoordNames.GENERIC_PIXEL.value: np.arange(self.num_points),
|
|
1141
|
+
}
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
# Define several methods for converting a Healpix map to a Rectangular map:
|
|
1145
|
+
def calculate_rect_pixel_value_from_healpix_map_n_subdivisions(
|
|
1146
|
+
self,
|
|
1147
|
+
rect_pix_center_lon_lat: np.typing.NDArray | tuple[float, float],
|
|
1148
|
+
rect_pix_spacing_deg: float,
|
|
1149
|
+
value_array: xr.DataArray,
|
|
1150
|
+
num_subdivisions: int,
|
|
1151
|
+
) -> np.typing.NDArray:
|
|
1152
|
+
"""
|
|
1153
|
+
Interpolate the value of a rectangular pixel from a healpix map w/ subdivisions.
|
|
1154
|
+
|
|
1155
|
+
This function splits a single rectangular pixel into smaller subpixels
|
|
1156
|
+
and calculates the solid angle weighted mean value of
|
|
1157
|
+
the healpix map at all of the subpixel centers.
|
|
1158
|
+
|
|
1159
|
+
Parameters
|
|
1160
|
+
----------
|
|
1161
|
+
rect_pix_center_lon_lat : np.typing.NDArray | tuple[float, float]
|
|
1162
|
+
The center longitude and latitude of the rectangular pixel.
|
|
1163
|
+
rect_pix_spacing_deg : float
|
|
1164
|
+
The spacing of the rectangular pixel in degrees.
|
|
1165
|
+
value_array : xr.DataArray
|
|
1166
|
+
The data array containing the healpix map values.
|
|
1167
|
+
num_subdivisions : int
|
|
1168
|
+
The number of subdivisions to create for the rectangular pixel.
|
|
1169
|
+
The more subdivisions, the more accurate the interpolation, but also
|
|
1170
|
+
the more computationally expensive it is.
|
|
1171
|
+
|
|
1172
|
+
Returns
|
|
1173
|
+
-------
|
|
1174
|
+
np.typing.NDArray
|
|
1175
|
+
The mean value of the healpix map at the subpixel centers.
|
|
1176
|
+
|
|
1177
|
+
If value_array has a single value at each pixel, the output
|
|
1178
|
+
will be a single value, but if there are other dimensions,
|
|
1179
|
+
(e.g., if self.data_1d['flux'].sizes =
|
|
1180
|
+
{"epoch": 1, "energy": 24, "pixel": 16200}),
|
|
1181
|
+
the output will be an array with the same dims except the pixel dimension
|
|
1182
|
+
(e.g., (1, 24)).
|
|
1183
|
+
"""
|
|
1184
|
+
# Assumes that you already checked the pixel doesn't fall entirely in an HP pix
|
|
1185
|
+
# TODO: Ask Nick if we need to add this here to mimic his code.
|
|
1186
|
+
# It shouldn't really be necessary, as the next function
|
|
1187
|
+
# get_pixel_value_recursive_subdivs will finish at 1 subdivision
|
|
1188
|
+
|
|
1189
|
+
# Ensure input contains lon in the first column and lat in the second column
|
|
1190
|
+
rect_pix_center_lon_lat = np.array(rect_pix_center_lon_lat).reshape(-1, 2)
|
|
1191
|
+
|
|
1192
|
+
# Calculate the number of subdivisions and the spacing of the subpixels
|
|
1193
|
+
# Then calculate the subpixel centers
|
|
1194
|
+
n_subpix_side = 2**num_subdivisions
|
|
1195
|
+
subpix_spacing = rect_pix_spacing_deg / n_subpix_side
|
|
1196
|
+
left_edge_lon = rect_pix_center_lon_lat[:, 0] - rect_pix_spacing_deg / 2
|
|
1197
|
+
bottom_edge_lat = rect_pix_center_lon_lat[:, 1] - rect_pix_spacing_deg / 2
|
|
1198
|
+
|
|
1199
|
+
rect_subpix_lon_ctrs = (
|
|
1200
|
+
left_edge_lon
|
|
1201
|
+
+ subpix_spacing * np.arange(n_subpix_side)
|
|
1202
|
+
+ subpix_spacing / 2
|
|
1203
|
+
)
|
|
1204
|
+
rect_subpix_lat_ctrs = (
|
|
1205
|
+
bottom_edge_lat
|
|
1206
|
+
+ subpix_spacing * np.arange(n_subpix_side)
|
|
1207
|
+
+ subpix_spacing / 2
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
# We must weight by solid angle, which is not exactly equal for all subpixels
|
|
1211
|
+
# Calculate the solid angle of the full rectangular pixel (sterad)
|
|
1212
|
+
full_rect_pixel_solid_angle = np.deg2rad(rect_pix_spacing_deg) * (
|
|
1213
|
+
np.sin(np.deg2rad(bottom_edge_lat + rect_pix_spacing_deg))
|
|
1214
|
+
- np.sin(np.deg2rad(bottom_edge_lat))
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
# Calculate solid angle of each subpix from the rect_subpix_lat_ctrs (sterad)
|
|
1218
|
+
all_edges_lat = bottom_edge_lat + np.arange(n_subpix_side + 1) * subpix_spacing
|
|
1219
|
+
sine_all_edges_lat = np.sin(np.deg2rad(all_edges_lat))
|
|
1220
|
+
rect_subpix_solid_angle_by_lat = np.diff(sine_all_edges_lat) * np.deg2rad(
|
|
1221
|
+
subpix_spacing
|
|
1222
|
+
)
|
|
1223
|
+
rect_subpix_solid_angle_by_lat = np.repeat(
|
|
1224
|
+
rect_subpix_solid_angle_by_lat[np.newaxis, :], n_subpix_side, axis=0
|
|
1225
|
+
).reshape(-1)
|
|
1226
|
+
|
|
1227
|
+
rect_subpix_ctrs = (
|
|
1228
|
+
np.array(
|
|
1229
|
+
np.meshgrid(rect_subpix_lon_ctrs, rect_subpix_lat_ctrs, indexing="ij")
|
|
1230
|
+
)
|
|
1231
|
+
.reshape(2, -1)
|
|
1232
|
+
.T
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
# Get the healpix pixel indices at the rectangular subpixel centers
|
|
1236
|
+
hp_pix_at_rect_subpix_ctrs = hp.ang2pix(
|
|
1237
|
+
nside=self.nside,
|
|
1238
|
+
nest=self.nested,
|
|
1239
|
+
theta=rect_subpix_ctrs[:, 0],
|
|
1240
|
+
phi=rect_subpix_ctrs[:, 1],
|
|
1241
|
+
lonlat=True,
|
|
1242
|
+
)
|
|
1243
|
+
# Get the healpix values at the rectangular subpixel centers
|
|
1244
|
+
hp_vals_at_rect_pix_ctrs = value_array.values[..., hp_pix_at_rect_subpix_ctrs]
|
|
1245
|
+
|
|
1246
|
+
# Weighted mean (weighted by solid angle) of these values over the pixel axis,
|
|
1247
|
+
# which is the last axis of this array
|
|
1248
|
+
weighted_hp_vals_at_rect_pix_ctrs = (
|
|
1249
|
+
hp_vals_at_rect_pix_ctrs * rect_subpix_solid_angle_by_lat
|
|
1250
|
+
)
|
|
1251
|
+
mean_pixel_value = (
|
|
1252
|
+
weighted_hp_vals_at_rect_pix_ctrs.sum(axis=-1) / full_rect_pixel_solid_angle
|
|
1253
|
+
)
|
|
1254
|
+
# Log the mean pixel value and the number of subdivisions for debugging
|
|
1255
|
+
logger.debug(
|
|
1256
|
+
f" Mean pixel value at Number of subdivisions: {num_subdivisions}: "
|
|
1257
|
+
f"array of shape {mean_pixel_value.shape}: {mean_pixel_value}"
|
|
1258
|
+
)
|
|
1259
|
+
return mean_pixel_value
|
|
1260
|
+
|
|
1261
|
+
def get_rect_pixel_value_recursive_subdivs(
|
|
1262
|
+
self,
|
|
1263
|
+
rect_pix_center_lon_lat: np.typing.NDArray | tuple[float, float],
|
|
1264
|
+
rect_pix_spacing_deg: float,
|
|
1265
|
+
value_array: xr.DataArray,
|
|
1266
|
+
*,
|
|
1267
|
+
rtol: float = 1e-3,
|
|
1268
|
+
atol: float = 1e-12,
|
|
1269
|
+
max_subdivision_depth: int = MAX_SUBDIV_RECURSION_DEPTH,
|
|
1270
|
+
) -> tuple[np.typing.NDArray, int]:
|
|
1271
|
+
"""
|
|
1272
|
+
Recursively subdivide a rectangular pixel to get a mean value within tolerances.
|
|
517
1273
|
|
|
518
|
-
|
|
519
|
-
|
|
1274
|
+
Takes a rectangular pixel, and recursively breaks it up into
|
|
1275
|
+
smaller and smaller subpixels, then calculates the solid-angle weighted mean
|
|
1276
|
+
of the healpix map's value at this pixel, until the difference
|
|
1277
|
+
between the mean values of two consecutive subdivisions is within the
|
|
1278
|
+
specified tolerances. The function returns the mean value at the final level
|
|
1279
|
+
of subdivision and the depth of recursion.
|
|
1280
|
+
|
|
1281
|
+
Parameters
|
|
1282
|
+
----------
|
|
1283
|
+
rect_pix_center_lon_lat : np.typing.NDArray | tuple[float, float]
|
|
1284
|
+
The center longitude and latitude of the rectangular pixel.
|
|
1285
|
+
rect_pix_spacing_deg : float
|
|
1286
|
+
The spacing of the rectangular pixel in degrees.
|
|
1287
|
+
value_array : xr.DataArray
|
|
1288
|
+
The data array containing the healpix map values to interpolate from.
|
|
1289
|
+
rtol : float, optional
|
|
1290
|
+
The relative tolerance for convergence, by default 1e-3.
|
|
1291
|
+
atol : float, optional
|
|
1292
|
+
The absolute tolerance for convergence, by default 1e-12.
|
|
1293
|
+
max_subdivision_depth : int, optional
|
|
1294
|
+
The maximum depth of recursion for subdivision,
|
|
1295
|
+
by default MAX_SUBDIV_RECURSION_DEPTH.
|
|
1296
|
+
Computation grows exponentially with depth, but only where the value
|
|
1297
|
+
has a significant gradient between adjacent healpix pixels.
|
|
1298
|
+
If the value is smooth, the recursion depth will be low.
|
|
1299
|
+
|
|
1300
|
+
Returns
|
|
1301
|
+
-------
|
|
1302
|
+
tuple[list[float], int]
|
|
1303
|
+
The mean value at the final level of subdivision and the depth of recursion.
|
|
1304
|
+
"""
|
|
1305
|
+
# Recursively subdivide a pixel and calculate its mean value until either the
|
|
1306
|
+
# difference between consecutive levels is within the specified tolerances
|
|
1307
|
+
# or the maximum recursion depth is reached
|
|
1308
|
+
depth = 0
|
|
1309
|
+
previous_mean_pixel_value: NDArray = np.full((1,), np.nan)
|
|
1310
|
+
while depth < max_subdivision_depth:
|
|
1311
|
+
mean_pixel_value = (
|
|
1312
|
+
self.calculate_rect_pixel_value_from_healpix_map_n_subdivisions(
|
|
1313
|
+
rect_pix_center_lon_lat=rect_pix_center_lon_lat,
|
|
1314
|
+
rect_pix_spacing_deg=rect_pix_spacing_deg,
|
|
1315
|
+
value_array=value_array,
|
|
1316
|
+
num_subdivisions=depth,
|
|
1317
|
+
)
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
# Determine if tolerance is met
|
|
1321
|
+
# (skip on the 0th iteration, as there's no delta)
|
|
1322
|
+
if depth > 0:
|
|
1323
|
+
# TODO: Ask Nick/Ultra Instrument team if we need to compare each value
|
|
1324
|
+
# in the pixel's array, or just the mean value.
|
|
1325
|
+
if np.isclose(
|
|
1326
|
+
mean_pixel_value.mean(),
|
|
1327
|
+
previous_mean_pixel_value.mean(),
|
|
1328
|
+
rtol=rtol,
|
|
1329
|
+
atol=atol,
|
|
1330
|
+
):
|
|
1331
|
+
break
|
|
1332
|
+
depth += 1
|
|
1333
|
+
previous_mean_pixel_value = mean_pixel_value
|
|
1334
|
+
|
|
1335
|
+
logger.debug(
|
|
1336
|
+
f"Pixel at ({rect_pix_center_lon_lat} deg size={rect_pix_spacing_deg} deg,)"
|
|
1337
|
+
f" converged to {mean_pixel_value.mean()} in {depth} subdivisions."
|
|
1338
|
+
f" Previous mean was {previous_mean_pixel_value.mean()}."
|
|
1339
|
+
)
|
|
1340
|
+
# Only keep the last (best) mean pixel value
|
|
1341
|
+
return mean_pixel_value, depth
|
|
1342
|
+
|
|
1343
|
+
def to_rectangular_skymap(
|
|
1344
|
+
self,
|
|
1345
|
+
rect_spacing_deg: float,
|
|
1346
|
+
value_keys: list[str],
|
|
1347
|
+
max_subdivision_depth: int = MAX_SUBDIV_RECURSION_DEPTH,
|
|
1348
|
+
) -> tuple[RectangularSkyMap, dict[str, np.typing.NDArray]]:
|
|
1349
|
+
"""
|
|
1350
|
+
Interpolate a healpix map to a rectangular map using recursive subdivision.
|
|
1351
|
+
|
|
1352
|
+
Parameters
|
|
1353
|
+
----------
|
|
1354
|
+
rect_spacing_deg : float
|
|
1355
|
+
The spacing of the rectangular map in degrees.
|
|
1356
|
+
value_keys : list[str]
|
|
1357
|
+
The names of the values to interpolate from the healpix map.
|
|
1358
|
+
Each must be independently interpolated because the subdivision depth
|
|
1359
|
+
depends on the gradient of the value between adjacent healpix pixels.
|
|
1360
|
+
max_subdivision_depth : int, optional
|
|
1361
|
+
The maximum depth of recursion for subdivision,
|
|
1362
|
+
by default MAX_SUBDIV_RECURSION_DEPTH.
|
|
1363
|
+
|
|
1364
|
+
Returns
|
|
1365
|
+
-------
|
|
1366
|
+
tuple[RectangularSkyMap, dict[str, np.typing.NDArray]]
|
|
1367
|
+
A RectangularSkyMap containing the interpolated values, and a dictionary of
|
|
1368
|
+
each value and its corresponding subdivision depth by pixel.
|
|
1369
|
+
"""
|
|
1370
|
+
# Begin by defining the rectangular map we want to create, which must be
|
|
1371
|
+
# in the same spice reference frame as the healpix map
|
|
1372
|
+
rect_map = RectangularSkyMap(
|
|
1373
|
+
spacing_deg=rect_spacing_deg,
|
|
1374
|
+
spice_frame=self.spice_reference_frame,
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
# Depending on the maximum recursion depth, the number of pixels in the
|
|
1378
|
+
# RectangularSkyMap, and the number of value keys, and especially on the
|
|
1379
|
+
# gradients of the values, the number of operations can be very large, so
|
|
1380
|
+
# log key information about the expected number of operations.
|
|
1381
|
+
approx_max_operations = (
|
|
1382
|
+
(4**max_subdivision_depth) * self.num_points * len(value_keys)
|
|
1383
|
+
)
|
|
1384
|
+
logger.info(
|
|
1385
|
+
f"Converting from a HealpixSkyMap(nside={self.nside}) to a "
|
|
1386
|
+
f"RectangularSkyMap(spacing_deg={rect_spacing_deg}) with recursive "
|
|
1387
|
+
"subdivision.\n The maximum recursion depth is "
|
|
1388
|
+
f"{max_subdivision_depth}, yielding a maximum number of healpix calls"
|
|
1389
|
+
f" of {approx_max_operations:.3e}."
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
# Dict to hold the subdivision depth by pixel for each value key
|
|
1393
|
+
subdiv_depth_dict = {}
|
|
1394
|
+
for value_key in value_keys:
|
|
1395
|
+
# For each of the values, calculate each pixel's value with
|
|
1396
|
+
# recursive subdivision. Unfortunately, this must be done independently
|
|
1397
|
+
# for each value key.
|
|
1398
|
+
|
|
1399
|
+
# Yields a list of tuple (mean_value, depth) for each pixel in the map
|
|
1400
|
+
healpix_values_array = self.data_1d[value_key]
|
|
1401
|
+
best_value_and_recursion_depth_by_pixel = [
|
|
1402
|
+
self.get_rect_pixel_value_recursive_subdivs(
|
|
1403
|
+
rect_pix_center_lon_lat=lon_lat,
|
|
1404
|
+
rect_pix_spacing_deg=rect_map.spacing_deg,
|
|
1405
|
+
value_array=healpix_values_array,
|
|
1406
|
+
max_subdivision_depth=max_subdivision_depth,
|
|
1407
|
+
)
|
|
1408
|
+
for lon_lat in rect_map.az_el_points
|
|
1409
|
+
]
|
|
1410
|
+
|
|
1411
|
+
# Separate the best value and the recursion depth for each pixel
|
|
1412
|
+
# into two lists, then convert both to numpy arrays
|
|
1413
|
+
# and move the pixel dim to the last dim of values
|
|
1414
|
+
interpolated_data_by_rect_pixel, subdiv_depth_of_value_by_pixel = zip(
|
|
1415
|
+
*best_value_and_recursion_depth_by_pixel
|
|
1416
|
+
)
|
|
1417
|
+
interpolated_data_by_rect_pixel = np.moveaxis(
|
|
1418
|
+
np.array(interpolated_data_by_rect_pixel), 0, -1
|
|
1419
|
+
)
|
|
1420
|
+
subdiv_depth_of_value_by_pixel = np.array(subdiv_depth_of_value_by_pixel)
|
|
1421
|
+
|
|
1422
|
+
# This can introduce an extra dim as the last dim of the array
|
|
1423
|
+
# to values with only one dimension
|
|
1424
|
+
if len(healpix_values_array.dims) == 1:
|
|
1425
|
+
interpolated_data_by_rect_pixel = np.squeeze(
|
|
1426
|
+
interpolated_data_by_rect_pixel,
|
|
1427
|
+
)
|
|
1428
|
+
|
|
1429
|
+
# Store the best value(s) of each pixel in the rectangular map with the
|
|
1430
|
+
# leading coordinates of the healpix map, and the pixel coordinate last
|
|
1431
|
+
rect_map.data_1d[value_key] = xr.DataArray(
|
|
1432
|
+
data=interpolated_data_by_rect_pixel,
|
|
1433
|
+
dims=(*healpix_values_array.dims[:-1], CoordNames.GENERIC_PIXEL.value),
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
# Update the coordinates of the rectangular map with any new coordinates
|
|
1437
|
+
# from the healpix map except the pixel coord,
|
|
1438
|
+
# which will be different in the rectangular map.
|
|
1439
|
+
for coord in healpix_values_array.coords:
|
|
1440
|
+
if coord not in (
|
|
1441
|
+
CoordNames.GENERIC_PIXEL.value,
|
|
1442
|
+
CoordNames.HEALPIX_INDEX.value,
|
|
1443
|
+
):
|
|
1444
|
+
rect_map.data_1d.coords[coord] = healpix_values_array.coords[coord]
|
|
1445
|
+
|
|
1446
|
+
# Add the subdivision depth by pixel of this value_key to the dictionary
|
|
1447
|
+
# This may be necessary for uncertainty estimation
|
|
1448
|
+
subdiv_depth_dict[value_key] = subdiv_depth_of_value_by_pixel
|
|
1449
|
+
logger.info(
|
|
1450
|
+
f"Summary of subdivision depth for {value_key}:\n"
|
|
1451
|
+
"Mean +/- std number of subdivisions for the "
|
|
1452
|
+
f"{rect_map.num_points} pixels of {value_key} is:\n"
|
|
1453
|
+
f" {np.mean(subdiv_depth_of_value_by_pixel):.6f}."
|
|
1454
|
+
f" +/- {np.std(subdiv_depth_of_value_by_pixel):.6f}.\n"
|
|
1455
|
+
"Min / Max number of subdivisions: \n"
|
|
1456
|
+
f" {np.min(subdiv_depth_of_value_by_pixel):.6f} / "
|
|
1457
|
+
f"{np.max(subdiv_depth_of_value_by_pixel):.6f}.\n"
|
|
1458
|
+
f"The maximum allowed depth is {max_subdivision_depth}."
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
return rect_map, subdiv_depth_dict
|
|
1462
|
+
|
|
1463
|
+
def __repr__(self) -> str:
|
|
1464
|
+
"""
|
|
1465
|
+
Return a string representation of the HealpixSkyMap.
|
|
1466
|
+
|
|
1467
|
+
Returns
|
|
1468
|
+
-------
|
|
1469
|
+
str
|
|
1470
|
+
String representation of the HealpixSkyMap.
|
|
1471
|
+
"""
|
|
1472
|
+
return (
|
|
1473
|
+
f"{self.__class__.__name__}\n\t(reference_frame="
|
|
1474
|
+
f"{self.spice_reference_frame.name} ({self.spice_reference_frame.value}), "
|
|
1475
|
+
f"nside={self.nside}, num_points={self.num_points})"
|
|
1476
|
+
)
|