Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions imap_processing/hi/hi_l1b.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,9 @@ def compute_hae_coordinates(dataset: xr.Dataset) -> dict[str, xr.DataArray]:
pointing_coordinates = instrument_pointing(
et, SpiceFrame[f"IMAP_HI_{sensor_number}"], SpiceFrame.ECLIPJ2000
)
new_vars["hae_latitude"].values = pointing_coordinates[:, 0]
new_vars["hae_longitude"].values = pointing_coordinates[:, 1]
# instrument_pointing returns (longitude, latitude)
new_vars["hae_longitude"].values = pointing_coordinates[:, 0]
new_vars["hae_latitude"].values = pointing_coordinates[:, 1]

return new_vars

Expand Down
15 changes: 7 additions & 8 deletions imap_processing/spice/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"""

import logging
import typing
from enum import IntEnum

import numpy as np
Expand Down Expand Up @@ -457,9 +456,10 @@ def instrument_pointing(
"""
Compute the instrument pointing at the specified times.

By default, the coordinates returned are (Longitude, Latitude) coordinates in
the reference frame `to_frame`. Cartesian coordinates can be returned if
desired by setting `cartesian=True`.
By default, the coordinates returned are (Longitude, Latitude), in degrees, in
the reference frame `to_frame`. Longitude is in the range [0, 360) and
latitude is in the range [-90, 90]. Cartesian coordinates can be returned
if desired by setting `cartesian=True`.

Parameters
----------
Expand All @@ -476,14 +476,13 @@ def instrument_pointing(
Returns
-------
pointing : np.ndarray
The instrument pointing at the specified times.
The instrument pointing at the specified times. Shape is (..., 2) for
(Longitude, Latitude) or (..., 3) if `cartesian=True`.
"""
pointing = frame_transform(et, BORESIGHT_LOOKUP[instrument], instrument, to_frame)
if cartesian:
return pointing
if isinstance(et, typing.Collection):
return np.rad2deg([spiceypy.reclat(vec)[1:] for vec in pointing])
return np.rad2deg(spiceypy.reclat(pointing)[1:])
return cartesian_to_spherical(pointing, degrees=True)[..., 1:3]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change to instrument_pointing when cartesian=False. Searching the repo, the only usage with cartesian=False was in the hi_l1b.py module. Two other uses of instrument_pointing all pass cartesian=True:



def basis_vectors(
Expand Down
20 changes: 14 additions & 6 deletions imap_processing/tests/hi/test_hi_l1b.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,18 @@ def test_de_nominal_bin_and_spin_phase(
np.testing.assert_array_equal(new_vars["nominal_bin"].values, expected_nominal_bin)


@pytest.mark.parametrize("sensor_number", [45, 90])
@pytest.mark.parametrize(
"sensor_number, expected_lon, expected_lat",
[(45, 260.0, -30.0), (90, 120.0, 15.0)],
)
@mock.patch("imap_processing.hi.hi_l1b.any_good_direct_events", return_value=True)
@mock.patch("imap_processing.hi.hi_l1b.instrument_pointing")
def test_compute_hae_coordinates(
mock_instrument_pointing, mock_any_good_de, sensor_number
mock_instrument_pointing,
mock_any_good_de,
sensor_number,
expected_lon,
expected_lat,
):
"""Test coverage for compute_hae_coordinates function."""

Expand All @@ -344,9 +351,10 @@ def side_effect_func(et, inst_frame: SpiceFrame, to_frame):

The function signature matches the signature of `instrument_pointing`.
The output is the same type and shape as what `instrument_pointing` returns
but is filled with values that match the instrument sensor number.
(longitude, latitude) with distinct values so a lat/lon swap is detected.
"""
return np.full((et.size, 2), 45 if "45" in inst_frame.name else 90)
assert str(sensor_number) in inst_frame.name
return np.tile([expected_lon, expected_lat], (et.size, 1))

mock_instrument_pointing.side_effect = side_effect_func

Expand All @@ -363,10 +371,10 @@ def side_effect_func(et, inst_frame: SpiceFrame, to_frame):
new_vars = compute_hae_coordinates(fake_dataset)
assert "hae_latitude" in new_vars
assert new_vars["hae_latitude"].shape == fake_dataset.event_met.shape
np.testing.assert_allclose(new_vars["hae_latitude"].values, sensor_number)
np.testing.assert_allclose(new_vars["hae_latitude"].values, expected_lat)
assert "hae_longitude" in new_vars
assert new_vars["hae_longitude"].shape == fake_dataset.event_met.shape
np.testing.assert_allclose(new_vars["hae_longitude"].values, sensor_number)
np.testing.assert_allclose(new_vars["hae_longitude"].values, expected_lon)


@mock.patch("imap_processing.hi.hi_l1b.any_good_direct_events", return_value=True)
Expand Down
43 changes: 43 additions & 0 deletions imap_processing/tests/spice/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,49 @@ def test_instrument_pointing(furnish_kernels):
assert ins_pointing.shape == (3, 3)


@pytest.mark.parametrize(
"vectors, expected_lon_lat",
[
# Single vector input: -y axis has a longitude of 270, not -90
(np.array([0.0, -1.0, 0.0]), np.array([270.0, 0.0])),
# Multiple vector input covering all four quadrants and the poles
(
np.array(
[
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[-1.0, 1.0, 0.0],
[-1.0, -1.0, 0.0],
[1.0, -1.0, np.sqrt(2)],
[0.0, 0.0, 1.0],
[0.0, 0.0, -1.0],
]
),
np.array(
[
[0.0, 0.0],
[45.0, 0.0],
[135.0, 0.0],
[225.0, 0.0],
[315.0, 45.0],
[0.0, 90.0],
[0.0, -90.0],
]
),
),
],
)
@mock.patch("imap_processing.spice.geometry.frame_transform")
def test_instrument_pointing_lon_lat(mock_frame_transform, vectors, expected_lon_lat):
"""Test instrument_pointing (lon, lat) output with known cartesian vectors."""
mock_frame_transform.return_value = vectors
et = 0.0 if vectors.ndim == 1 else np.zeros(len(vectors))
lon_lat = instrument_pointing(et, SpiceFrame.IMAP_HI_90, SpiceFrame.ECLIPJ2000)
assert lon_lat.shape == expected_lon_lat.shape
np.testing.assert_allclose(lon_lat, expected_lon_lat, atol=1e-12)
assert np.all((lon_lat[..., 0] >= 0) & (lon_lat[..., 0] < 360))


@pytest.mark.parametrize(
"frame",
[
Expand Down
Loading