1111from imap_processing .ena_maps .ena_maps import HealpixSkyMap , RectangularSkyMap
1212from imap_processing .ena_maps .utils .naming import MapDescriptor
1313from imap_processing .hi .hi_l2 import (
14+ CALIBRATION_UNCERTAINTY_FRACTION ,
1415 _calculate_improved_stat_variance ,
1516 calculate_all_rates_and_intensities ,
1617 calculate_ena_intensity ,
@@ -184,7 +185,6 @@ def test_hi_l2(
184185 write_cdf (l2_dataset , istp = True )
185186
186187
187- @pytest .mark .external_test_data
188188@patch (
189189 "imap_processing.ena_maps.ena_maps.RectangularSkyMap.build_cdf_dataset" ,
190190 autospec = True ,
@@ -195,11 +195,12 @@ def test_hi_l2_uses_descriptor_to_setup_map(
195195 mock_create_sky_map_from_psets ,
196196 mock_calculate_all_rates_and_intensities ,
197197 mock_map_build_cdf_dataset ,
198- hi_l1_test_data_path ,
199198):
200- pset_path = hi_l1_test_data_path / "imap_hi_l1c_45sensor-pset_20250415_v999.cdf"
199+ """Test that hi_l2 uses the descriptor to set up the map correctly."""
200+ pset_path = "fake_pset.cdf" # Not used due to mocking
201201 descriptor_str = "h90-ena-h-sf-nsp-full-hnu-2deg-3mo"
202202 rect_map = MapDescriptor .from_string (descriptor_str ).to_empty_map ()
203+
203204 # create_sky_map_from_psets returns a dict with spin_phase key
204205 mock_create_sky_map_from_psets .return_value = {"full" : rect_map }
205206 # calculate_all_rates_and_intensities modifies and returns the map data
@@ -1236,6 +1237,7 @@ def test_cleanup_intermediate_variables():
12361237 ds = xr .Dataset (
12371238 {
12381239 "bg_rate" : xr .DataArray ([1 , 2 , 3 ], dims = ["x" ]),
1240+ "bg_rate_sys_err" : xr .DataArray ([0.1 , 0.2 , 0.3 ], dims = ["x" ]),
12391241 "energy_sc" : xr .DataArray ([4 , 5 , 6 ], dims = ["x" ]),
12401242 "ena_signal_rates" : xr .DataArray ([7 , 8 , 9 ], dims = ["x" ]),
12411243 "ena_signal_rate_stat_unc" : xr .DataArray ([0.1 , 0.2 , 0.3 ], dims = ["x" ]),
@@ -1248,6 +1250,7 @@ def test_cleanup_intermediate_variables():
12481250
12491251 # Intermediate variables should be removed
12501252 assert "bg_rate" not in result
1253+ assert "bg_rate_sys_err" not in result
12511254 assert "energy_sc" not in result
12521255 assert "ena_signal_rates" not in result
12531256 assert "ena_signal_rate_stat_unc" not in result
@@ -1664,3 +1667,75 @@ def test_combine_maps_handles_nan_sys_err(mock_sky_map_for_combine):
16641667 expected_sys_err_valid ,
16651668 decimal = 10 ,
16661669 )
1670+
1671+
1672+ # =============================================================================
1673+ # SYSTEMATIC UNCERTAINTY ALGORITHM TESTS
1674+ # =============================================================================
1675+
1676+
1677+ def test_calculate_ena_intensity_uses_bg_rate_sys_err (
1678+ ena_intensity_map_ds , anc_path_dict
1679+ ):
1680+ """Test that calculate_ena_intensity uses bg_rate_sys_err field.
1681+
1682+ The systematic uncertainty should be computed from the exposure-time
1683+ weighted average of bg_rate_sys_err, converted from rate to intensity.
1684+ This replaces the old algorithm that computed sqrt(bg_counts)/exposure.
1685+ """
1686+ descriptor_str = "h90-ena-h-sf-nsp-full-gcs-6deg-3mo"
1687+ map_descriptor = MapDescriptor .from_string (descriptor_str )
1688+
1689+ result_ds = calculate_ena_intensity (
1690+ ena_intensity_map_ds , anc_path_dict , map_descriptor
1691+ )
1692+
1693+ # Verify ena_intensity_sys_err was calculated
1694+ assert "ena_intensity_sys_err" in result_ds
1695+
1696+ # The sys_err should be based on bg_rate_sys_err / (geometric_factor * energy)
1697+ # After combine_calibration_products, it's combined in quadrature across cal prods
1698+ # We just verify it's finite and positive where expected
1699+ sys_err = result_ds ["ena_intensity_sys_err" ]
1700+ assert np .all (sys_err .values [np .isfinite (sys_err .values )] >= 0 )
1701+
1702+
1703+ def test_calculate_all_rates_adds_calibration_systematic (
1704+ mock_map_dataset_for_rates , anc_path_dict
1705+ ):
1706+ """Test that calculate_all_rates_and_intensities adds calibration systematic.
1707+
1708+ The final systematic uncertainty should include both:
1709+ 1. Background-associated systematic (from bg_rate_sys_err)
1710+ 2. Calibration systematic (22% of intensity)
1711+
1712+ These should be combined in quadrature.
1713+ """
1714+ descriptor = MapDescriptor .from_string ("h90-ena-h-sf-nsp-full-gcs-6deg-3mo" )
1715+
1716+ result_ds = calculate_all_rates_and_intensities (
1717+ mock_map_dataset_for_rates ,
1718+ anc_path_dict ,
1719+ descriptor ,
1720+ )
1721+
1722+ # Verify ena_intensity_sys_err includes calibration systematic
1723+ assert "ena_intensity_sys_err" in result_ds
1724+
1725+ # The sys_err should be larger than CALIBRATION_UNCERTAINTY_FRACTION * intensity
1726+ # because it includes both bg systematic and calibration systematic in quadrature
1727+ intensity = result_ds ["ena_intensity" ]
1728+ sys_err = result_ds ["ena_intensity_sys_err" ]
1729+
1730+ # Minimum expected sys_err is 22% of intensity (if bg systematic were zero)
1731+ min_expected = CALIBRATION_UNCERTAINTY_FRACTION * np .abs (intensity )
1732+
1733+ # Where both values are finite and intensity > 0, sys_err should be >= min_expected
1734+ valid_mask = np .isfinite (sys_err .values ) & np .isfinite (intensity .values )
1735+ valid_mask &= intensity .values > 0
1736+ if np .any (valid_mask ):
1737+ np .testing .assert_array_less (
1738+ min_expected .values [valid_mask ] - 1e-10 , # small tolerance
1739+ sys_err .values [valid_mask ],
1740+ err_msg = "Sys err should include calibration systematic (22% of intensity)" ,
1741+ )
0 commit comments