|
1 | 1 | """Comprehensive test suite for IMAP-Lo L2 data processing.""" |
2 | 2 |
|
| 3 | +from pathlib import Path |
3 | 4 | from unittest.mock import Mock, patch |
4 | 5 |
|
5 | 6 | import numpy as np |
|
23 | 24 | calculate_backgrounds, |
24 | 25 | calculate_bootstrap_corrections, |
25 | 26 | calculate_efficiency_corrected_quantities, |
| 27 | + calculate_flux_corrections, |
26 | 28 | calculate_intensities, |
27 | 29 | calculate_rates, |
28 | 30 | calculate_sputtering_corrections, |
@@ -532,6 +534,56 @@ def sample_dataset_with_bootstrap_data(): |
532 | 534 | return dataset |
533 | 535 |
|
534 | 536 |
|
| 537 | +@pytest.fixture |
| 538 | +def lo_flux_factors_file(): |
| 539 | + """Path to the LO flux factors test file.""" |
| 540 | + # Use the actual test data file from the ena_maps test data |
| 541 | + test_data_path = Path(__file__).parent.parent / "ena_maps" / "data" |
| 542 | + return test_data_path / "imap_lo_esa-eta-fit-factors_20240101_v001.csv" |
| 543 | + |
| 544 | + |
| 545 | +@pytest.fixture |
| 546 | +def sample_dataset_with_intensities(): |
| 547 | + """Create a dataset with intensities for flux correction testing.""" |
| 548 | + n_energy = 7 |
| 549 | + n_lon, n_lat = 6, 4 # Small for testing |
| 550 | + |
| 551 | + # Create realistic energy values matching the flux factors file |
| 552 | + energy_values = np.array([16.35, 30.56, 56.4, 105, 199.8, 407.5, 795.3]) |
| 553 | + |
| 554 | + coords = { |
| 555 | + "epoch": [8.1794907049e17], |
| 556 | + "energy": energy_values, |
| 557 | + "longitude": np.linspace(0, 360, n_lon, endpoint=False), |
| 558 | + "latitude": np.linspace(-90, 90, n_lat), |
| 559 | + } |
| 560 | + |
| 561 | + # Create intensity values with some spatial and energy structure |
| 562 | + intensity_values = np.ones((1, n_energy, n_lon, n_lat)) |
| 563 | + for i in range(n_energy): |
| 564 | + # Power law: I = I0 * (E/E0)^(-2.0) |
| 565 | + intensity_values[0, i, :, :] = 1e6 * (energy_values[i] / 100.0) ** (-2.0) |
| 566 | + |
| 567 | + # Add some spatial structure |
| 568 | + for j in range(n_lon): |
| 569 | + for k in range(n_lat): |
| 570 | + intensity_values[0, :, j, k] *= 1.0 + 0.1 * np.sin(j) * np.cos(k) |
| 571 | + |
| 572 | + dataset = xr.Dataset(coords=coords) |
| 573 | + dataset["ena_intensity"] = ( |
| 574 | + ("epoch", "energy", "longitude", "latitude"), |
| 575 | + intensity_values, |
| 576 | + ) |
| 577 | + |
| 578 | + # Add statistical uncertainties (10% of intensity) |
| 579 | + dataset["ena_intensity_stat_uncert"] = ( |
| 580 | + ("epoch", "energy", "longitude", "latitude"), |
| 581 | + intensity_values * 0.1, |
| 582 | + ) |
| 583 | + |
| 584 | + return dataset |
| 585 | + |
| 586 | + |
535 | 587 | # ============================================================================= |
536 | 588 | # UNIT TESTS FOR INDIVIDUAL FUNCTIONS |
537 | 589 | # ============================================================================= |
@@ -1002,6 +1054,129 @@ def test_calculate_backgrounds_zero_exposure(self): |
1002 | 1054 | assert np.all(np.isinf(result["bg_rates_stat_uncert"].values)) |
1003 | 1055 |
|
1004 | 1056 |
|
| 1057 | +class TestCalculateFluxCorrections: |
| 1058 | + """Tests for the calculate_flux_corrections function.""" |
| 1059 | + |
| 1060 | + def test_calculate_flux_corrections_basic( |
| 1061 | + self, sample_dataset_with_intensities, lo_flux_factors_file |
| 1062 | + ): |
| 1063 | + """Test basic flux correction calculation.""" |
| 1064 | + # Make a copy to avoid modifying the original fixture |
| 1065 | + original_dataset = sample_dataset_with_intensities.copy(deep=True) |
| 1066 | + |
| 1067 | + # Run flux correction |
| 1068 | + result = calculate_flux_corrections(original_dataset, lo_flux_factors_file) |
| 1069 | + |
| 1070 | + # Verify that the function returns a dataset |
| 1071 | + assert isinstance(result, xr.Dataset) |
| 1072 | + |
| 1073 | + # Verify that intensity variables are present |
| 1074 | + assert "ena_intensity" in result.data_vars |
| 1075 | + assert "ena_intensity_stat_uncert" in result.data_vars |
| 1076 | + |
| 1077 | + # Verify that data shape is preserved |
| 1078 | + original_shape = sample_dataset_with_intensities["ena_intensity"].shape |
| 1079 | + assert result["ena_intensity"].shape == original_shape |
| 1080 | + |
| 1081 | + # Check that corrections were applied by comparing to the original fixture |
| 1082 | + # (not the potentially modified copy) |
| 1083 | + original_intensity = sample_dataset_with_intensities["ena_intensity"].values |
| 1084 | + corrected_intensity = result["ena_intensity"].values |
| 1085 | + |
| 1086 | + # Check for meaningful differences |
| 1087 | + relative_diff = np.abs( |
| 1088 | + (corrected_intensity - original_intensity) / original_intensity |
| 1089 | + ) |
| 1090 | + max_relative_diff = np.max(relative_diff) |
| 1091 | + # Should have at least 10% change somewhere |
| 1092 | + assert max_relative_diff > 0.1, ( |
| 1093 | + f"Max relative difference was only {max_relative_diff}" |
| 1094 | + ) |
| 1095 | + |
| 1096 | + # Verify that uncertainties were also corrected |
| 1097 | + original_uncert = sample_dataset_with_intensities[ |
| 1098 | + "ena_intensity_stat_uncert" |
| 1099 | + ].values |
| 1100 | + corrected_uncert = result["ena_intensity_stat_uncert"].values |
| 1101 | + uncert_relative_diff = np.abs( |
| 1102 | + (corrected_uncert - original_uncert) / original_uncert |
| 1103 | + ) |
| 1104 | + max_uncert_diff = np.max(uncert_relative_diff) |
| 1105 | + # Should have at least 10% change in uncertainties too |
| 1106 | + assert max_uncert_diff > 0.1, ( |
| 1107 | + f"Max uncertainty relative difference was only {max_uncert_diff}" |
| 1108 | + ) |
| 1109 | + |
| 1110 | + def test_calculate_flux_corrections_preserves_other_vars( |
| 1111 | + self, sample_dataset_with_intensities, lo_flux_factors_file |
| 1112 | + ): |
| 1113 | + """Test that flux correction preserves other variables in the dataset.""" |
| 1114 | + # Add an extra variable to the dataset |
| 1115 | + sample_dataset_with_intensities["extra_var"] = (("energy",), np.ones(7)) |
| 1116 | + |
| 1117 | + result = calculate_flux_corrections( |
| 1118 | + sample_dataset_with_intensities, lo_flux_factors_file |
| 1119 | + ) |
| 1120 | + |
| 1121 | + # Verify that other variables are preserved |
| 1122 | + assert "extra_var" in result.data_vars |
| 1123 | + np.testing.assert_array_equal( |
| 1124 | + result["extra_var"].values, |
| 1125 | + sample_dataset_with_intensities["extra_var"].values, |
| 1126 | + ) |
| 1127 | + |
| 1128 | + def test_calculate_flux_corrections_energy_dimension_handling( |
| 1129 | + self, lo_flux_factors_file |
| 1130 | + ): |
| 1131 | + """Test that flux correction properly handles energy dimension reshaping.""" |
| 1132 | + # Create a dataset with different spatial dimensions |
| 1133 | + n_energy = 7 |
| 1134 | + n_x, n_y = 12, 8 # Different spatial dimensions |
| 1135 | + |
| 1136 | + energy_values = np.array([16.35, 30.56, 56.4, 105, 199.8, 407.5, 795.3]) |
| 1137 | + |
| 1138 | + coords = { |
| 1139 | + "epoch": [8.1794907049e17], |
| 1140 | + "energy": energy_values, |
| 1141 | + "x": np.arange(n_x), |
| 1142 | + "y": np.arange(n_y), |
| 1143 | + } |
| 1144 | + |
| 1145 | + # Create intensity values with energy-dependent structure (power law) |
| 1146 | + intensity_values = np.ones((1, n_energy, n_x, n_y)) |
| 1147 | + for i in range(n_energy): |
| 1148 | + intensity_values[0, i, :, :] = 1e6 * (energy_values[i] / 100.0) ** (-2.0) |
| 1149 | + uncert_values = intensity_values * 0.1 |
| 1150 | + |
| 1151 | + original_dataset = xr.Dataset(coords=coords) |
| 1152 | + original_dataset["ena_intensity"] = ( |
| 1153 | + ("epoch", "energy", "x", "y"), |
| 1154 | + intensity_values.copy(), |
| 1155 | + ) |
| 1156 | + original_dataset["ena_intensity_stat_uncert"] = ( |
| 1157 | + ("epoch", "energy", "x", "y"), |
| 1158 | + uncert_values.copy(), |
| 1159 | + ) |
| 1160 | + |
| 1161 | + # Run flux correction on a copy |
| 1162 | + dataset_copy = original_dataset.copy(deep=True) |
| 1163 | + result = calculate_flux_corrections(dataset_copy, lo_flux_factors_file) |
| 1164 | + |
| 1165 | + # Verify shape is preserved |
| 1166 | + assert result["ena_intensity"].shape == (1, n_energy, n_x, n_y) |
| 1167 | + assert result["ena_intensity_stat_uncert"].shape == (1, n_energy, n_x, n_y) |
| 1168 | + |
| 1169 | + # Verify corrections were applied by checking for meaningful differences |
| 1170 | + original_values = original_dataset["ena_intensity"].values |
| 1171 | + corrected_values = result["ena_intensity"].values |
| 1172 | + relative_diff = np.abs((corrected_values - original_values) / original_values) |
| 1173 | + max_relative_diff = np.max(relative_diff) |
| 1174 | + # Should have at least 10% change somewhere (flux corrections are significant) |
| 1175 | + assert max_relative_diff > 0.1, ( |
| 1176 | + f"Max relative difference was only {max_relative_diff}" |
| 1177 | + ) |
| 1178 | + |
| 1179 | + |
1005 | 1180 | class TestCalculateSputteringCorrections: |
1006 | 1181 | """Tests for the calculate_sputtering_corrections function.""" |
1007 | 1182 |
|
@@ -1970,11 +2145,13 @@ def test_calculate_all_rates_and_intensities_complete(self): |
1970 | 2145 | class TestIntegrationWithMocks: |
1971 | 2146 | """Integration tests using mocked external dependencies.""" |
1972 | 2147 |
|
1973 | | - def test_lo_l2_integration_minimal(self, minimal_pset_for_species): |
| 2148 | + def test_lo_l2_integration_minimal( |
| 2149 | + self, minimal_pset_for_species, lo_flux_factors_file |
| 2150 | + ): |
1974 | 2151 | """Test the main lo_l2 function with minimal mocking.""" |
1975 | 2152 | # Test with hydrogen data |
1976 | 2153 | sci_dependencies = {"imap_lo_l1c_pset": [minimal_pset_for_species]} |
1977 | | - anc_dependencies = [] |
| 2154 | + anc_dependencies = [lo_flux_factors_file] # Include flux factors file |
1978 | 2155 | descriptor = "l090-ena-h-sf-nsp-ram-hae-6deg-3mo" |
1979 | 2156 |
|
1980 | 2157 | # Mock the complex external dependencies to return simple results |
|
0 commit comments