diff --git a/chandra_aca/maude_decom.py b/chandra_aca/maude_decom.py index 16966f8..fba6447 100644 --- a/chandra_aca/maude_decom.py +++ b/chandra_aca/maude_decom.py @@ -78,10 +78,16 @@ from struct import Struct from struct import unpack as _unpack +import astropy.units as u import maude import numpy as np from astropy.table import Table, vstack from Chandra.Time import DateTime +from cxotime import CxoTime, CxoTimeLike + +# Maude fetch limits +MAUDE_SINGLE_FETCH_LIMIT = 3.0 * u.hour +MAUDE_FETCH_LIMIT = 5 * u.day # The following are the tables in the docstring above. They appear to be transposed, # but the resultt agrees with level0. @@ -687,8 +693,9 @@ def get_raw_aca_packets(start, stop, maude_result=None, **maude_kwargs): {'flags': int, 'packets': [], 'TIME': np.array([]), 'MNF': np.array([]), 'MJF': np.array([])} """ - date_start, date_stop = DateTime(start), DateTime( - stop + date_start, date_stop = ( + DateTime(start), + DateTime(stop), ) # ensure input is proper date stop_pad = 1.5 / 86400 # padding at the end in case of trailing partial ACA packets @@ -1061,13 +1068,15 @@ def get_aca_packets( combine = True calibrate = True - date_start, date_stop = DateTime(start), DateTime( - stop + date_start, date_stop = ( + DateTime(start), + DateTime(stop), ) # ensure input is proper date - if 24 * (date_stop - date_start) > 3: + if (CxoTime(stop) - CxoTime(start)) > MAUDE_SINGLE_FETCH_LIMIT: raise ValueError( - f"Requested {24 * (date_stop - date_start)} hours of telemetry. " - "Maximum allowed is 3 hours at a time" + f"Requested {CxoTime(stop) - CxoTime(start)} of telemetry. " + f"Maximum allowed is {MAUDE_SINGLE_FETCH_LIMIT} at a time " + "(see MAUDE_SINGLE_FETCH_LIMIT)." ) stop_pad = 0 @@ -1209,24 +1218,54 @@ def _get_aca_packets( return table -def get_aca_images(start, stop, **maude_kwargs): +def get_aca_images(start: CxoTimeLike, stop: CxoTimeLike, **kwargs): """ Fetch ACA image telemetry + Fetch ACA image telemetry from MAUDE and return it as an astropy Table. With the default + settings and no additional kwargs, this calls `get_aca_packets()` in a configuration that + uses MAUDE frames, combines image data, and sets the TIME associated with each image to the + midpoint of the integration time during which that pixel data was collected (matches CXC L0 + times). See `get_aca_packets()`. + + Parameters ---------- start - timestamp interpreted as a Chandra.Time.DateTime + timestamp, CxoTimeLike stop - timestamp interpreted as a Chandra.Time.DateTime - maude_kwargs - keyword args passed to maude + timestamp, CxoTimeLike. stop - start cannot be greater than MAUDE_FETCH_LIMIT + kwargs + keyword args passed to get_aca_packets Returns ------- astropy.table.Table """ - return get_aca_packets(start, stop, level0=True, **maude_kwargs) + start = CxoTime(start) + stop = CxoTime(stop) + if (stop - start) > MAUDE_FETCH_LIMIT: + raise ValueError( + f"stop - start cannot be greater than {MAUDE_FETCH_LIMIT}. " + "Set module variable MAUDE_FETCH_LIMIT if needed." + ) + maude_fetch_times = CxoTime.linspace( + start, stop, step_max=MAUDE_SINGLE_FETCH_LIMIT - 1 * u.s + ) + packet_stack = [ + get_aca_packets( + start=istart, + stop=istop, + level0=True, + **kwargs, + ) + for istart, istop in zip( + maude_fetch_times[:-1], maude_fetch_times[1:], strict=False + ) + ] + out = vstack(packet_stack) + out.meta["times"] = maude_fetch_times + return out ###################### @@ -1263,8 +1302,9 @@ def get_raw_aca_blobs(start, stop, maude_result=None, **maude_kwargs): dict {'blobs': [], 'names': np.array([]), 'types': np.array([])} """ - date_start, date_stop = DateTime(start), DateTime( - stop + date_start, date_stop = ( + DateTime(start), + DateTime(stop), ) # ensure input is proper date stop_pad = 1.5 / 86400 # padding at the end in case of trailing partial ACA packets diff --git a/chandra_aca/tests/test_maude_decom.py b/chandra_aca/tests/test_maude_decom.py index 24acc24..a3f9011 100755 --- a/chandra_aca/tests/test_maude_decom.py +++ b/chandra_aca/tests/test_maude_decom.py @@ -4,9 +4,11 @@ import os import pickle +import astropy.units as u import maude import numpy as np import pytest +from cxotime import CxoTime from chandra_aca import maude_decom @@ -272,6 +274,58 @@ def test_partial_images(): assert np.all(table[i]["IMG"].mask == mask[table[i]["IMGTYPE"]]) +def test_aca_images_chunks_1(): + """Test that a fetch longer than the maude step limit works""" + single_fetch_limit = maude_decom.MAUDE_SINGLE_FETCH_LIMIT + maude_decom.MAUDE_SINGLE_FETCH_LIMIT = 1 * u.min + start = "2023:001:00:00:01.000" + stop = "2023:001:00:05:01.000" + tstart = CxoTime(start).secs + tstop = CxoTime(stop).secs + try: + imgs = maude_decom.get_aca_images(start, stop) + finally: + maude_decom.MAUDE_SINGLE_FETCH_LIMIT = single_fetch_limit + + imgs.sort(["TIME", "IMGNUM"]) + + for slot in range(8): + # Confirm that the data is basically contiguous + ok_slot = imgs["IMGNUM"] == slot + # Confirm no duplicates by VCDUCTR + assert len(np.unique(imgs[ok_slot]["VCDUCTR"])) == len(imgs[ok_slot]) + # Confirm for these 8x8 data that there's no gap > 5 seconds + assert np.max(np.diff(imgs[ok_slot]["TIME"])) < 5 + + # Confirm that the returned data times are within the start stop + assert imgs["TIME"][0] >= tstart + assert imgs["TIME"][-1] <= tstop + + # Confirm that the beginning and end match the expected values + assert abs(imgs[0]["TIME"] - tstart) < 2 + assert abs(imgs[-1]["TIME"] - tstop) < 2 + + imgs_start = maude_decom.get_aca_images(start, CxoTime(start) + 60 * u.s) + imgs_stop = maude_decom.get_aca_images(CxoTime(stop) - 60 * u.s, stop) + imgs_start.sort(["TIME", "IMGNUM"]) + imgs_stop.sort(["TIME", "IMGNUM"]) + assert np.all(imgs[0] == imgs_start[0]) + assert np.all(imgs[-1] == imgs_stop[-1]) + + # Check the linspace times in the returned data + used_step_max = 1 * u.min - 1 * u.s + deltas_lte = [t <= used_step_max for t in np.diff(imgs.meta["times"])] + assert np.all(deltas_lte) + + +def test_aca_images_chunks_2(): + """Test that a fetch longer than MAUDE_FETCH_LIMIT throws a ValueError""" + start = CxoTime("2023:001:00:00:01.000") + stop = start + (maude_decom.MAUDE_FETCH_LIMIT * 1.1) + with pytest.raises(ValueError, match="stop - start cannot be greater than"): + maude_decom.get_aca_images(start, stop) + + def test_vcdu_vs_level0(): from astropy.table import Table