From cb8808a05e178b10df66843cd5bc975dc8c9eb4e Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 15 Jul 2025 14:51:03 -0600 Subject: [PATCH 1/9] Modified `SensorData` and `TapData` to be instances of `named_arrays.FunctionArray`. --- msfc_ccd/__init__.py | 3 +- msfc_ccd/_images/__init__.py | 2 + msfc_ccd/_images/_images.py | 95 +--------- msfc_ccd/_images/_sensor_images.py | 166 +++++++----------- msfc_ccd/_images/_tap_images.py | 59 ++----- msfc_ccd/_images/_tests/test_images.py | 89 +--------- msfc_ccd/_images/_tests/test_sensor_images.py | 92 +++++----- msfc_ccd/_images/_tests/test_tap_images.py | 4 +- msfc_ccd/_images/_tests/test_vectors.py | 112 ++++++++++++ msfc_ccd/_images/_vectors.py | 80 +++++++++ 10 files changed, 330 insertions(+), 372 deletions(-) create mode 100644 msfc_ccd/_images/_tests/test_vectors.py create mode 100644 msfc_ccd/_images/_vectors.py diff --git a/msfc_ccd/__init__.py b/msfc_ccd/__init__.py index 56d924d..4f4509e 100644 --- a/msfc_ccd/__init__.py +++ b/msfc_ccd/__init__.py @@ -6,6 +6,7 @@ "abc", "samples", "TeledyneCCD230", + "ImageHeader", "SensorData", "TapData", "fits", @@ -14,5 +15,5 @@ from . import abc from . import samples from ._sensors import TeledyneCCD230 -from ._images import SensorData, TapData +from ._images import ImageHeader, SensorData, TapData from . import fits diff --git a/msfc_ccd/_images/__init__.py b/msfc_ccd/_images/__init__.py index e068e36..3af374a 100644 --- a/msfc_ccd/_images/__init__.py +++ b/msfc_ccd/_images/__init__.py @@ -3,9 +3,11 @@ """ __all__ = [ + "ImageHeader", "SensorData", "TapData", ] +from ._vectors import ImageHeader from ._sensor_images import SensorData from ._tap_images import TapData diff --git a/msfc_ccd/_images/_images.py b/msfc_ccd/_images/_images.py index 875be9a..dbe3037 100644 --- a/msfc_ccd/_images/_images.py +++ b/msfc_ccd/_images/_images.py @@ -1,17 +1,18 @@ import abc import dataclasses -import astropy.units as u -import astropy.time import named_arrays as na -import optika from .._sensors import AbstractSensor +from ._vectors import ImageHeader __all__ = [] @dataclasses.dataclass(eq=False, repr=False) class AbstractImageData( - optika.mixins.Printable, + na.FunctionArray[ + ImageHeader, + na.AbstractScalarArray, + ], ): """ An interface for image-like data. @@ -20,18 +21,6 @@ class AbstractImageData( sensor and for data from only a single tap on the sensor. """ - @property - @abc.abstractmethod - def data(self) -> na.AbstractScalar: - """The underlying array storing the image data.""" - - @property - @abc.abstractmethod - def pixel(self) -> dict[str, na.AbstractScalarArray]: - """ - The 2-dimensional index of each pixel in the image. - """ - @property @abc.abstractmethod def axis_x(self) -> str: @@ -53,29 +42,14 @@ def num_x(self) -> int: """ The number of pixels along the x-axis. """ - return self.data.shape[self.axis_x] + return self.outputs.shape[self.axis_x] @property def num_y(self) -> int: """ The number of pixels along the y-axis. """ - return self.data.shape[self.axis_y] - - @property - @abc.abstractmethod - def time(self) -> astropy.time.Time | na.AbstractScalar: - """The time in UTC at the midpoint of the exposure.""" - - @property - @abc.abstractmethod - def timedelta(self) -> u.Quantity | na.AbstractScalar: - """The measured exposure time of each image.""" - - @property - @abc.abstractmethod - def timedelta_requested(self) -> u.Quantity | na.AbstractScalar: - """The requested exposure time of each image.""" + return self.outputs.shape[self.axis_y] @property @abc.abstractmethod @@ -83,58 +57,3 @@ def sensor(self) -> AbstractSensor: """ A model of the sensor used to capture these images. """ - - @property - @abc.abstractmethod - def serial_number(self) -> str | na.AbstractScalar: - """The serial number of the camera that captured each image.""" - - @property - @abc.abstractmethod - def run_mode(self) -> str | na.AbstractScalar: - """The Run Mode of the camera when each image was captured.""" - - @property - @abc.abstractmethod - def status(self) -> str | na.AbstractScalar: - """The status of the camera while each image was being captured.""" - - @property - @abc.abstractmethod - def voltage_fpga_vccint(self) -> u.Quantity | na.AbstractScalar: - """The VCCINT voltage of the FPGA when each image was captured.""" - - @property - @abc.abstractmethod - def voltage_fpga_vccaux(self) -> u.Quantity | na.AbstractScalar: - """The VCCAUX voltage of the FPGA when each image was captured.""" - - @property - @abc.abstractmethod - def voltage_fpga_vccbram(self) -> u.Quantity | na.AbstractScalar: - """The VCCBRAM voltage of the FPGA when each image was captured.""" - - @property - @abc.abstractmethod - def temperature_fpga(self) -> u.Quantity | na.AbstractScalar: - """The temperature of the FPGA when each image was captured.""" - - @property - @abc.abstractmethod - def temperature_adc_1(self) -> u.Quantity | na.AbstractScalar: - """Temperature 1 of the ADC when each image was captured.""" - - @property - @abc.abstractmethod - def temperature_adc_2(self) -> u.Quantity | na.AbstractScalar: - """Temperature 2 of the ADC when each image was captured.""" - - @property - @abc.abstractmethod - def temperature_adc_3(self) -> u.Quantity | na.AbstractScalar: - """Temperature 3 of the ADC when each image was captured.""" - - @property - @abc.abstractmethod - def temperature_adc_4(self) -> u.Quantity | na.AbstractScalar: - """Temperature 4 of the ADC when each image was captured.""" diff --git a/msfc_ccd/_images/_sensor_images.py b/msfc_ccd/_images/_sensor_images.py index bcc32e5..6443517 100644 --- a/msfc_ccd/_images/_sensor_images.py +++ b/msfc_ccd/_images/_sensor_images.py @@ -9,6 +9,7 @@ import named_arrays as na import msfc_ccd from .._sensors import AbstractSensor +from ._vectors import ImageHeader from ._images import AbstractImageData __all__ = [ @@ -24,17 +25,6 @@ class AbstractSensorData( An interface for representing data captured by an entire image sensor. """ - @property - def pixel(self) -> dict[str, na.AbstractScalarArray]: - axis_x = self.axis_x - axis_y = self.axis_y - shape = self.data.shape - shape_img = { - axis_x: shape[axis_x], - axis_y: shape[axis_y], - } - return na.indices(shape_img) - def taps( self, axis_tap_x: str = "tap_x", @@ -61,57 +51,37 @@ def taps( num_tap_x = self.sensor.num_tap_x num_tap_y = self.sensor.num_tap_y - shape_img = {axis_x: num_x, axis_y: num_y} - num_x_new = num_x // num_tap_x num_y_new = num_y // num_tap_y - slice_left_x = slice(None, num_x_new) - slice_left_y = slice(None, num_y_new) + range_left_x = na.arange(0, num_x_new, axis=axis_x) + range_left_y = na.arange(0, num_y_new, axis=axis_y) - slice_right_x = slice(None, num_x_new - 1, -1) - slice_right_y = slice(None, num_y_new - 1, -1) + range_right_x = na.arange(num_x - 1, num_x_new - 1, axis=axis_x, step=-1) + range_right_y = na.arange(num_y - 1, num_y_new - 1, axis=axis_y, step=-1) - slices_x = [slice_left_x, slice_right_x] - slices_y = [slice_left_y, slice_right_y] + ranges_x = [range_left_x, range_right_x] + ranges_y = [range_left_y, range_right_y] - pixel = self.pixel + indices_x = na.stack(ranges_x, axis=axis_tap_x) + indices_y = na.stack(ranges_y, axis=axis_tap_y) - for ax in pixel: - p = pixel[ax].broadcast_to(shape_img) - pixel[ax] = na.stack( - arrays=[ - na.stack( - arrays=[p[{axis_x: sx, axis_y: sy}] for sy in slices_y], - axis=axis_tap_y, - ) - for sx in slices_x - ], - axis=axis_tap_x, - ) + indices = { + axis_x: indices_x, + axis_y: indices_y, + } return msfc_ccd.TapData( - data=self.data[pixel], - pixel=pixel, + inputs=dataclasses.replace( + self.inputs, + pixel=self.inputs.pixel[indices], + ), + outputs=self.outputs[indices], axis_x=self.axis_x, axis_y=self.axis_y, axis_tap_x=axis_tap_x, axis_tap_y=axis_tap_y, - time=self.time, - timedelta=self.timedelta, - timedelta_requested=self.timedelta_requested, sensor=self.sensor, - serial_number=self.serial_number, - run_mode=self.run_mode, - status=self.status, - voltage_fpga_vccint=self.voltage_fpga_vccint, - voltage_fpga_vccaux=self.voltage_fpga_vccaux, - voltage_fpga_vccbram=self.voltage_fpga_vccbram, - temperature_fpga=self.temperature_fpga, - temperature_adc_1=self.temperature_adc_1, - temperature_adc_2=self.temperature_adc_2, - temperature_adc_3=self.temperature_adc_3, - temperature_adc_4=self.temperature_adc_4, ) @@ -158,8 +128,15 @@ class SensorData( ); """ - data: na.AbstractScalar = dataclasses.MISSING - """The underlying array storing the image data.""" + inputs: ImageHeader = dataclasses.MISSING + """ + A vector which contains the time and index of each pixel in the set of images. + """ + + outputs: na.ScalarArray = dataclasses.MISSING + """ + The underlying array storing the image data + """ axis_x: str = dataclasses.MISSING """ @@ -173,51 +150,9 @@ class SensorData( the images. """ - time: astropy.time.Time | na.AbstractScalar = dataclasses.MISSING - """The time in UTC at the midpoint of the exposure.""" - - timedelta: u.Quantity | na.AbstractScalar = dataclasses.MISSING - """The measured exposure time of each image.""" - - timedelta_requested: u.Quantity | na.AbstractScalar = dataclasses.MISSING - """The requested exposure time of each image.""" - sensor: AbstractSensor = dataclasses.MISSING """A model of the sensor used to capture these images.""" - serial_number: None | str | na.AbstractScalar = None - """The serial number of the camera that captured each image.""" - - run_mode: None | str | na.AbstractScalar = None - """The Run Mode of the camera when each image was captured.""" - - status: None | str | na.AbstractScalar = None - """The status of the camera while each image was being captured.""" - - voltage_fpga_vccint: u.Quantity | na.AbstractScalar = 0 - """The VCCINT voltage of the FPGA when each image was captured.""" - - voltage_fpga_vccaux: u.Quantity | na.AbstractScalar = 0 - """The VCCAUX voltage of the FPGA when each image was captured.""" - - voltage_fpga_vccbram: u.Quantity | na.AbstractScalar = 0 - """The VCCBRAM voltage of the FPGA when each image was captured.""" - - temperature_fpga: u.Quantity | na.AbstractScalar = 0 - """The temperature of the FPGA when each image was captured.""" - - temperature_adc_1: u.Quantity | na.AbstractScalar = 0 - """Temperature 1 of the ADC when each image was captured.""" - - temperature_adc_2: u.Quantity | na.AbstractScalar = 0 - """Temperature 2 of the ADC when each image was captured.""" - - temperature_adc_3: u.Quantity | na.AbstractScalar = 0 - """Temperature 3 of the ADC when each image was captured.""" - - temperature_adc_4: u.Quantity | na.AbstractScalar = 0 - """Temperature 4 of the ADC when each image was captured.""" - @classmethod def _calibrate_timedelta(cls, value: int) -> u.Quantity: return value * 0.000000025 * u.s @@ -338,23 +273,40 @@ def from_fits( temperature_adc_3 = cls._calibrate_temperature_adc_234(temperature_adc_3) temperature_adc_4 = cls._calibrate_temperature_adc_234(temperature_adc_4) + shape = data.shape + + shape_img = { + axis_x: shape[axis_x], + axis_y: shape[axis_y], + } + + pixel = na.indices(shape_img) + + pixel = na.Cartesian2dVectorArray( + x=pixel[axis_x], + y=pixel[axis_y], + ) + return cls( - data=data, + inputs=ImageHeader( + pixel=pixel, + time=time, + timedelta=timedelta, + timedelta_requested=timedelta_requested, + serial_number=serial_number, + run_mode=run_mode, + status=status, + voltage_fpga_vccint=voltage_fpga_vccint, + voltage_fpga_vccaux=voltage_fpga_vccaux, + voltage_fpga_vccbram=voltage_fpga_vccbram, + temperature_fpga=temperature_fpga, + temperature_adc_1=temperature_adc_1, + temperature_adc_2=temperature_adc_2, + temperature_adc_3=temperature_adc_3, + temperature_adc_4=temperature_adc_4, + ), + outputs=data, axis_x=axis_x, axis_y=axis_y, - time=time, - timedelta=timedelta, - timedelta_requested=timedelta_requested, sensor=sensor, - serial_number=serial_number, - run_mode=run_mode, - status=status, - voltage_fpga_vccint=voltage_fpga_vccint, - voltage_fpga_vccaux=voltage_fpga_vccaux, - voltage_fpga_vccbram=voltage_fpga_vccbram, - temperature_fpga=temperature_fpga, - temperature_adc_1=temperature_adc_1, - temperature_adc_2=temperature_adc_2, - temperature_adc_3=temperature_adc_3, - temperature_adc_4=temperature_adc_4, ) diff --git a/msfc_ccd/_images/_tap_images.py b/msfc_ccd/_images/_tap_images.py index d239803..51df5e8 100644 --- a/msfc_ccd/_images/_tap_images.py +++ b/msfc_ccd/_images/_tap_images.py @@ -1,9 +1,8 @@ import abc import dataclasses -import astropy.units as u -import astropy.time import named_arrays as na from .._sensors import AbstractSensor +from ._vectors import ImageHeader from ._images import AbstractImageData __all__ = [ @@ -43,7 +42,7 @@ def tap(self) -> dict[str, na.AbstractScalarArray]: """ axis_tap_x = self.axis_tap_x axis_tap_y = self.axis_tap_y - shape = self.data.shape + shape = self.outputs.shape shape_img = { axis_tap_x: shape[axis_tap_x], axis_tap_y: shape[axis_tap_y], @@ -106,11 +105,15 @@ class TapData( """ - data: na.AbstractScalar = dataclasses.MISSING - """The underlying array storing the image data.""" + inputs: ImageHeader = dataclasses.MISSING + """ + A vector which contains the time and index of each pixel in the set of images. + """ - pixel: dict[str, na.AbstractScalarArray] = dataclasses.MISSING - """The 2-dimensional index of each pixel in the image""" + outputs: na.ScalarArray = dataclasses.MISSING + """ + The underlying array storing the image data + """ axis_x: str = dataclasses.MISSING """ @@ -136,47 +139,5 @@ class TapData( variation of the tap index. """ - time: astropy.time.Time | na.AbstractScalar = dataclasses.MISSING - """The time in UTC at the midpoint of the exposure.""" - - timedelta: u.Quantity | na.AbstractScalar = dataclasses.MISSING - """The measured exposure time of each image.""" - - timedelta_requested: u.Quantity | na.AbstractScalar = dataclasses.MISSING - """The requested exposure time of each image.""" - sensor: AbstractSensor = dataclasses.MISSING """A model of the sensor used to capture these images.""" - - serial_number: None | str | na.AbstractScalar = None - """The serial number of the camera that captured each image.""" - - run_mode: None | str | na.AbstractScalar = None - """The Run Mode of the camera when each image was captured.""" - - status: None | str | na.AbstractScalar = None - """The status of the camera while each image was being captured.""" - - voltage_fpga_vccint: u.Quantity | na.AbstractScalar = 0 - """The VCCINT voltage of the FPGA when each image was captured.""" - - voltage_fpga_vccaux: u.Quantity | na.AbstractScalar = 0 - """The VCCAUX voltage of the FPGA when each image was captured.""" - - voltage_fpga_vccbram: u.Quantity | na.AbstractScalar = 0 - """The VCCBRAM voltage of the FPGA when each image was captured.""" - - temperature_fpga: u.Quantity | na.AbstractScalar = 0 - """The temperature of the FPGA when each image was captured.""" - - temperature_adc_1: u.Quantity | na.AbstractScalar = 0 - """Temperature 1 of the ADC when each image was captured.""" - - temperature_adc_2: u.Quantity | na.AbstractScalar = 0 - """Temperature 2 of the ADC when each image was captured.""" - - temperature_adc_3: u.Quantity | na.AbstractScalar = 0 - """Temperature 3 of the ADC when each image was captured.""" - - temperature_adc_4: u.Quantity | na.AbstractScalar = 0 - """Temperature 4 of the ADC when each image was captured.""" diff --git a/msfc_ccd/_images/_tests/test_images.py b/msfc_ccd/_images/_tests/test_images.py index a5876ae..2f1ce89 100644 --- a/msfc_ccd/_images/_tests/test_images.py +++ b/msfc_ccd/_images/_tests/test_images.py @@ -1,34 +1,20 @@ -import numpy as np -import astropy.units as u -import astropy.time -import named_arrays as na -import optika._tests.test_mixins +import abc import msfc_ccd class AbstractTestAbstractImageData( - optika._tests.test_mixins.AbstractTestPrintable, + abc.ABC, ): - def test_data(self, a: msfc_ccd.abc.AbstractImageData): - result = a.data - assert result.ndim >= 2 - - def test_pixel(self, a: msfc_ccd.abc.AbstractImageData): - result = a.pixel - for ax in result: - assert isinstance(ax, str) - assert isinstance(result[ax], na.AbstractScalarArray) - def test_axis_x(self, a: msfc_ccd.abc.AbstractImageData): result = a.axis_x assert isinstance(result, str) - assert result in a.data.shape + assert result in a.outputs.shape def test_axis_y(self, a: msfc_ccd.abc.AbstractImageData): result = a.axis_y assert isinstance(result, str) - assert result in a.data.shape + assert result in a.outputs.shape def test_num_x(self, a: msfc_ccd.abc.AbstractImageData): result = a.num_x @@ -38,74 +24,7 @@ def test_num_y(self, a: msfc_ccd.abc.AbstractImageData): result = a.num_y assert isinstance(result, int) - def test_time(self, a: msfc_ccd.abc.AbstractImageData): - result = a.time - if not isinstance(result, astropy.time.Time): - assert isinstance(result.ndarray, astropy.time.Time) - - def test_timedelta(self, a: msfc_ccd.abc.AbstractImageData): - result = a.timedelta - assert np.all(result >= 1 * u.s) - assert np.all(result < 1000 * u.s) - - def test_timedelta_requested(self, a: msfc_ccd.abc.AbstractImageData): - result = a.timedelta - assert np.all(result >= 1 * u.s) - assert np.all(result < 1000 * u.s) - def test_sensor(self, a: msfc_ccd.abc.AbstractImageData): result = a.sensor if result is not None: assert isinstance(result, msfc_ccd.abc.AbstractSensor) - - def test_serial_number(self, a: msfc_ccd.abc.AbstractImageData): - result = a.serial_number - assert isinstance(result, (str, na.AbstractScalar)) - - def test_run_mode(self, a: msfc_ccd.abc.AbstractImageData): - result = a.run_mode - assert isinstance(result, (str, na.AbstractScalar)) - - def test_status(self, a: msfc_ccd.abc.AbstractImageData): - result = a.status - assert isinstance(result, (str, na.AbstractScalar)) - - def test_voltage_fpga_vccint(self, a: msfc_ccd.abc.AbstractImageData): - result = a.voltage_fpga_vccint - assert np.all(result >= 0 * u.V) - assert np.all(result < 50 * u.V) - - def test_voltage_fpga_vccaux(self, a: msfc_ccd.abc.AbstractImageData): - result = a.voltage_fpga_vccaux - assert np.all(result >= 0 * u.V) - assert np.all(result < 50 * u.V) - - def test_voltage_fpga_vccbram(self, a: msfc_ccd.abc.AbstractImageData): - result = a.voltage_fpga_vccbram - assert np.all(result >= 0 * u.V) - assert np.all(result < 50 * u.V) - - def test_temperature_fpga(self, a: msfc_ccd.abc.AbstractImageData): - result = a.temperature_fpga - assert np.all(result >= 0 * u.deg_C) - assert np.all(result < 100 * u.deg_C) - - def test_temperature_adc_1(self, a: msfc_ccd.abc.AbstractImageData): - result = a.temperature_adc_1 - assert np.all(result >= 0 * u.deg_C) - assert np.all(result < 100 * u.deg_C) - - def test_temperature_adc_2(self, a: msfc_ccd.abc.AbstractImageData): - result = a.temperature_adc_2 - assert np.all(result >= 0 * u.deg_C) - assert np.all(result < 100 * u.deg_C) - - def test_temperature_adc_3(self, a: msfc_ccd.abc.AbstractImageData): - result = a.temperature_adc_3 - assert np.all(result >= 0 * u.deg_C) - assert np.all(result < 100 * u.deg_C) - - def test_temperature_adc_4(self, a: msfc_ccd.abc.AbstractImageData): - result = a.temperature_adc_4 - assert np.all(result >= 0 * u.deg_C) - assert np.all(result < 100 * u.deg_C) diff --git a/msfc_ccd/_images/_tests/test_sensor_images.py b/msfc_ccd/_images/_tests/test_sensor_images.py index 28e9c1b..8825c84 100644 --- a/msfc_ccd/_images/_tests/test_sensor_images.py +++ b/msfc_ccd/_images/_tests/test_sensor_images.py @@ -19,58 +19,70 @@ def test_taps(self, a: msfc_ccd.abc.AbstractSensorData): argnames="a", argvalues=[ msfc_ccd.SensorData( - data=na.random.uniform(0, 1, shape_random=dict(x=22, y=12)), + inputs=msfc_ccd.ImageHeader( + pixel=na.Cartesian2dVectorArray( + x=na.arange(0, 22, "x"), + y=na.arange(0, 12, "y"), + ), + time=astropy.time.Time("2024-03-25T20:49"), + timedelta=9.98 * u.s, + timedelta_requested=10 * u.s, + serial_number="SN-001", + run_mode="sequence", + status="completed", + voltage_fpga_vccint=5 * u.V, + voltage_fpga_vccaux=6 * u.V, + voltage_fpga_vccbram=7 * u.V, + temperature_fpga=20 * u.deg_C, + temperature_adc_1=21 * u.deg_C, + temperature_adc_2=22 * u.deg_C, + temperature_adc_3=23 * u.deg_C, + temperature_adc_4=24 * u.deg_C, + ), + outputs=na.random.uniform(0, 1, shape_random=dict(x=22, y=12)), axis_x="x", axis_y="y", - time=astropy.time.Time("2024-03-25T20:49"), - timedelta=9.98 * u.s, - timedelta_requested=10 * u.s, sensor=msfc_ccd.TeledyneCCD230(), - serial_number="SN-001", - run_mode="sequence", - status="completed", - voltage_fpga_vccint=5 * u.V, - voltage_fpga_vccaux=6 * u.V, - voltage_fpga_vccbram=7 * u.V, - temperature_fpga=20 * u.deg_C, - temperature_adc_1=21 * u.deg_C, - temperature_adc_2=22 * u.deg_C, - temperature_adc_3=23 * u.deg_C, - temperature_adc_4=24 * u.deg_C, ), msfc_ccd.SensorData( - data=na.random.uniform( + inputs=msfc_ccd.ImageHeader( + pixel=na.Cartesian2dVectorArray( + x=na.arange(0, 22, "x"), + y=na.arange(0, 12, "y"), + ), + time=na.ScalarArray( + ndarray=np.linspace( + astropy.time.Time("2024-03-25T20:49"), + astropy.time.Time("2024-03-25T21:49"), + num=5, + ), + axes="t", + ), + timedelta=na.ScalarArray( + ndarray=[9.98, 9.99, 10.1, 10.06, 9.76] * u.s, + axes="t", + ), + timedelta_requested=10 * u.s, + serial_number="SN-001", + run_mode="sequence", + status="completed", + voltage_fpga_vccint=5 * u.V, + voltage_fpga_vccaux=6 * u.V, + voltage_fpga_vccbram=7 * u.V, + temperature_fpga=20 * u.deg_C, + temperature_adc_1=21 * u.deg_C, + temperature_adc_2=22 * u.deg_C, + temperature_adc_3=23 * u.deg_C, + temperature_adc_4=24 * u.deg_C, + ), + outputs=na.random.uniform( low=0, high=1, shape_random=dict(t=5, x=22, y=12), ), axis_x="x", axis_y="y", - time=na.ScalarArray( - ndarray=np.linspace( - astropy.time.Time("2024-03-25T20:49"), - astropy.time.Time("2024-03-25T21:49"), - num=5, - ), - axes="t", - ), - timedelta=na.ScalarArray( - ndarray=[9.98, 9.99, 10.1, 10.06, 9.76] * u.s, - axes="t", - ), - timedelta_requested=10 * u.s, sensor=msfc_ccd.TeledyneCCD230(), - serial_number="SN-001", - run_mode="sequence", - status="completed", - voltage_fpga_vccint=5 * u.V, - voltage_fpga_vccaux=6 * u.V, - voltage_fpga_vccbram=7 * u.V, - temperature_fpga=20 * u.deg_C, - temperature_adc_1=21 * u.deg_C, - temperature_adc_2=22 * u.deg_C, - temperature_adc_3=23 * u.deg_C, - temperature_adc_4=24 * u.deg_C, ), msfc_ccd.SensorData.from_fits( path=msfc_ccd.samples.path_fe55_esis1, diff --git a/msfc_ccd/_images/_tests/test_tap_images.py b/msfc_ccd/_images/_tests/test_tap_images.py index 225955a..f23f2d7 100644 --- a/msfc_ccd/_images/_tests/test_tap_images.py +++ b/msfc_ccd/_images/_tests/test_tap_images.py @@ -10,12 +10,12 @@ class AbstractTestAbstractTapImage( def test_axis_tap_x(self, a: msfc_ccd.abc.AbstractTapData): result = a.axis_tap_x assert isinstance(result, str) - assert result in a.data.shape + assert result in a.outputs.shape def test_axis_tap_y(self, a: msfc_ccd.abc.AbstractTapData): result = a.axis_tap_y assert isinstance(result, str) - assert result in a.data.shape + assert result in a.outputs.shape def test_tap(self, a: msfc_ccd.abc.AbstractTapData): result = a.tap diff --git a/msfc_ccd/_images/_tests/test_vectors.py b/msfc_ccd/_images/_tests/test_vectors.py new file mode 100644 index 0000000..584f991 --- /dev/null +++ b/msfc_ccd/_images/_tests/test_vectors.py @@ -0,0 +1,112 @@ +import pytest +import numpy as np +import astropy.units as u +import astropy.time +import named_arrays as na +import msfc_ccd + + +@pytest.mark.parametrize( + argnames="a", + argvalues=[ + msfc_ccd.ImageHeader( + pixel=na.Cartesian2dVectorArrayRange( + start=0, + stop=32, + axis=na.Cartesian2dVectorArray(x="x", y="y"), + ), + time=na.ScalarArray( + ndarray=np.linspace( + astropy.time.Time("2024-03-25T20:49"), + astropy.time.Time("2024-03-25T21:49"), + num=5, + ), + axes="t", + ), + timedelta=na.ScalarArray( + ndarray=[9.98, 9.99, 10.1, 10.06, 9.76] * u.s, + axes="t", + ), + timedelta_requested=10 * u.s, + serial_number="SN-001", + run_mode="sequence", + status="completed", + voltage_fpga_vccint=5 * u.V, + voltage_fpga_vccaux=6 * u.V, + voltage_fpga_vccbram=7 * u.V, + temperature_fpga=20 * u.deg_C, + temperature_adc_1=21 * u.deg_C, + temperature_adc_2=22 * u.deg_C, + temperature_adc_3=23 * u.deg_C, + temperature_adc_4=24 * u.deg_C, + ) + ], +) +class TestImageHeader: + + def test_time(self, a: msfc_ccd.abc.AbstractImageData): + result = a.time + if not isinstance(result, astropy.time.Time): + assert isinstance(result.ndarray, astropy.time.Time) + + def test_timedelta(self, a: msfc_ccd.abc.AbstractImageData): + result = a.timedelta + assert np.all(result >= 1 * u.s) + assert np.all(result < 1000 * u.s) + + def test_timedelta_requested(self, a: msfc_ccd.abc.AbstractImageData): + result = a.timedelta + assert np.all(result >= 1 * u.s) + assert np.all(result < 1000 * u.s) + + def test_serial_number(self, a: msfc_ccd.abc.AbstractImageData): + result = a.serial_number + assert isinstance(result, (str, na.AbstractScalar)) + + def test_run_mode(self, a: msfc_ccd.abc.AbstractImageData): + result = a.run_mode + assert isinstance(result, (str, na.AbstractScalar)) + + def test_status(self, a: msfc_ccd.abc.AbstractImageData): + result = a.status + assert isinstance(result, (str, na.AbstractScalar)) + + def test_voltage_fpga_vccint(self, a: msfc_ccd.abc.AbstractImageData): + result = a.voltage_fpga_vccint + assert np.all(result >= 0 * u.V) + assert np.all(result < 50 * u.V) + + def test_voltage_fpga_vccaux(self, a: msfc_ccd.abc.AbstractImageData): + result = a.voltage_fpga_vccaux + assert np.all(result >= 0 * u.V) + assert np.all(result < 50 * u.V) + + def test_voltage_fpga_vccbram(self, a: msfc_ccd.abc.AbstractImageData): + result = a.voltage_fpga_vccbram + assert np.all(result >= 0 * u.V) + assert np.all(result < 50 * u.V) + + def test_temperature_fpga(self, a: msfc_ccd.abc.AbstractImageData): + result = a.temperature_fpga + assert np.all(result >= 0 * u.deg_C) + assert np.all(result < 100 * u.deg_C) + + def test_temperature_adc_1(self, a: msfc_ccd.abc.AbstractImageData): + result = a.temperature_adc_1 + assert np.all(result >= 0 * u.deg_C) + assert np.all(result < 100 * u.deg_C) + + def test_temperature_adc_2(self, a: msfc_ccd.abc.AbstractImageData): + result = a.temperature_adc_2 + assert np.all(result >= 0 * u.deg_C) + assert np.all(result < 100 * u.deg_C) + + def test_temperature_adc_3(self, a: msfc_ccd.abc.AbstractImageData): + result = a.temperature_adc_3 + assert np.all(result >= 0 * u.deg_C) + assert np.all(result < 100 * u.deg_C) + + def test_temperature_adc_4(self, a: msfc_ccd.abc.AbstractImageData): + result = a.temperature_adc_4 + assert np.all(result >= 0 * u.deg_C) + assert np.all(result < 100 * u.deg_C) diff --git a/msfc_ccd/_images/_vectors.py b/msfc_ccd/_images/_vectors.py new file mode 100644 index 0000000..a283746 --- /dev/null +++ b/msfc_ccd/_images/_vectors.py @@ -0,0 +1,80 @@ +from typing import Type +from typing_extensions import Self +import dataclasses +import astropy.units as u +import astropy.time +import named_arrays as na + +__all__ = [ + "ImageHeader" +] + + +@dataclasses.dataclass(eq=False, repr=False) +class ImageHeader( + na.AbstractExplicitCartesianVectorArray, +): + pixel: na.AbstractCartesian2dVectorArray = dataclasses.MISSING + """The indices of each pixel in the image.""" + + time: astropy.time.Time | na.AbstractScalar = dataclasses.MISSING + """The time in UTC at the midpoint of the exposure.""" + + timedelta: u.Quantity | na.AbstractScalar = dataclasses.MISSING + """The measured exposure time of each image.""" + + timedelta_requested: u.Quantity | na.AbstractScalar = dataclasses.MISSING + """The requested exposure time of each image.""" + + serial_number: None | str | na.AbstractScalar = None + """The serial number of the camera that captured each image.""" + + run_mode: None | str | na.AbstractScalar = None + """The Run Mode of the camera when each image was captured.""" + + status: None | str | na.AbstractScalar = None + """The status of the camera while each image was being captured.""" + + voltage_fpga_vccint: u.Quantity | na.AbstractScalar = 0 + """The VCCINT voltage of the FPGA when each image was captured.""" + + voltage_fpga_vccaux: u.Quantity | na.AbstractScalar = 0 + """The VCCAUX voltage of the FPGA when each image was captured.""" + + voltage_fpga_vccbram: u.Quantity | na.AbstractScalar = 0 + """The VCCBRAM voltage of the FPGA when each image was captured.""" + + temperature_fpga: u.Quantity | na.AbstractScalar = 0 + """The temperature of the FPGA when each image was captured.""" + + temperature_adc_1: u.Quantity | na.AbstractScalar = 0 + """Temperature 1 of the ADC when each image was captured.""" + + temperature_adc_2: u.Quantity | na.AbstractScalar = 0 + """Temperature 2 of the ADC when each image was captured.""" + + temperature_adc_3: u.Quantity | na.AbstractScalar = 0 + """Temperature 3 of the ADC when each image was captured.""" + + temperature_adc_4: u.Quantity | na.AbstractScalar = 0 + """Temperature 4 of the ADC when each image was captured.""" + + @property + def type_abstract(self: Self) -> Type[Self]: + return ImageHeader + + @property + def type_explicit(self: Self) -> Type[Self]: + return ImageHeader + + @property + def type_matrix(self) -> Type[na.AbstractCartesianMatrixArray]: + raise NotImplementedError + + @classmethod + def from_scalar( + cls: Type[Self], + scalar: na.AbstractScalar, + like: None | na.AbstractExplicitVectorArray = None, + ) -> Self: + raise NotImplementedError From 396b7d6c1b6b3df6d69c5e64a8638636355529df Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 15 Jul 2025 15:03:51 -0600 Subject: [PATCH 2/9] black --- msfc_ccd/_images/_vectors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/msfc_ccd/_images/_vectors.py b/msfc_ccd/_images/_vectors.py index a283746..f0c7d17 100644 --- a/msfc_ccd/_images/_vectors.py +++ b/msfc_ccd/_images/_vectors.py @@ -73,8 +73,8 @@ def type_matrix(self) -> Type[na.AbstractCartesianMatrixArray]: @classmethod def from_scalar( - cls: Type[Self], - scalar: na.AbstractScalar, - like: None | na.AbstractExplicitVectorArray = None, + cls: Type[Self], + scalar: na.AbstractScalar, + like: None | na.AbstractExplicitVectorArray = None, ) -> Self: raise NotImplementedError From 01027339d31aec6a94a38daae1a0e710d6c56c16 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 15 Jul 2025 15:04:02 -0600 Subject: [PATCH 3/9] tests --- msfc_ccd/_tests/test_fits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msfc_ccd/_tests/test_fits.py b/msfc_ccd/_tests/test_fits.py index 574f138..2ed7c89 100644 --- a/msfc_ccd/_tests/test_fits.py +++ b/msfc_ccd/_tests/test_fits.py @@ -23,4 +23,4 @@ def test_open(path: str | pathlib.Path | na.AbstractScalarArray): result = msfc_ccd.fits.open(path) assert isinstance(result, msfc_ccd.SensorData) - assert result.data.sum() != 0 + assert result.outputs.sum() != 0 From 612cd8abfbb184023db962b000b3640ebe15ef62 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 15 Jul 2025 15:17:48 -0600 Subject: [PATCH 4/9] black --- msfc_ccd/_images/_vectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msfc_ccd/_images/_vectors.py b/msfc_ccd/_images/_vectors.py index f0c7d17..dcce3b9 100644 --- a/msfc_ccd/_images/_vectors.py +++ b/msfc_ccd/_images/_vectors.py @@ -6,7 +6,7 @@ import named_arrays as na __all__ = [ - "ImageHeader" + "ImageHeader", ] From 88cb26308d04ddaea8ad5456cf4f03631f3eaa73 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 15 Jul 2025 15:27:36 -0600 Subject: [PATCH 5/9] doc fixes --- msfc_ccd/_images/_sensor_images.py | 2 +- msfc_ccd/_images/_tap_images.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/msfc_ccd/_images/_sensor_images.py b/msfc_ccd/_images/_sensor_images.py index 6443517..58279a0 100644 --- a/msfc_ccd/_images/_sensor_images.py +++ b/msfc_ccd/_images/_sensor_images.py @@ -121,7 +121,7 @@ class SensorData( constrained_layout=True, ) im = na.plt.imshow( - image.data, + image.outputs, axis_x=axis_x, axis_y=axis_y, ax=ax, diff --git a/msfc_ccd/_images/_tap_images.py b/msfc_ccd/_images/_tap_images.py index 51df5e8..4e2c662 100644 --- a/msfc_ccd/_images/_tap_images.py +++ b/msfc_ccd/_images/_tap_images.py @@ -89,17 +89,17 @@ class TapData( # Display the four images fig, axs = na.plt.subplots( axis_rows=axis_tap_y, - nrows=taps.data.shape[axis_tap_y], + nrows=taps.outputs.shape[axis_tap_y], axis_cols=axis_tap_x, - ncols=taps.data.shape[axis_tap_x], + ncols=taps.outputs.shape[axis_tap_x], sharex=True, sharey=True, constrained_layout=True, ); axs = axs[{axis_tap_y: slice(None, None, -1)}] na.plt.pcolormesh( - *taps.pixel.values(), - C=taps.data, + taps.inputs.pixel, + C=taps.outputs, ax=axs, ); From 1ba666d7143c045bfff34e17107dacb80f1d66ed Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 15 Jul 2025 15:47:14 -0600 Subject: [PATCH 6/9] coverage --- msfc_ccd/_images/_vectors.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/msfc_ccd/_images/_vectors.py b/msfc_ccd/_images/_vectors.py index dcce3b9..4ab4229 100644 --- a/msfc_ccd/_images/_vectors.py +++ b/msfc_ccd/_images/_vectors.py @@ -60,15 +60,15 @@ class ImageHeader( """Temperature 4 of the ADC when each image was captured.""" @property - def type_abstract(self: Self) -> Type[Self]: + def type_abstract(self: Self) -> Type[Self]: # pragma: nocover return ImageHeader @property - def type_explicit(self: Self) -> Type[Self]: + def type_explicit(self: Self) -> Type[Self]: # pragma: nocover return ImageHeader @property - def type_matrix(self) -> Type[na.AbstractCartesianMatrixArray]: + def type_matrix(self) -> Type[na.AbstractCartesianMatrixArray]: # pragma: nocover raise NotImplementedError @classmethod @@ -76,5 +76,5 @@ def from_scalar( cls: Type[Self], scalar: na.AbstractScalar, like: None | na.AbstractExplicitVectorArray = None, - ) -> Self: + ) -> Self: # pragma: nocover raise NotImplementedError From a9f749a20aa867a5a8b513061029dd61cdbf81e1 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 15 Jul 2025 15:55:40 -0600 Subject: [PATCH 7/9] doc fixes --- msfc_ccd/fits.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msfc_ccd/fits.py b/msfc_ccd/fits.py index 531e941..3d68a1b 100644 --- a/msfc_ccd/fits.py +++ b/msfc_ccd/fits.py @@ -67,7 +67,7 @@ def open( constrained_layout=True, ) im = na.plt.imshow( - image.data, + image.outputs, axis_x=axis_x, axis_y=axis_y, ax=ax, @@ -114,7 +114,7 @@ def open( constrained_layout=True, ) im = na.plt.imshow( - image.data, + image.outputs, axis_x=axis_x, axis_y=axis_y, ax=axs, From 205adc6656d0d2cf76cee7c1d88ad7d9beeec477 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 15 Jul 2025 16:04:26 -0600 Subject: [PATCH 8/9] docfixes --- msfc_ccd/fits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msfc_ccd/fits.py b/msfc_ccd/fits.py index 3d68a1b..09092ad 100644 --- a/msfc_ccd/fits.py +++ b/msfc_ccd/fits.py @@ -109,7 +109,7 @@ def open( # Display the sample images fig, axs = na.plt.subplots( axis_rows=axis_time, - nrows=image.data.shape[axis_time], + nrows=image.outputs.shape[axis_time], sharex=True, constrained_layout=True, ) From b8c40ccaadbd50e8e55e7834107038431ae6035e Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 15 Jul 2025 16:07:53 -0600 Subject: [PATCH 9/9] Added docstring --- msfc_ccd/_images/_vectors.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/msfc_ccd/_images/_vectors.py b/msfc_ccd/_images/_vectors.py index 4ab4229..d5b3c0b 100644 --- a/msfc_ccd/_images/_vectors.py +++ b/msfc_ccd/_images/_vectors.py @@ -14,6 +14,11 @@ class ImageHeader( na.AbstractExplicitCartesianVectorArray, ): + """ + A class designed to represent the header information for a sequence + of images. + """ + pixel: na.AbstractCartesian2dVectorArray = dataclasses.MISSING """The indices of each pixel in the image."""