Skip to content

Commit 2a7dda5

Browse files
committed
Extract reusable compute_pointing_directions from set_pointing_directions
Generalize the Lo L1C pointing-direction geometry so it can be reused by other products (e.g. the l1c quickmap) without duplicating the DPS az/el -> sky transform.
1 parent 0a6de2d commit 2a7dda5

2 files changed

Lines changed: 137 additions & 13 deletions

File tree

imap_processing/lo/l1c/lo_l1c.py

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,63 @@ def set_background_rates(
780780
return bg_rates_data, bg_stat_uncert_data, bg_sys_err_data
781781

782782

783+
def compute_pointing_directions(
784+
epoch: float,
785+
pivot_angle: float,
786+
spin_angles: np.ndarray | None = None,
787+
off_angles: np.ndarray | None = None,
788+
to_frame: SpiceFrame = SpiceFrame.IMAP_HAE,
789+
) -> np.ndarray:
790+
"""
791+
Transform a DPS spin/off-angle grid to sky longitude/latitude at an epoch.
792+
793+
For the given ``epoch`` the instrument despun (IMAP_DPS) spin-angle / off-angle
794+
grid is transformed into ``to_frame`` longitude/latitude using SPICE. Off-angles
795+
are measured relative to ``pivot_angle`` (elevation = 90 - pivot_angle + off).
796+
797+
This is the reusable geometry core shared by pointing-direction products: pass
798+
the desired spin/off-angle bin centers and destination frame.
799+
800+
Parameters
801+
----------
802+
epoch : float
803+
The epoch time in TTJ2000ns.
804+
pivot_angle : float
805+
The pivot angle in degrees.
806+
Off-angles are adjusted relative to this pivot angle before transformation.
807+
spin_angles : numpy.ndarray, optional
808+
Spin-angle bin centers in degrees. Defaults to ``SPIN_ANGLE_BIN_CENTERS``
809+
(the 3600-bin PSET grid).
810+
off_angles : numpy.ndarray, optional
811+
Off-angle bin centers in degrees. Defaults to ``OFF_ANGLE_BIN_CENTERS``
812+
(the 40-bin PSET grid).
813+
to_frame : SpiceFrame, optional
814+
Destination reference frame. Defaults to ``SpiceFrame.IMAP_HAE``.
815+
816+
Returns
817+
-------
818+
numpy.ndarray
819+
Array of shape ``(n_spin, n_off, 2)`` where ``[..., 0]`` is longitude and
820+
``[..., 1]`` is latitude, both in degrees in ``to_frame``.
821+
"""
822+
if spin_angles is None:
823+
spin_angles = SPIN_ANGLE_BIN_CENTERS
824+
if off_angles is None:
825+
off_angles = OFF_ANGLE_BIN_CENTERS
826+
827+
et = ttj2000ns_to_et(epoch)
828+
# create a meshgrid of spin and off angles using the bin centers
829+
spin, off = np.meshgrid(spin_angles, off_angles, indexing="ij")
830+
# off_angles need to account for the pivot_angle
831+
off = off + (90 - pivot_angle)
832+
dps_az_el = np.stack([spin, off], axis=-1)
833+
834+
# Transform from DPS Az/El to the destination frame's lon/lat
835+
return frame_transform_az_el(
836+
et, dps_az_el, SpiceFrame.IMAP_DPS, to_frame, degrees=True
837+
)
838+
839+
783840
def set_pointing_directions(
784841
epoch: float,
785842
attr_mgr: ImapCdfAttributes,
@@ -809,19 +866,7 @@ def set_pointing_directions(
809866
hae_latitude : xr.DataArray
810867
The HAE latitude for each spin and off angle bin.
811868
"""
812-
et = ttj2000ns_to_et(epoch)
813-
# create a meshgrid of spin and off angles using the bin centers
814-
spin, off = np.meshgrid(
815-
SPIN_ANGLE_BIN_CENTERS, OFF_ANGLE_BIN_CENTERS, indexing="ij"
816-
)
817-
# off_angles need to account for the pivot_angle
818-
off += 90 - pivot_angle
819-
dps_az_el = np.stack([spin, off], axis=-1)
820-
821-
# Transform from DPS Az/El to HAE lon/lat
822-
hae_az_el = frame_transform_az_el(
823-
et, dps_az_el, SpiceFrame.IMAP_DPS, SpiceFrame.IMAP_HAE, degrees=True
824-
)
869+
hae_az_el = compute_pointing_directions(epoch, pivot_angle)
825870

826871
return xr.DataArray(
827872
data=hae_az_el[np.newaxis, :, :, 0].astype(np.float64),

imap_processing/tests/lo/test_lo_l1c.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
PSET_SHAPE,
1515
FilterType,
1616
calculate_exposure_times,
17+
compute_pointing_directions,
1718
create_pset_counts,
1819
filter_goodtimes,
1920
lo_l1c,
2021
set_background_rates,
2122
set_pointing_directions,
2223
)
24+
from imap_processing.spice.geometry import SpiceFrame
2325
from imap_processing.spice.time import met_to_ttj2000ns
2426

2527

@@ -729,3 +731,80 @@ def test_set_pointing_directions_pivot_angle(attr_mgr, pivot_angle):
729731
# dps_az_el[:, :, 1] should have the adjusted off angles repeated across spin
730732
actual_off_angles = dps_az_el[0, :, 1] # Take first spin angle
731733
np.testing.assert_allclose(actual_off_angles, expected_off_angles, rtol=1e-10)
734+
735+
736+
def test_compute_pointing_directions_defaults():
737+
"""Default grid/frame reproduce the PSET (3600 x 40) IMAP_DPS->IMAP_HAE case."""
738+
mock_et = 123456789.0
739+
mock_az_el = np.stack(
740+
np.meshgrid(np.arange(3600), np.arange(40), indexing="ij"), axis=-1
741+
)
742+
with (
743+
patch("imap_processing.lo.l1c.lo_l1c.ttj2000ns_to_et") as mock_ttj2000ns_to_et,
744+
patch(
745+
"imap_processing.lo.l1c.lo_l1c.frame_transform_az_el"
746+
) as mock_frame_transform,
747+
):
748+
mock_ttj2000ns_to_et.return_value = mock_et
749+
mock_frame_transform.return_value = mock_az_el
750+
751+
result = compute_pointing_directions(1000000000.0, 90)
752+
753+
# Returns the raw (n_spin, n_off, 2) array, not a DataArray.
754+
assert result.shape == (3600, 40, 2)
755+
call_args = mock_frame_transform.call_args
756+
assert call_args[0][1].shape == (3600, 40, 2) # dps_az_el grid
757+
assert call_args[0][2] == SpiceFrame.IMAP_DPS # from_frame
758+
assert call_args[0][3] == SpiceFrame.IMAP_HAE # default to_frame
759+
760+
761+
def test_compute_pointing_directions_custom_grid_and_frame():
762+
"""Custom spin/off angles and destination frame are honored."""
763+
spin_angles = np.arange(3.0, 360.0, 6.0) # 60 bins
764+
off_angles = np.array([0.0]) # single pivot-cone off-angle
765+
pivot_angle = 75.0
766+
with (
767+
patch("imap_processing.lo.l1c.lo_l1c.ttj2000ns_to_et") as mock_ttj2000ns_to_et,
768+
patch(
769+
"imap_processing.lo.l1c.lo_l1c.frame_transform_az_el"
770+
) as mock_frame_transform,
771+
):
772+
mock_ttj2000ns_to_et.return_value = 123456789.0
773+
mock_frame_transform.side_effect = lambda et, az_el, *a, **k: az_el
774+
775+
result = compute_pointing_directions(
776+
1000000000.0,
777+
pivot_angle,
778+
spin_angles=spin_angles,
779+
off_angles=off_angles,
780+
to_frame=SpiceFrame.ECLIPJ2000,
781+
)
782+
783+
assert result.shape == (60, 1, 2)
784+
# Spin component matches the requested spin angles.
785+
np.testing.assert_allclose(result[:, 0, 0], spin_angles)
786+
# Off component is the single off-angle offset by (90 - pivot_angle).
787+
np.testing.assert_allclose(result[:, 0, 1], 90 - pivot_angle)
788+
# Destination frame is forwarded.
789+
assert mock_frame_transform.call_args[0][3] == SpiceFrame.ECLIPJ2000
790+
791+
792+
def test_set_pointing_directions_delegates(attr_mgr):
793+
"""set_pointing_directions wraps compute_pointing_directions output unchanged."""
794+
mock_az_el = np.stack(
795+
np.meshgrid(np.arange(3600), np.arange(40), indexing="ij"), axis=-1
796+
).astype(float)
797+
with patch(
798+
"imap_processing.lo.l1c.lo_l1c.compute_pointing_directions"
799+
) as mock_compute:
800+
mock_compute.return_value = mock_az_el
801+
802+
hae_longitude, hae_latitude = set_pointing_directions(
803+
1000000000.0, attr_mgr, 90
804+
)
805+
806+
mock_compute.assert_called_once_with(1000000000.0, 90)
807+
assert hae_longitude.dims == ("epoch", "spin_angle", "off_angle")
808+
assert hae_longitude.shape == (1, 3600, 40)
809+
np.testing.assert_array_equal(hae_longitude.values[0], mock_az_el[:, :, 0])
810+
np.testing.assert_array_equal(hae_latitude.values[0], mock_az_el[:, :, 1])

0 commit comments

Comments
 (0)