Skip to content

Commit b22d5d4

Browse files
authored
Lo L1C - Add HAE Lat/Lon to pointing set (IMAP-Science-Operations-Center#2273)
* added pointing direction * added pointing direction * fixed l1c unit test * updated unit test and switching from ELCLIP2000 to HAE * removed transpose issue resolved, removed housekeeping print, fixed background rate error naming * fixed unit tests for background changes
1 parent 6d91c76 commit b22d5d4

3 files changed

Lines changed: 161 additions & 5 deletions

File tree

imap_processing/lo/l1c/lo_l1c.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@
1111
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
1212
from imap_processing.lo import lo_ancillary
1313
from imap_processing.lo.l1b.lo_l1b import set_bad_or_goodtimes
14+
from imap_processing.spice.geometry import SpiceFrame, frame_transform_az_el
1415
from imap_processing.spice.repoint import get_pointing_times
1516
from imap_processing.spice.spin import get_spin_number
16-
from imap_processing.spice.time import met_to_ttj2000ns, ttj2000ns_to_met
17+
from imap_processing.spice.time import (
18+
met_to_ttj2000ns,
19+
ttj2000ns_to_et,
20+
ttj2000ns_to_met,
21+
)
1722

1823
N_ESA_ENERGY_STEPS = 7
1924
N_SPIN_ANGLE_BINS = 3600
@@ -164,6 +169,10 @@ def lo_l1c(sci_dependencies: dict, anc_dependencies: list) -> list[xr.Dataset]:
164169
attr_mgr,
165170
)
166171

172+
pset["hae_longitude"], pset["hae_latitude"] = set_pointing_directions(
173+
pset["epoch"].item()
174+
)
175+
167176
pset.attrs = attr_mgr.get_global_attributes(logical_source)
168177

169178
pset = pset.assign_coords(
@@ -572,7 +581,7 @@ def set_background_rates(
572581
if row["type"] == "rate":
573582
bg_rates[esa_step, bin_start:bin_end, :] = value
574583
elif row["type"] == "sigma":
575-
bg_stat_uncert[esa_step, bin_start:bin_end, :] = value
584+
bg_sys_err[esa_step, bin_start:bin_end, :] = value
576585
else:
577586
raise ValueError("Unknown background type in ancillary file.")
578587
# set the background rates, uncertainties, and systematic errors
@@ -597,3 +606,52 @@ def set_background_rates(
597606
)
598607

599608
return bg_rates_data, bg_stat_uncert_data, bg_sys_err_data
609+
610+
611+
def set_pointing_directions(epoch: float) -> tuple[xr.DataArray, xr.DataArray]:
612+
"""
613+
Set the pointing directions for the given epoch.
614+
615+
The pointing directions are calculated by transforming Spin and off angles
616+
to HAE longitude and latitude using SPICE. This returns the HAE longitude and
617+
latitude as (3600, 40) arrays for each the latitude and longitude.
618+
619+
Parameters
620+
----------
621+
epoch : float
622+
The epoch time in TTJ2000ns.
623+
624+
Returns
625+
-------
626+
hae_longitude : xr.DataArray
627+
The HAE longitude for each spin and off angle bin.
628+
hae_latitude : xr.DataArray
629+
The HAE latitude for each spin and off angle bin.
630+
"""
631+
et = ttj2000ns_to_et(epoch)
632+
# create a meshgrid of spin and off angles using the bin centers
633+
spin, off = np.meshgrid(
634+
SPIN_ANGLE_BIN_CENTERS, OFF_ANGLE_BIN_CENTERS, indexing="ij"
635+
)
636+
dps_az_el = np.stack([spin, off], axis=-1)
637+
638+
# Transform from DPS Az/El to HAE lon/lat
639+
hae_az_el = frame_transform_az_el(
640+
et, dps_az_el, SpiceFrame.IMAP_DPS, SpiceFrame.IMAP_HAE, degrees=True
641+
)
642+
643+
return xr.DataArray(
644+
data=hae_az_el[:, :, 0].astype(np.float64),
645+
dims=["spin_angle", "off_angle"],
646+
# TODO: Add hae_longitude to yaml
647+
# attrs=attr_mgr.get_variable_attributes(
648+
# "hae_longitude"
649+
# )
650+
), xr.DataArray(
651+
data=hae_az_el[:, :, 1].astype(np.float64),
652+
dims=["spin_angle", "off_angle"],
653+
# TODO: Add hae_longitude to yaml
654+
# attrs=attr_mgr.get_variable_attributes(
655+
# "hae_latitude"
656+
# )
657+
)

imap_processing/tests/lo/test_lo_housekeeping.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def test_housekeeping(housekeeping_datasets):
4141
# the first 3 and the final value.
4242
small_ds = ds.isel(epoch=[0, 1, 2, -1])
4343
for var in validation_data_l1a.columns:
44-
print(var)
4544
if var == "PPM_UPPER_BOUND":
4645
# This is 65535 in the validation data, but only 4095 in the dataset.
4746
# This is because it is defined as a 12-bit quantity in the packet

imap_processing/tests/lo/test_lo_l1c.py

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
filter_goodtimes,
1515
lo_l1c,
1616
set_background_rates,
17+
set_pointing_directions,
1718
)
1819
from imap_processing.spice.time import met_to_ttj2000ns
1920

@@ -170,7 +171,7 @@ def expected_bg():
170171
dtype=np.float16,
171172
)
172173

173-
expected_uncert = np.array(
174+
expected_err = np.array(
174175
[
175176
np.full((3600, 40), 0.0025),
176177
np.full((3600, 40), 0.002),
@@ -183,15 +184,17 @@ def expected_bg():
183184
dtype=np.float16,
184185
)
185186

186-
expected_err = np.zeros((7, 3600, 40), dtype=np.float16)
187+
expected_uncert = np.zeros((7, 3600, 40), dtype=np.float16)
187188

188189
expected_bg = (expected_rates, expected_uncert, expected_err)
189190
return expected_bg
190191

191192

192193
@patch("imap_processing.lo.l1c.lo_l1c.set_background_rates")
193194
@patch("imap_processing.lo.l1c.lo_l1c.filter_goodtimes")
195+
@patch("imap_processing.lo.l1c.lo_l1c.set_pointing_directions")
194196
def test_lo_l1c(
197+
mock_set_pointing_directions,
195198
mock_filter_goodtimes,
196199
mock_set_background_rates,
197200
l1b_de_spin,
@@ -206,6 +209,10 @@ def test_lo_l1c(
206209
use_fake_repoint_data_for_time(np.arange(511000000, 511000000 + 86400 * 5, 86400))
207210
mock_set_background_rates.return_value = (None, None, None)
208211
mock_filter_goodtimes.return_value = l1b_de_spin
212+
mock_set_pointing_directions.return_value = (
213+
xr.DataArray(np.zeros((3600, 40)), dims=("spin_angle", "off_angle")),
214+
xr.DataArray(np.zeros((3600, 40)), dims=("spin_angle", "off_angle")),
215+
)
209216
expected_logical_source = "imap_lo_l1c_pset"
210217

211218
# Act
@@ -354,3 +361,95 @@ def test_set_background_rates_species_error(anc_dependencies, attr_mgr):
354361
rates, uncert, err = set_background_rates(
355362
pointing_start_met, pointing_end_met, species, anc_dependencies, attr_mgr
356363
)
364+
365+
366+
def test_set_pointing_directions():
367+
"""Test the set_pointing_directions function."""
368+
# Mock the external dependencies
369+
mock_et = 123456789.0
370+
mock_hae_az_el = np.stack(
371+
np.meshgrid(np.arange(3600), np.arange(40), indexing="ij"), axis=-1
372+
) # spin_angle x off_angle x 2
373+
with (
374+
patch("imap_processing.lo.l1c.lo_l1c.ttj2000ns_to_et") as mock_ttj2000ns_to_et,
375+
patch(
376+
"imap_processing.lo.l1c.lo_l1c.frame_transform_az_el"
377+
) as mock_frame_transform,
378+
):
379+
# Set up mocks
380+
mock_ttj2000ns_to_et.return_value = mock_et
381+
mock_frame_transform.return_value = mock_hae_az_el
382+
383+
# Test input
384+
test_epoch = 1000000000.0
385+
386+
# Call the function
387+
hae_longitude, hae_latitude = set_pointing_directions(test_epoch)
388+
389+
# Verify ttj2000ns_to_et was called correctly
390+
mock_ttj2000ns_to_et.assert_called_once_with(test_epoch)
391+
392+
# Verify frame_transform_az_el was called correctly
393+
mock_frame_transform.assert_called_once()
394+
call_args = mock_frame_transform.call_args
395+
assert call_args[0][0] == mock_et # et parameter
396+
assert call_args[1]["degrees"] is True
397+
# Verify the shape of dps_az_el
398+
dps_az_el = call_args[0][1]
399+
assert dps_az_el.shape == (3600, 40, 2) # spin_angle x off_angle x 2
400+
401+
# Verify the returned DataArrays
402+
assert isinstance(hae_longitude, xr.DataArray)
403+
assert isinstance(hae_latitude, xr.DataArray)
404+
405+
# Check dimensions
406+
assert hae_longitude.dims == ("spin_angle", "off_angle")
407+
assert hae_latitude.dims == ("spin_angle", "off_angle")
408+
409+
# Check shapes
410+
assert hae_longitude.shape == (3600, 40) # off_angle x spin_angle
411+
assert hae_latitude.shape == (3600, 40) # off_angle x spin_angle
412+
413+
# Check data types
414+
assert hae_longitude.dtype == np.float64
415+
assert hae_latitude.dtype == np.float64
416+
417+
# Check that longitude uses first component (index 0)
418+
# and latitude uses second (index 1)
419+
np.testing.assert_array_equal(hae_longitude.values, mock_hae_az_el[:, :, 0])
420+
np.testing.assert_array_equal(hae_latitude.values, mock_hae_az_el[:, :, 1])
421+
422+
423+
def test_set_pointing_directions_meshgrid():
424+
"""Test that the meshgrid is created correctly."""
425+
with (
426+
patch("imap_processing.lo.l1c.lo_l1c.ttj2000ns_to_et") as mock_ttj2000ns_to_et,
427+
patch(
428+
"imap_processing.lo.l1c.lo_l1c.frame_transform_az_el"
429+
) as mock_frame_transform,
430+
):
431+
mock_ttj2000ns_to_et.return_value = 123456789.0
432+
mock_hae_az_el = np.stack(
433+
np.meshgrid(np.arange(3600), np.arange(40), indexing="ij"), axis=-1
434+
) # spin_angle x off_angle x 2
435+
mock_frame_transform.return_value = mock_hae_az_el
436+
437+
set_pointing_directions(1000000000.0)
438+
439+
# Get the dps_az_el array that was passed to frame_transform_az_el
440+
call_args = mock_frame_transform.call_args
441+
dps_az_el = call_args[0][1]
442+
443+
# Verify the meshgrid was created correctly
444+
# The first component should be spin angles repeated for each off angle
445+
expected_spin_shape = (3600, 40)
446+
assert dps_az_el[:, :, 0].shape == expected_spin_shape
447+
448+
# The second component should be off angles repeated for each spin angle
449+
assert dps_az_el[:, :, 1].shape == expected_spin_shape
450+
451+
# Check that spin angles vary along the first dimension
452+
assert not np.allclose(dps_az_el[0, 0, 0], dps_az_el[1, 0, 0])
453+
454+
# Check that off angles vary along the second dimension
455+
assert not np.allclose(dps_az_el[0, 0, 1], dps_az_el[0, 1, 1])

0 commit comments

Comments
 (0)