diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3514b950..3ccd10ab 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: python-version: ["3.11"] name: ${{ matrix.os }}, Python ${{ matrix.python-version }} tests steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: lfs: true - name: Pull LFS objects @@ -36,10 +36,10 @@ jobs: pip install pytest pytest-cov pytest --cov=. --cov-report=xml --cov-report=html - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - file: coverage.xml + files: coverage.xml flags: unittests env_vars: OS,PYTHON name: codecov-umbrella diff --git a/README.md b/README.md index 39e78d39..456580c8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ESIS -![tests](https://github.com/Kankelborg-Group/ESIS/workflows/tests/badge.svg) +[![tests](https://github.com/Kankelborg-Group/ESIS/actions/workflows/tests.yml/badge.svg)](https://github.com/Kankelborg-Group/ESIS/actions/workflows/tests.yml) [![codecov](https://codecov.io/gh/Kankelborg-Group/ESIS/graph/badge.svg?token=CALT5W6YG3)](https://codecov.io/gh/Kankelborg-Group/ESIS) [![Black](https://github.com/Kankelborg-Group/ESIS/actions/workflows/black.yml/badge.svg)](https://github.com/Kankelborg-Group/ESIS/actions/workflows/black.yml) [![Ruff](https://github.com/Kankelborg-Group/ESIS/actions/workflows/ruff.yml/badge.svg)](https://github.com/Kankelborg-Group/ESIS/actions/workflows/ruff.yml) diff --git a/docs/make.bat b/docs/make.bat index 0cef56e4..1332e88f 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b593cd1da19314a43db9a0854f6f92f2c59217571502da3321f9498c02fa16c8 -size 795 +oid sha256:bcf205ef2f3026bb71a0ed44d71afef0519162e1854d66bbb782dc864e3a05d7 +size 803 diff --git a/esis/__init__.py b/esis/__init__.py index 63b2dfeb..671d327d 100644 --- a/esis/__init__.py +++ b/esis/__init__.py @@ -4,10 +4,12 @@ from . import optics from . import nsroc +from . import data from . import flights __all__ = [ "optics", "nsroc", + "data", "flights", ] diff --git a/esis/data/__init__.py b/esis/data/__init__.py new file mode 100644 index 00000000..f25026f5 --- /dev/null +++ b/esis/data/__init__.py @@ -0,0 +1,18 @@ +""" +Represent and process ESIS observations into spatial-spectral cubes. + +Description of the ESIS Data Levels +=================================== + +Level 0 +------- + +* The raw data gathered by the ESIS instrument, saved as FITS files. + +""" + +from ._level_0 import Level_0 + +__all__ = [ + "Level_0", +] diff --git a/esis/data/_level_0/__init__.py b/esis/data/_level_0/__init__.py new file mode 100644 index 00000000..8c1f8546 --- /dev/null +++ b/esis/data/_level_0/__init__.py @@ -0,0 +1,5 @@ +from ._level_0 import Level_0 + +__all__ = [ + "Level_0", +] diff --git a/esis/data/_level_0/_level_0.py b/esis/data/_level_0/_level_0.py new file mode 100644 index 00000000..b2182c5f --- /dev/null +++ b/esis/data/_level_0/_level_0.py @@ -0,0 +1,71 @@ +import numpy as np +from typing_extensions import Self +import dataclasses +import pathlib +import numpy.typing as npt +import named_arrays as na +import msfc_ccd +import esis + +__all__ = [ + "Level_0", +] + + +@dataclasses.dataclass(eq=False, repr=False) +class Level_0( + msfc_ccd.SensorData, +): + """ + Representation of ESIS Level-0 images, the raw images gathered by the + Data Acquisition and Control System (DACS). + """ + + timeline: None | esis.nsroc.Timeline = None + """ + The sequence of NSROC events associated with these images. + """ + + @classmethod + def from_fits( + cls, + path: str | pathlib.Path | na.AbstractScalarArray, + sensor: msfc_ccd.abc.AbstractSensor, + axis_x: str = "detector_x", + axis_y: str = "detector_y", + timeline: None | esis.nsroc.Timeline = None, + ) -> Self: + + self = super().from_fits( + path=path, + sensor=sensor, + axis_x=axis_x, + axis_y=axis_y, + ) + + self.timeline = timeline + + self.inputs + + return self + + @property + def channel(self) -> na.ScalarArray[npt.NDArray[str]]: + """ + The name of each ESIS channel in a human-readable format. + """ + + sn = self.inputs.serial_number + where_1 = sn == "6" + where_2 = sn == "7" + where_3 = sn == "9" + where_4 = sn == "1" + + result = np.empty_like(sn, dtype=object) + + result[where_1] = "Channel 1" + result[where_2] = "Channel 2" + result[where_3] = "Channel 3" + result[where_4] = "Channel 4" + + return result diff --git a/esis/data/_level_0/_level_0_test.py b/esis/data/_level_0/_level_0_test.py new file mode 100644 index 00000000..05a91b38 --- /dev/null +++ b/esis/data/_level_0/_level_0_test.py @@ -0,0 +1,18 @@ +import pytest +from msfc_ccd._images._tests.test_sensor_images import AbstractTestAbstractSensorData +import esis + + +@pytest.mark.parametrize( + argnames="a", + argvalues=[ + esis.flights.f1.data.level_0(), + ], +) +class TestLevel_0( + AbstractTestAbstractSensorData, +): + def test_timeline(self, a: esis.data.Level_0): + result = a.timeline + if result is not None: + assert isinstance(result, esis.nsroc.Timeline) diff --git a/esis/flights/f1/data/__init__.py b/esis/flights/f1/data/__init__.py index 19468e3e..9cb2ad40 100644 --- a/esis/flights/f1/data/__init__.py +++ b/esis/flights/f1/data/__init__.py @@ -3,7 +3,9 @@ """ from ._fits import path_fits +from ._level_0 import level_0 __all__ = [ "path_fits", + "level_0", ] diff --git a/esis/flights/f1/data/_level_0/__init__.py b/esis/flights/f1/data/_level_0/__init__.py new file mode 100644 index 00000000..cef2a963 --- /dev/null +++ b/esis/flights/f1/data/_level_0/__init__.py @@ -0,0 +1,5 @@ +from ._level_0 import level_0 + +__all__ = [ + "level_0", +] diff --git a/esis/flights/f1/data/_level_0/_level_0.py b/esis/flights/f1/data/_level_0/_level_0.py new file mode 100644 index 00000000..2199b71d --- /dev/null +++ b/esis/flights/f1/data/_level_0/_level_0.py @@ -0,0 +1,158 @@ +import msfc_ccd +import esis +from ... import nsroc +from .. import path_fits + +__all__ = [ + "level_0", +] + + +def level_0( + axis_time: str = "time", + axis_channel: str = "channel", + axis_x: str = "detector_x", + axis_y: str = "detector_y", +) -> esis.data.Level_0: + """ + All the raw images captured by ESIS during the 2019 flight. + + Parameters + ---------- + axis_time + The name of the logical axis representing time. + axis_channel + The name of the logical axis representing the different ESIS channels. + axis_x + The name of the logical axis representing the detector's long axis. + axis_y + The name of the logical axis representing the detector's short axis. + + Examples + -------- + + Load the Level-0 dataset into a :class:`esis.data.Level_0` instance. + + .. jupyter-execute:: + + import IPython.display + import matplotlib.pyplot as plt + import astropy.visualization + import named_arrays as na + import esis + + # Define the names of the logical axes + # to use for constructing the Level-0 dataset + axis_time = "time" + axis_channel = "channel" + + # Load the Level-0 dataset into memory + level_0 = esis.flights.f1.data.level_0( + axis_time=axis_time, + axis_channel=axis_channel, + ) + + Make a movie of three frames of the Level-0 dataset. + + .. jupyter-execute:: + + # Define a slice of three frames near apogee + index = {axis_time: slice(20, 23)} + + # Create a figure + fig, axs = na.plt.subplots( + axis_rows="rows", + axis_cols="cols", + nrows=level_0.shape[axis_channel] // 2, + ncols=2, + sharex=True, + sharey=True, + constrained_layout=True, + figsize=(10, 5), + ) + + # Reorganize the axes into a flat array + ax = axs.combine_axes(("rows", "cols"), axis_channel) + ax = ax[{axis_channel: slice(None, None, -1)}] + + # Define the colormap + colorizer = plt.Colorizer( + norm=plt.Normalize( + vmin=level_0.outputs.percentile(1).ndarray, + vmax=level_0.outputs.percentile(99).ndarray, + ), + ) + + # Animate the Level-0 dataset frames + ani = na.plt.pcolormovie( + level_0.inputs.time[index].mean(axis_channel), + level_0.inputs.pixel.x, + level_0.inputs.pixel.y, + C=level_0.outputs[index], + axis_time=axis_time, + ax=ax, + kwargs_pcolormesh=dict( + colorizer=colorizer, + ), + ) + + # Create labels for each axis + na.plt.text( + x=0.5, + y=1.01, + s=level_0.channel, + transform=na.plt.transAxes(ax), + ax=ax, + ha="center", + va="bottom", + ) + na.plt.set_aspect("equal", ax=ax) + na.plt.set_xlabel("detector $x$ (pix)", ax=axs[dict(rows=0)]) + na.plt.set_ylabel("detector $y$ (pix)", ax=axs[dict(cols=0)]) + + # Plot the colorbar using the colormap + plt.colorbar( + mappable=plt.cm.ScalarMappable(colorizer=colorizer), + ax=ax.ndarray, + label="signal (DN)" + ) + + # Render the movie as a javascript animation + plt.close(fig) + IPython.display.HTML(ani.to_jshtml()) + + Plot the FPGA temperatures over the flight. + + .. jupyter-execute:: + + # Convert the time array from ISO to a Python :class:`datetime.datetime` + # instance + time = level_0.inputs.time + time = time.replace(ndarray=time.ndarray.datetime) + + # Plot the result as a line plot + with astropy.visualization.quantity_support(): + fig, ax = plt.subplots() + na.plt.plot( + time, + level_0.inputs.temperature_fpga, + axis=axis_time, + ax=ax, + label=level_0.channel, + ) + ax.set_ylabel(f"FPGA temperature ({ax.get_ylabel()})") + ax.legend() + """ + + path = path_fits( + axis_time=axis_time, + axis_channel=axis_channel, + ) + + return esis.data.Level_0.from_fits( + path=path, + sensor=msfc_ccd.TeledyneCCD230(), + axis_x=axis_x, + axis_y=axis_y, + timeline=nsroc.timeline(), + ) diff --git a/esis/flights/f1/data/_level_0/_level_0_test.py b/esis/flights/f1/data/_level_0/_level_0_test.py new file mode 100644 index 00000000..54ca2c52 --- /dev/null +++ b/esis/flights/f1/data/_level_0/_level_0_test.py @@ -0,0 +1,7 @@ +import esis + +def test_level_0(): + + lvl0 = esis.flights.f1.data.level_0() + + assert isinstance(lvl0, esis.data.Level_0) diff --git a/esis/flights/f1/data/level_0.py b/esis/flights/f1/data/level_0.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pyproject.toml b/pyproject.toml index 2d926024..2c4e8f69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,8 +17,9 @@ dependencies = [ "numpy", "matplotlib", "astropy", - "optika==0.11.0", - "aastex==0.3.1", + "optika>=0.12.0", + "msfc-ccd>=0.2.0", + "aastex>=0.3.1", ] dynamic = ["version"] @@ -49,6 +50,7 @@ packages = ["esis"] testpaths = [ "esis/optics/_tests", "esis/flights", + "esis/data", "esis/science/papers/instrument/_tests", ]