|
13 | 13 | any_good_direct_events, |
14 | 14 | compute_coincidence_type_and_tofs, |
15 | 15 | compute_hae_coordinates, |
| 16 | + de_ccsds_qf, |
16 | 17 | de_esa_energy_step, |
| 18 | + de_esa_step_met, |
17 | 19 | de_nominal_bin_and_spin_phase, |
18 | 20 | get_esa_to_esa_energy_step_lut, |
19 | 21 | housekeeping, |
|
23 | 25 | EsaEnergyStepLookupTable, |
24 | 26 | HiConstants, |
25 | 27 | ) |
| 28 | +from imap_processing.quality_flags import ImapHiL1bDeFlags |
26 | 29 | from imap_processing.spice.geometry import SpiceFrame |
27 | 30 |
|
28 | 31 |
|
@@ -66,7 +69,7 @@ def test_hi_annotate_direct_events( |
66 | 69 | l1b_datasets = annotate_direct_events(l1a_dataset, xr.Dataset(), esa_energies_csv) |
67 | 70 | assert len(l1b_datasets) == 1 |
68 | 71 | assert l1b_datasets[0].attrs["Logical_source"] == "imap_hi_l1b_45sensor-de" |
69 | | - assert len(l1b_datasets[0].data_vars) == 15 |
| 72 | + assert len(l1b_datasets[0].data_vars) == 17 |
70 | 73 |
|
71 | 74 |
|
72 | 75 | @pytest.mark.parametrize( |
@@ -128,7 +131,10 @@ def test_annotate_direct_events_with_hk( |
128 | 131 | l1b_datasets = annotate_direct_events(l1a_dataset, hk_dataset, esa_energies_csv) |
129 | 132 | assert len(l1b_datasets) == 1 |
130 | 133 | assert l1b_datasets[0].attrs["Logical_source"] == "imap_hi_l1b_90sensor-de" |
131 | | - assert len(l1b_datasets[0].data_vars) == 15 |
| 134 | + assert len(l1b_datasets[0].data_vars) == 17 |
| 135 | + # Verify new L1B variables exist |
| 136 | + assert "esa_step_met" in l1b_datasets[0].data_vars |
| 137 | + assert "ccsds_qf" in l1b_datasets[0].data_vars |
132 | 138 |
|
133 | 139 |
|
134 | 140 | @pytest.fixture |
@@ -640,3 +646,148 @@ def test_cal_data(self, hi_l1_test_data_path): |
640 | 646 | # Check the generated lookup table |
641 | 647 | # We expect 1 dataframe entry per esa step in the range [1, 9] |
642 | 648 | np.testing.assert_array_equal(lut.df["esa_step"].values, np.arange(9) + 1) |
| 649 | + |
| 650 | + |
| 651 | +class TestDeEsaStepMet: |
| 652 | + """Tests for de_esa_step_met function.""" |
| 653 | + |
| 654 | + def test_computes_esa_step_met(self): |
| 655 | + """Test that esa_step_met calculation from seconds and milliseconds.""" |
| 656 | + ds = xr.Dataset( |
| 657 | + coords={"epoch": [0, 1, 2], "event_met": [0.0, 1.0]}, |
| 658 | + data_vars={ |
| 659 | + "esa_step_seconds": ( |
| 660 | + ["epoch"], |
| 661 | + np.array([100, 200, 300], dtype=np.uint32), |
| 662 | + ), |
| 663 | + "esa_step_milliseconds": ( |
| 664 | + ["epoch"], |
| 665 | + np.array([500, 250, 750], dtype=np.uint16), |
| 666 | + ), |
| 667 | + "trigger_id": xr.DataArray( |
| 668 | + [1, 2], dims=["event_met"], attrs={"FILLVAL": 0} |
| 669 | + ), |
| 670 | + }, |
| 671 | + ) |
| 672 | + result = de_esa_step_met(ds) |
| 673 | + expected = np.array([100.5, 200.25, 300.75]) |
| 674 | + np.testing.assert_array_almost_equal(result["esa_step_met"].values, expected) |
| 675 | + |
| 676 | + |
| 677 | +class TestDeCcsdsQf: |
| 678 | + """Tests for de_ccsds_qf function.""" |
| 679 | + |
| 680 | + def test_packet_full_flag_set(self): |
| 681 | + """Test that PACKET_FULL flag is set for packets with 664 events.""" |
| 682 | + n_packets = 3 |
| 683 | + # Create events: packet 0 has 664 events, packet 1 has 100, packet 2 has 664 |
| 684 | + ccsds_indices = np.concatenate( |
| 685 | + [ |
| 686 | + np.zeros(664, dtype=np.uint16), # 664 events for packet 0 |
| 687 | + np.ones(100, dtype=np.uint16), # 100 events for packet 1 |
| 688 | + np.full(664, 2, dtype=np.uint16), # 664 events for packet 2 |
| 689 | + ] |
| 690 | + ) |
| 691 | + ds = xr.Dataset( |
| 692 | + coords={ |
| 693 | + "epoch": np.arange(n_packets), |
| 694 | + "event_met": np.arange(len(ccsds_indices), dtype=np.float64), |
| 695 | + }, |
| 696 | + data_vars={ |
| 697 | + "ccsds_index": (["event_met"], ccsds_indices), |
| 698 | + "trigger_id": xr.DataArray( |
| 699 | + np.ones(len(ccsds_indices), dtype=np.uint8), |
| 700 | + dims=["event_met"], |
| 701 | + attrs={"FILLVAL": 0}, |
| 702 | + ), |
| 703 | + }, |
| 704 | + ) |
| 705 | + result = de_ccsds_qf(ds) |
| 706 | + # Packet 0 and 2 should have PACKET_FULL flag (1), packet 1 should be 0 |
| 707 | + assert result["ccsds_qf"].values[0] == ImapHiL1bDeFlags.PACKET_FULL |
| 708 | + assert result["ccsds_qf"].values[1] == 0 |
| 709 | + assert result["ccsds_qf"].values[2] == ImapHiL1bDeFlags.PACKET_FULL |
| 710 | + |
| 711 | + def test_no_full_packets(self): |
| 712 | + """Test that no flags are set when no packets are full.""" |
| 713 | + n_packets = 2 |
| 714 | + ccsds_indices = np.concatenate( |
| 715 | + [ |
| 716 | + np.zeros(100, dtype=np.uint16), |
| 717 | + np.ones(200, dtype=np.uint16), |
| 718 | + ] |
| 719 | + ) |
| 720 | + ds = xr.Dataset( |
| 721 | + coords={ |
| 722 | + "epoch": np.arange(n_packets), |
| 723 | + "event_met": np.arange(len(ccsds_indices), dtype=np.float64), |
| 724 | + }, |
| 725 | + data_vars={ |
| 726 | + "ccsds_index": (["event_met"], ccsds_indices), |
| 727 | + "trigger_id": xr.DataArray( |
| 728 | + np.ones(len(ccsds_indices), dtype=np.uint8), |
| 729 | + dims=["event_met"], |
| 730 | + attrs={"FILLVAL": 0}, |
| 731 | + ), |
| 732 | + }, |
| 733 | + ) |
| 734 | + result = de_ccsds_qf(ds) |
| 735 | + assert result["ccsds_qf"].values[0] == 0 |
| 736 | + assert result["ccsds_qf"].values[1] == 0 |
| 737 | + |
| 738 | + def test_no_valid_direct_events_all_fill_trigger_id(self): |
| 739 | + """de_ccsds_qf returns all zeros when trigger_id is entirely FILLVAL.""" |
| 740 | + n_packets = 3 |
| 741 | + # Some arbitrary, in-range CCSDS indices that would normally map to packets |
| 742 | + ccsds_indices = np.array([0, 0, 1, 1, 2, 2, 0, 1, 2], dtype=np.uint16) |
| 743 | + n_events = len(ccsds_indices) |
| 744 | + # All trigger_id values are set to the FILLVAL (0), |
| 745 | + # meaning no valid direct events |
| 746 | + trigger_fillval = 0 |
| 747 | + ds = xr.Dataset( |
| 748 | + coords={ |
| 749 | + "epoch": np.arange(n_packets), |
| 750 | + "event_met": np.arange(n_events, dtype=np.float64), |
| 751 | + }, |
| 752 | + data_vars={ |
| 753 | + "ccsds_index": (["event_met"], ccsds_indices), |
| 754 | + "trigger_id": xr.DataArray( |
| 755 | + np.full(n_events, trigger_fillval, dtype=np.uint8), |
| 756 | + dims=["event_met"], |
| 757 | + attrs={"FILLVAL": trigger_fillval}, |
| 758 | + ), |
| 759 | + }, |
| 760 | + ) |
| 761 | + result = de_ccsds_qf(ds) |
| 762 | + # With no valid direct events, all CCSDS quality flags should be zero |
| 763 | + assert "ccsds_qf" in result |
| 764 | + assert result["ccsds_qf"].shape[0] == n_packets |
| 765 | + assert np.all(result["ccsds_qf"].values == 0) |
| 766 | + |
| 767 | + def test_ccsds_index_fillvals_ignored(self): |
| 768 | + """de_ccsds_qf returns all zeros when ccsds_index includes FILLVALs (65535).""" |
| 769 | + n_packets = 2 |
| 770 | + fillval = np.uint16(65535) |
| 771 | + # Include some events with CCSDS index FILLVAL that should be ignored |
| 772 | + ccsds_indices = np.array([fillval, fillval, 0, 0, 1, 1], dtype=np.uint16) |
| 773 | + n_events = len(ccsds_indices) |
| 774 | + ds = xr.Dataset( |
| 775 | + coords={ |
| 776 | + "epoch": np.arange(n_packets), |
| 777 | + "event_met": np.arange(n_events, dtype=np.float64), |
| 778 | + }, |
| 779 | + data_vars={ |
| 780 | + "ccsds_index": (["event_met"], ccsds_indices), |
| 781 | + "trigger_id": xr.DataArray( |
| 782 | + np.ones(n_events, dtype=np.uint8), |
| 783 | + dims=["event_met"], |
| 784 | + attrs={"FILLVAL": 0}, |
| 785 | + ), |
| 786 | + }, |
| 787 | + ) |
| 788 | + result = de_ccsds_qf(ds) |
| 789 | + # No packet reaches the full-packet threshold; |
| 790 | + # FILLVAL indices must not cause errors |
| 791 | + assert "ccsds_qf" in result |
| 792 | + assert result["ccsds_qf"].shape[0] == n_packets |
| 793 | + assert np.all(result["ccsds_qf"].values == 0) |
0 commit comments