Skip to content

Commit 768642b

Browse files
authored
1 parent 8278414 commit 768642b

2 files changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Tests Culling for ULTRA L1c."""
2+
3+
import astropy_healpix.healpy as hp
4+
import numpy as np
5+
import pytest
6+
import spiceypy
7+
8+
from imap_processing.spice.geometry import SpiceBody
9+
from imap_processing.ultra.l1c.ultra_l1c_culling import compute_culling_mask
10+
11+
12+
@pytest.mark.external_kernel
13+
@pytest.mark.usefixtures("_unset_metakernel_path")
14+
def test_compute_culling_mask(furnish_kernels, spice_test_data_path):
15+
"""Tests compute_culling_mask function."""
16+
17+
planet_radii_km = {
18+
"EARTH": 6378.137,
19+
}
20+
21+
kernels = [
22+
"imap_science_100.tf",
23+
"sim_1yr_imap_pointing_frame.bc",
24+
"imap_spk_demo.bsp",
25+
]
26+
27+
keepout_radius_km = 30 * planet_radii_km["EARTH"]
28+
29+
# Corresponds to 2025-11-28T00:00:00
30+
et_start = 817561854.185627
31+
et_end = 817644684.1856259
32+
step_seconds = 1800 # 30 minutes
33+
et_steps = np.arange(et_start, et_end, step_seconds)
34+
35+
spiceypy.kclear()
36+
37+
with furnish_kernels(kernels):
38+
mask, _ = compute_culling_mask(et_steps, keepout_radius_km)
39+
40+
assert mask.shape[0] == len(et_steps)
41+
assert mask.shape[1] == hp.nside2npix(128)
42+
43+
# Check that some pixels are masked out
44+
assert not np.all(mask)
45+
assert np.any(mask)
46+
47+
48+
@pytest.mark.external_kernel
49+
@pytest.mark.usefixtures("_unset_metakernel_path")
50+
def test_compare_sincpt_with_culling_mask_deterministic(furnish_kernels):
51+
"""Compare culling mask output for the closest-to-Earth pixel with sincpt."""
52+
53+
with furnish_kernels(
54+
[
55+
"imap_science_100.tf",
56+
"imap_sclk_0000.tsc",
57+
"sim_1yr_imap_pointing_frame.bc",
58+
"imap_spk_demo.bsp",
59+
"earth_1962_240827_2124_combined.bpc",
60+
"pck00011.tpc",
61+
"naif0012.tls",
62+
"de440s.bsp",
63+
]
64+
):
65+
et = np.array([817561854.185627])
66+
keepout_radius_km = 6378.1 # Earth radius
67+
nside = 128
68+
69+
# Compute culling mask and IMAP-to-Earth unit vector
70+
mask, unit_vectors = compute_culling_mask(
71+
et, keepout_radius_km, observer=SpiceBody.EARTH, nside=nside
72+
)
73+
74+
# Computes the 3D unit vectors pointing to the centers of all HEALPix pixels
75+
pixel_vecs_dps = np.column_stack(
76+
hp.pix2vec(nside, np.arange(hp.nside2npix(nside)), nest=False)
77+
)
78+
# Find the HEALPix pixel direction closest to the direction from IMAP to Earth
79+
closest_idx = np.argmax(np.dot(pixel_vecs_dps, unit_vectors[0]))
80+
81+
# Transform closest pixel vector to J2000
82+
rot_dps_to_j2000 = spiceypy.pxform("IMAP_DPS", "J2000", et[0])
83+
pixel_vec_j2000 = np.dot(rot_dps_to_j2000, pixel_vecs_dps[closest_idx])
84+
85+
with spiceypy.no_found_check():
86+
result = spiceypy.sincpt(
87+
method="ELLIPSOID",
88+
target="EARTH",
89+
et=et[0],
90+
fixref="IAU_EARTH",
91+
abcorr="NONE",
92+
obsrvr="IMAP",
93+
dref="J2000",
94+
dvec=pixel_vec_j2000,
95+
)
96+
97+
assert result[-1] # Check if the ray intersects Earth
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Culling for ULTRA L1c."""
2+
3+
import astropy_healpix.healpy as hp
4+
import numpy as np
5+
from numpy.typing import NDArray
6+
7+
from imap_processing.spice.geometry import (
8+
SpiceBody,
9+
SpiceFrame,
10+
imap_state,
11+
)
12+
13+
14+
def compute_culling_mask(
15+
et: NDArray,
16+
keepout_radius_km: float,
17+
observer: SpiceBody = SpiceBody.EARTH,
18+
nside: int = 128,
19+
nested: bool = False,
20+
) -> tuple[NDArray, NDArray]:
21+
"""
22+
Compute a mask for HEALPix pixels within a keep-out radius of the target body.
23+
24+
Parameters
25+
----------
26+
et : NDArray
27+
Ephemeris times in TDB seconds past J2000.
28+
keepout_radius_km : float
29+
Radius (in km) within which HEALPix pixels will be excluded.
30+
observer : SpiceBody, optional
31+
Body from which IMAP is observed.
32+
nside : int, optional
33+
HEALPix NSIDE resolution. Default is 128.
34+
nested : bool, optional
35+
Whether to use NESTED indexing.
36+
37+
Returns
38+
-------
39+
mask : tuple[NDArray, NDArray]
40+
Boolean array of shape (len(et), npix).
41+
unit_target_vecs : NDArray
42+
Unit vectors from IMAP to the target body
43+
(e.g., Earth), shape (len(et), 3).
44+
"""
45+
# Compute number of HEALPix pixels
46+
npix = hp.nside2npix(nside)
47+
48+
# Compute IMAP to Earth position in the pointing frame.
49+
state = imap_state(et, ref_frame=SpiceFrame.IMAP_DPS, observer=observer)
50+
# Flip to get vector from IMAP to Earth
51+
# position.shape = (len(et), 3)
52+
position = -state[:, :3]
53+
54+
# Distance from IMAP to target (e.g. Earth) (km):
55+
# distance.shape = (len(et),)
56+
distance = np.linalg.norm(position, axis=1) # shape (len(et),)
57+
58+
# Calculate the keepout angle (radians).
59+
# keepout_angle.shape = (len(et),)
60+
keepout_angle = np.arcsin(keepout_radius_km / distance) # radians
61+
62+
# Calculate the direction from IMAP to Earth. (shape: [N, 3])
63+
# unit_target_vecs.shape = (len(et), 3)
64+
unit_target_vecs = position / distance[:, np.newaxis]
65+
66+
# Get pixel unit vectors pointing from the center of the
67+
# HEALPix sphere to the center of each pixel on the sky.
68+
pixel_vecs = np.column_stack(
69+
hp.pix2vec(nside, np.arange(npix), nest=nested)
70+
) # shape: (npix, 3)
71+
72+
# Returns cos(theta) where theta is the separation angle between:
73+
# (1) vector from IMAP to Earth
74+
# (2) vector from IMAP to HEALPix pixel center
75+
# If theta is within the keepout angle, then the pixel is culled.
76+
cos_sep = np.dot(unit_target_vecs, pixel_vecs.T) # shape (N, npix)
77+
cos_sep = np.clip(cos_sep, -1.0, 1.0)
78+
# Get theta here.
79+
sep_angle = np.arccos(cos_sep)
80+
81+
# Exclude pixels within the keepout angle.
82+
# mask.shape = (len(et), npix)
83+
mask = sep_angle > keepout_angle[:, np.newaxis]
84+
85+
return mask, unit_target_vecs

0 commit comments

Comments
 (0)