|
1 | 1 | """Test coverage for imap_processing.hi.hi_goodtimes.py""" |
2 | 2 |
|
3 | 3 | import numpy as np |
| 4 | +import pandas as pd |
4 | 5 | import pytest |
5 | 6 | import xarray as xr |
6 | 7 |
|
|
10 | 11 | create_goodtimes_dataset, |
11 | 12 | mark_drf_times, |
12 | 13 | mark_incomplete_spin_sets, |
| 14 | + mark_overflow_packets, |
13 | 15 | ) |
14 | 16 |
|
15 | 17 |
|
@@ -1218,3 +1220,217 @@ def test_mark_drf_times_transition_at_end(self): |
1218 | 1220 | n_culled = np.sum(gt["cull_flags"].values[:, 0] == CullCode.LOOSE) |
1219 | 1221 | assert n_culled > 0 # Some should be culled |
1220 | 1222 | assert n_culled <= 31 # But not all (only last ~30 minutes) |
| 1223 | + |
| 1224 | + |
| 1225 | +class TestMarkOverflowPackets: |
| 1226 | + """Test suite for mark_overflow_packets function.""" |
| 1227 | + |
| 1228 | + @pytest.fixture |
| 1229 | + def mock_config_df(self): |
| 1230 | + """Create a mock calibration product configuration DataFrame.""" |
| 1231 | + # Create a minimal config with coincidence types |
| 1232 | + # ABC1C2 = 15, ABC1 = 14, AB = 12 |
| 1233 | + data = { |
| 1234 | + "coincidence_type_list": [("ABC1C2", "ABC1"), ("AB",)], |
| 1235 | + "tof_ab_low": [0, 0], |
| 1236 | + "tof_ab_high": [100, 100], |
| 1237 | + "tof_ac1_low": [0, 0], |
| 1238 | + "tof_ac1_high": [100, 100], |
| 1239 | + "tof_bc1_low": [-50, -50], |
| 1240 | + "tof_bc1_high": [50, 50], |
| 1241 | + "tof_c1c2_low": [0, 0], |
| 1242 | + "tof_c1c2_high": [100, 100], |
| 1243 | + } |
| 1244 | + df = pd.DataFrame( |
| 1245 | + data, |
| 1246 | + index=pd.MultiIndex.from_tuples( |
| 1247 | + [(1, 1), (2, 1)], names=["calibration_prod", "esa_energy_step"] |
| 1248 | + ), |
| 1249 | + ) |
| 1250 | + # Add coincidence_type_values column (converted from strings to ints) |
| 1251 | + # ABC1C2=15, ABC1=14, AB=12 |
| 1252 | + df["coincidence_type_values"] = [(15, 14), (12,)] |
| 1253 | + return df |
| 1254 | + |
| 1255 | + @pytest.fixture |
| 1256 | + def mock_goodtimes(self): |
| 1257 | + """Create a mock goodtimes dataset.""" |
| 1258 | + met_values = np.arange(1000.0, 1100.0, 10.0) |
| 1259 | + return xr.Dataset( |
| 1260 | + { |
| 1261 | + "cull_flags": xr.DataArray( |
| 1262 | + np.zeros((len(met_values), 90), dtype=np.uint8), |
| 1263 | + dims=["met", "spin_bin"], |
| 1264 | + ), |
| 1265 | + "esa_step": xr.DataArray( |
| 1266 | + np.ones(len(met_values), dtype=np.uint8), dims=["met"] |
| 1267 | + ), |
| 1268 | + }, |
| 1269 | + coords={"met": met_values, "spin_bin": np.arange(90)}, |
| 1270 | + attrs={"sensor": "Hi45", "pointing": 1}, |
| 1271 | + ) |
| 1272 | + |
| 1273 | + def test_no_full_packets(self, mock_goodtimes, mock_config_df): |
| 1274 | + """Test that no culling occurs when no packets are full.""" |
| 1275 | + # Create L1B DE with packets having < 664 events |
| 1276 | + n_events = 100 |
| 1277 | + l1b_de = xr.Dataset( |
| 1278 | + { |
| 1279 | + "ccsds_index": (["event_met"], np.zeros(n_events, dtype=np.uint16)), |
| 1280 | + "coincidence_type": ( |
| 1281 | + ["event_met"], |
| 1282 | + np.full(n_events, 15, dtype=np.uint8), |
| 1283 | + ), |
| 1284 | + }, |
| 1285 | + coords={"event_met": np.linspace(1000.0, 1010.0, n_events)}, |
| 1286 | + ) |
| 1287 | + |
| 1288 | + mark_overflow_packets(mock_goodtimes, l1b_de, mock_config_df) |
| 1289 | + |
| 1290 | + # No times should be culled |
| 1291 | + assert np.all(mock_goodtimes["cull_flags"].values == 0) |
| 1292 | + |
| 1293 | + def test_full_packet_with_qualified_event(self, mock_goodtimes, mock_config_df): |
| 1294 | + """Test that full packet with qualified final event is culled.""" |
| 1295 | + # Create L1B DE with one packet having exactly 664 events |
| 1296 | + n_events = 664 |
| 1297 | + event_mets = np.linspace(1005.0, 1006.0, n_events) |
| 1298 | + l1b_de = xr.Dataset( |
| 1299 | + { |
| 1300 | + "ccsds_index": (["event_met"], np.zeros(n_events, dtype=np.uint16)), |
| 1301 | + # Final event has coincidence_type=15 (ABC1C2), which is qualified |
| 1302 | + "coincidence_type": ( |
| 1303 | + ["event_met"], |
| 1304 | + np.full(n_events, 15, dtype=np.uint8), |
| 1305 | + ), |
| 1306 | + }, |
| 1307 | + coords={"event_met": event_mets}, |
| 1308 | + ) |
| 1309 | + |
| 1310 | + mark_overflow_packets(mock_goodtimes, l1b_de, mock_config_df) |
| 1311 | + |
| 1312 | + # MET ~1006 should be culled (maps to goodtimes MET 1000) |
| 1313 | + # The MET 1000 bin should have all spin bins culled |
| 1314 | + assert mock_goodtimes["cull_flags"].values[0, :].sum() == 90 |
| 1315 | + |
| 1316 | + def test_full_packet_with_unqualified_event(self, mock_goodtimes, mock_config_df): |
| 1317 | + """Test that full packet with unqualified final event is NOT culled.""" |
| 1318 | + # Create L1B DE with one packet having exactly 664 events |
| 1319 | + n_events = 664 |
| 1320 | + event_mets = np.linspace(1005.0, 1006.0, n_events) |
| 1321 | + l1b_de = xr.Dataset( |
| 1322 | + { |
| 1323 | + "ccsds_index": (["event_met"], np.zeros(n_events, dtype=np.uint16)), |
| 1324 | + # Final event has coincidence_type=3 (not in any cal product) |
| 1325 | + "coincidence_type": ( |
| 1326 | + ["event_met"], |
| 1327 | + np.full(n_events, 3, dtype=np.uint8), |
| 1328 | + ), |
| 1329 | + }, |
| 1330 | + coords={"event_met": event_mets}, |
| 1331 | + ) |
| 1332 | + |
| 1333 | + mark_overflow_packets(mock_goodtimes, l1b_de, mock_config_df) |
| 1334 | + |
| 1335 | + # No times should be culled since final event is unqualified |
| 1336 | + assert np.all(mock_goodtimes["cull_flags"].values == 0) |
| 1337 | + |
| 1338 | + def test_multiple_full_packets(self, mock_goodtimes, mock_config_df): |
| 1339 | + """Test handling of multiple full packets.""" |
| 1340 | + # Create L1B DE with two packets, each having 664 events |
| 1341 | + n_events_per_packet = 664 |
| 1342 | + n_packets = 2 |
| 1343 | + |
| 1344 | + ccsds_indices = np.concatenate( |
| 1345 | + [np.full(n_events_per_packet, i, dtype=np.uint16) for i in range(n_packets)] |
| 1346 | + ) |
| 1347 | + # Packet 0: final event qualified (15) |
| 1348 | + # Packet 1: final event unqualified (3) |
| 1349 | + coincidence_types = np.concatenate( |
| 1350 | + [ |
| 1351 | + np.concatenate( |
| 1352 | + [np.full(n_events_per_packet - 1, 3, dtype=np.uint8), [15]] |
| 1353 | + ), |
| 1354 | + np.full(n_events_per_packet, 3, dtype=np.uint8), |
| 1355 | + ] |
| 1356 | + ) |
| 1357 | + event_mets = np.concatenate( |
| 1358 | + [ |
| 1359 | + np.linspace(1005.0, 1006.0, n_events_per_packet), # Packet 0 |
| 1360 | + np.linspace(1015.0, 1016.0, n_events_per_packet), # Packet 1 |
| 1361 | + ] |
| 1362 | + ) |
| 1363 | + |
| 1364 | + l1b_de = xr.Dataset( |
| 1365 | + { |
| 1366 | + "ccsds_index": (["event_met"], ccsds_indices), |
| 1367 | + "coincidence_type": (["event_met"], coincidence_types), |
| 1368 | + }, |
| 1369 | + coords={"event_met": event_mets}, |
| 1370 | + ) |
| 1371 | + |
| 1372 | + mark_overflow_packets(mock_goodtimes, l1b_de, mock_config_df) |
| 1373 | + |
| 1374 | + # Only packet 0's MET should be culled (MET 1000) |
| 1375 | + # Packet 1 has unqualified final event, so MET 1010 should not be culled |
| 1376 | + assert np.sum(mock_goodtimes["cull_flags"].values[0, :] > 0) == 90 # All bins |
| 1377 | + assert np.all(mock_goodtimes["cull_flags"].values[1, :] == 0) # MET 1010 |
| 1378 | + |
| 1379 | + def test_empty_de_data(self, mock_goodtimes, mock_config_df): |
| 1380 | + """Test handling of empty L1B DE data.""" |
| 1381 | + l1b_de = xr.Dataset( |
| 1382 | + { |
| 1383 | + "ccsds_index": (["event_met"], np.array([], dtype=np.uint16)), |
| 1384 | + "coincidence_type": (["event_met"], np.array([], dtype=np.uint8)), |
| 1385 | + }, |
| 1386 | + coords={"event_met": np.array([])}, |
| 1387 | + ) |
| 1388 | + |
| 1389 | + # Should not raise, just return without culling |
| 1390 | + mark_overflow_packets(mock_goodtimes, l1b_de, mock_config_df) |
| 1391 | + assert np.all(mock_goodtimes["cull_flags"].values == 0) |
| 1392 | + |
| 1393 | + def test_custom_cull_code(self, mock_goodtimes, mock_config_df): |
| 1394 | + """Test using a custom cull code.""" |
| 1395 | + n_events = 664 |
| 1396 | + event_mets = np.linspace(1005.0, 1006.0, n_events) |
| 1397 | + l1b_de = xr.Dataset( |
| 1398 | + { |
| 1399 | + "ccsds_index": (["event_met"], np.zeros(n_events, dtype=np.uint16)), |
| 1400 | + "coincidence_type": ( |
| 1401 | + ["event_met"], |
| 1402 | + np.concatenate([np.full(n_events - 1, 3, dtype=np.uint8), [15]]), |
| 1403 | + ), |
| 1404 | + }, |
| 1405 | + coords={"event_met": event_mets}, |
| 1406 | + ) |
| 1407 | + |
| 1408 | + custom_cull = 5 |
| 1409 | + mark_overflow_packets( |
| 1410 | + mock_goodtimes, l1b_de, mock_config_df, cull_code=custom_cull |
| 1411 | + ) |
| 1412 | + |
| 1413 | + # Check that the custom cull code was used |
| 1414 | + assert np.any(mock_goodtimes["cull_flags"].values == custom_cull) |
| 1415 | + |
| 1416 | + def test_final_event_is_last_in_list(self, mock_goodtimes, mock_config_df): |
| 1417 | + """Test that the final event is the last one in the list for the packet.""" |
| 1418 | + n_events = 664 |
| 1419 | + event_mets = np.linspace(1005.0, 1006.0, n_events) |
| 1420 | + |
| 1421 | + # All events have unqualified type except the last one in the list |
| 1422 | + coincidence_types = np.full(n_events, 3, dtype=np.uint8) |
| 1423 | + coincidence_types[-1] = 12 # Last event is qualified |
| 1424 | + |
| 1425 | + l1b_de = xr.Dataset( |
| 1426 | + { |
| 1427 | + "ccsds_index": (["event_met"], np.zeros(n_events, dtype=np.uint16)), |
| 1428 | + "coincidence_type": (["event_met"], coincidence_types), |
| 1429 | + }, |
| 1430 | + coords={"event_met": event_mets}, |
| 1431 | + ) |
| 1432 | + |
| 1433 | + mark_overflow_packets(mock_goodtimes, l1b_de, mock_config_df) |
| 1434 | + |
| 1435 | + # Should be culled because the final event (last in list) is qualified |
| 1436 | + assert np.sum(mock_goodtimes["cull_flags"].values > 0) > 0 |
0 commit comments