Skip to content

Commit b29a2da

Browse files
authored
ULTRA L1C HELIO index maps (#2492)
* helio map func
1 parent a8a8a79 commit b29a2da

22 files changed

+5512
-338
lines changed

codecov.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ coverage:
1515
ignore:
1616
- "**/conftest.py"
1717
- "**/test_spacecraft_pset.py"
18+
- "**/test_helio_pset.py"
19+
- "**/*make_helio_index_maps.py"

imap_processing/tests/ultra/unit/conftest.py

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -599,23 +599,23 @@ def random_spin_data():
599599
@pytest.fixture
600600
def mock_spacecraft_pointing_lookups():
601601
"""Test lookup tables fixture."""
602+
np.random.seed(42)
602603
pix = hp.nside2npix(128) # reduced for testing
603604
steps = 2 # Reduced for testing
604-
for_indices_by_spin_phase = np.random.choice(
605-
[True, False], size=(pix, steps), p=[0.1, 0.9]
605+
for_indices_by_spin_phase = xr.DataArray(
606+
np.random.choice([True, False], size=(steps, pix), p=[0.1, 0.9]),
607+
dims=("spin_phase_step", "pixel"),
606608
)
607-
theta_vals = np.random.uniform(-60, 60, size=(pix, steps))
608-
phi_vals = np.random.uniform(-60, 60, size=(pix, steps))
609+
theta_vals = np.random.uniform(-60, 60, size=(steps, pix))
610+
phi_vals = np.random.uniform(-60, 60, size=(steps, pix))
609611
# Ra and Dec pixel shape needs to be the default healpix pixel count
610-
ra_and_dec = np.random.uniform(-80, 80, size=(pix, 2))
611-
boundary_scale_factors = np.ones((pix, steps))
612+
ra_and_dec = np.random.uniform(-80, 80, size=(steps, pix))
613+
boundary_scale_factors = np.ones((steps, pix))
614+
612615
with (
613616
mock.patch(
614617
"imap_processing.ultra.l1c.spacecraft_pset.get_spacecraft_pointing_lookup_tables"
615618
) as mock_lookup,
616-
mock.patch(
617-
"imap_processing.ultra.l1c.helio_pset.get_spacecraft_pointing_lookup_tables"
618-
) as mock_lookup_helio,
619619
):
620620
mock_lookup.return_value = (
621621
for_indices_by_spin_phase,
@@ -624,11 +624,60 @@ def mock_spacecraft_pointing_lookups():
624624
ra_and_dec,
625625
boundary_scale_factors,
626626
)
627-
mock_lookup_helio.return_value = (
628-
for_indices_by_spin_phase,
629-
theta_vals,
630-
phi_vals,
631-
ra_and_dec,
632-
boundary_scale_factors,
627+
yield mock_lookup
628+
629+
630+
@pytest.fixture
631+
def mock_helio_pointing_lookups():
632+
"""Test lookup tables fixture returning an xarray Dataset."""
633+
np.random.seed(42)
634+
pix = hp.nside2npix(32) # reduced for testing
635+
steps = 2 # Reduced for testing
636+
energy = 46
637+
638+
# Ra and Dec pixel shape needs to be the default healpix pixel count
639+
ra_and_dec = np.random.uniform(-80, 80, size=(steps, pix))
640+
641+
index_map = np.random.choice([True, False], size=(steps, energy, pix), p=[0.1, 0.9])
642+
index_map = index_map.astype(bool)
643+
theta_map = np.random.uniform(-60, 60, size=(steps, energy, pix))
644+
phi_map = np.random.uniform(-60, 60, size=(steps, energy, pix))
645+
bsf_map = np.ones((steps, energy, pix))
646+
with (
647+
mock.patch(
648+
"imap_processing.ultra.l1c.helio_pset.make_helio_index_maps_with_nominal_kernels"
649+
) as mock_lookup,
650+
):
651+
ds = xr.Dataset(
652+
data_vars={
653+
"index": (
654+
["spin_phase_step", "energy", "pixel"],
655+
index_map,
656+
{"long_name": "Pixel in FOV flag"},
657+
),
658+
"theta": (
659+
["spin_phase_step", "energy", "pixel"],
660+
theta_map,
661+
{"long_name": "Instrument theta angle", "units": "degrees"},
662+
),
663+
"phi": (
664+
["spin_phase_step", "energy", "pixel"],
665+
phi_map,
666+
{"long_name": "Instrument phi angle", "units": "degrees"},
667+
),
668+
"bsf": (
669+
["spin_phase_step", "energy", "pixel"],
670+
bsf_map,
671+
{"long_name": "Boundary scale factor", "units": "fractional"},
672+
),
673+
"ra_and_dec": (["spin_phase_step", "pixel"], ra_and_dec),
674+
},
675+
coords={
676+
"spin_phase_step": np.arange(steps),
677+
"energy": np.arange(energy),
678+
"pixel": np.arange(pix),
679+
},
633680
)
681+
mock_lookup.return_value = ds
682+
634683
yield mock_lookup
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from unittest import mock
2+
3+
import numpy as np
4+
import pandas as pd
5+
import pytest
6+
import xarray as xr
7+
8+
from imap_processing import imap_module_directory
9+
from imap_processing.cdf.utils import load_cdf
10+
from imap_processing.tests.conftest import _download_external_data
11+
from imap_processing.ultra.constants import UltraConstants
12+
from imap_processing.ultra.l1c.helio_pset import calculate_helio_pset
13+
14+
TEST_PATH = imap_module_directory / "tests" / "ultra" / "data" / "l1"
15+
16+
17+
@pytest.mark.skip(reason="Long running test for validation purposes.")
18+
def test_validate_exposure_time_and_sensitivities(
19+
ancillary_files, deadtime_datasets, imap_ena_sim_metakernel
20+
):
21+
"""Validates exposure time and sensitivities for ebin 0."""
22+
sens_filename = "SENS-IMAP_ULTRA_90-IMAP_DPS-HELIO-nside32-ebin0.csv"
23+
exposure_filename = "Exposures-IMAP_ULTRA_90-IMAP_DPS-HELIO-nside32-ebin0.csv"
24+
de_filename = "imap_ultra_l1b_90sensor-de_20000101-repoint00000_v000.cdf"
25+
test_data = [
26+
(sens_filename, "ultra/data/l1/"),
27+
(exposure_filename, "ultra/data/l1/"),
28+
(de_filename, "ultra/data/l1/"),
29+
]
30+
_download_external_data(test_data)
31+
l1b_de = TEST_PATH / de_filename
32+
l1b_de = load_cdf(l1b_de)
33+
sensitivities_ebin_0 = pd.read_csv(TEST_PATH / sens_filename)
34+
exposure_factor_ebin_0 = pd.read_csv(TEST_PATH / exposure_filename)
35+
36+
test_deadtimes = (
37+
pd.read_csv(TEST_PATH / "test_p0_ebin0_deadtimes.csv", header=None)
38+
.to_numpy()
39+
.squeeze()
40+
)
41+
npix = 12288 # nside 32
42+
# Create a minimal dataset to pass to the function
43+
dataset = xr.Dataset(
44+
{
45+
"spin_number": (["epoch"], np.array([1, 2, 3])),
46+
}
47+
)
48+
dataset.attrs["Repointing"] = "repoint00000"
49+
50+
pointing_range_met = (472374890.0, 582378000.0)
51+
# Create mock spin data that has 5525 nominal spins
52+
# Create DataFrame
53+
nspins = 5522
54+
nominal_spin_seconds = 15.0
55+
spin_data = pd.DataFrame(
56+
{
57+
"spin_start_met": np.linspace(
58+
pointing_range_met[0], pointing_range_met[1], nspins
59+
),
60+
"spin_period_sec": np.full(nspins, nominal_spin_seconds),
61+
"spin_phase_valid": np.ones(nspins),
62+
"spin_period_valid": np.ones(nspins),
63+
}
64+
)
65+
with (
66+
# Mock the pointing times
67+
mock.patch(
68+
"imap_processing.ultra.l1c.helio_pset.get_pointing_times_from_id",
69+
return_value=pointing_range_met,
70+
),
71+
mock.patch(
72+
"imap_processing.ultra.l1c.ultra_l1c_pset_bins.ttj2000ns_to_met",
73+
side_effect=lambda x: x,
74+
),
75+
# Mock deadtimes to be all ones
76+
mock.patch(
77+
"imap_processing.ultra.l1c.ultra_l1c_pset_bins."
78+
"get_deadtime_ratios_by_spin_phase",
79+
return_value=xr.DataArray(test_deadtimes, dims="spin_phase_step"),
80+
),
81+
# Mock spin data to match nominal spins in a pointing period
82+
mock.patch(
83+
"imap_processing.ultra.l1c.ultra_l1c_pset_bins.get_spin_data",
84+
return_value=spin_data,
85+
),
86+
# Mock background rates to be constant 0.1
87+
mock.patch(
88+
"imap_processing.ultra.l1c.helio_pset.get_spacecraft_background_rates",
89+
return_value=np.ones((46, npix)),
90+
),
91+
# Mock culling mask (no culling)
92+
mock.patch("imap_processing.ultra.l1c.helio_pset.compute_culling_mask"),
93+
):
94+
pset = calculate_helio_pset(
95+
l1b_de,
96+
dataset,
97+
deadtime_datasets["rates"],
98+
deadtime_datasets["params"],
99+
"imap_ultra_l1c_90sensor-heliopset",
100+
ancillary_files,
101+
90,
102+
UltraConstants.TOFXPH_SPECIES_GROUPS["proton"],
103+
)
104+
105+
# Validate exposure times for ebin 0
106+
exposure_times = pset["exposure_factor"][0, 0, :].values
107+
expected_exposure_times = exposure_factor_ebin_0["P0"].to_numpy()
108+
np.testing.assert_allclose(
109+
exposure_times,
110+
expected_exposure_times,
111+
atol=95, # TODO This is due to the helio index map differences
112+
err_msg="Exposure times do not match expected values for ebin 0.",
113+
)
114+
# Validate sensitivities for ebin 0
115+
sensitivity = pset["sensitivity"][0, :].values
116+
expected_sensitivity = sensitivities_ebin_0["Sensitivity (cm2)"].to_numpy()
117+
np.testing.assert_allclose(
118+
sensitivity,
119+
expected_sensitivity,
120+
atol=0.0006, # TODO This is due to the helio index map differences
121+
err_msg="Sensitivities times do not match expected values for ebin 0.",
122+
)

imap_processing/tests/ultra/unit/test_l1c_lookup_utils.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import astropy_healpix.healpy as hp
22
import numpy as np
33
import pytest
4+
import xarray as xr
45

56
from imap_processing.ultra.l1c.l1c_lookup_utils import (
7+
calculate_fwhm_spun_scattering,
68
get_scattering_thresholds_for_energy,
79
get_spacecraft_pointing_lookup_tables,
810
get_static_deadtime_ratios,
@@ -25,10 +27,10 @@ def test_get_spacecraft_pointing_lookup_tables(ancillary_files):
2527
# Test shapes
2628
# There should be 498 spin phase steps. In the real test files there will be 15000
2729
cols = 498
28-
assert for_indices_by_spin_phase.shape == (npix, cols)
29-
assert theta_vals.shape == (npix, cols)
30-
assert phi_vals.shape == (npix, cols)
31-
assert ra_and_dec.shape == (npix, 2)
30+
assert for_indices_by_spin_phase.shape == (cols, npix)
31+
assert theta_vals.shape == (cols, npix)
32+
assert phi_vals.shape == (cols, npix)
33+
assert ra_and_dec.shape == (2, npix)
3234

3335
# Value tests
3436
assert for_indices_by_spin_phase.dtype == bool
@@ -111,3 +113,47 @@ def test_get_static_deadtime_ratios(ancillary_files):
111113
np.testing.assert_array_equal(dt_ratio.shape, (721,))
112114
# Test the values
113115
assert np.all((dt_ratio >= 0.0) & (dt_ratio <= 1.0))
116+
117+
118+
def test_calculate_fwhm_spun_scattering(ancillary_files):
119+
"""Test calculate_fwhm_spun_scattering function."""
120+
# Make array with ones (we are only testing the shape here)
121+
for_pixels = np.ones((50, 10))
122+
theta_vals = np.ones((50, 10)) * 20 # All theta values are 20
123+
phi_vals = np.ones((50, 5)) * 15 # All phi
124+
with pytest.raises(ValueError, match="Shape mismatch"):
125+
calculate_fwhm_spun_scattering(
126+
for_pixels, theta_vals, phi_vals, ancillary_files, 45
127+
)
128+
129+
130+
@pytest.mark.external_test_data
131+
def test_calculate_fwhm_spun_scattering_reject(ancillary_files):
132+
"""Test calculate_fwhm_spun_scattering function."""
133+
nside = 8
134+
pix = hp.nside2npix(nside)
135+
steps = 5 # Reduced for testing
136+
energy_dim = 46
137+
np.random.seed(42)
138+
mock_theta = np.random.uniform(-60, 60, (steps, energy_dim, pix))
139+
mock_phi = np.random.uniform(-60, 60, (steps, energy_dim, pix))
140+
for_pixels = xr.DataArray(
141+
np.zeros((steps, energy_dim, pix)).astype(bool),
142+
dims=("spin_phase_step", "energy", "pixel"),
143+
)
144+
# Simulate first 100 pixels are in the FOR for all spin phases
145+
inside_inds = 100
146+
for_pixels[:, :, :inside_inds] = True
147+
valid_spun_pixels, fwhm_theta, fwhm_phi, thresholds = (
148+
calculate_fwhm_spun_scattering(
149+
for_pixels,
150+
mock_theta,
151+
mock_phi,
152+
ancillary_files,
153+
45,
154+
reject_scattering=True,
155+
)
156+
)
157+
assert valid_spun_pixels.shape == (steps, energy_dim, pix)
158+
# Check that some pixels are rejected
159+
assert not np.array_equal(valid_spun_pixels, for_pixels)

0 commit comments

Comments
 (0)