Source code for esis.data._level_0._level_0
from typing_extensions import Self
import dataclasses
import pathlib
import numpy as np
import astropy.units as u
import astropy.time
import named_arrays as na
import msfc_ccd
import esis
from .. import abc
__all__ = [
"Level_0",
]
[docs]
@dataclasses.dataclass(eq=False, repr=False)
class Level_0(
msfc_ccd.SensorData,
abc.AbstractChannelData,
):
"""
Representation of ESIS Level-0 images, the raw data gathered by the instrument.
The Data Acquisition and Control System (DACS) reads out the cameras and
saves the resulting images as FITS files.
This represents those FITS files as a Python class.
"""
timeline: None | esis.nsroc.Timeline = None
"""The sequence of NSROC events associated with these images."""
[docs]
@classmethod
def from_fits(
cls,
path: str | pathlib.Path | na.AbstractScalarArray,
camera: msfc_ccd.abc.AbstractCamera,
axis_x: str = "detector_x",
axis_y: str = "detector_y",
timeline: None | esis.nsroc.Timeline = None,
) -> Self:
self = super().from_fits(
path=path,
camera=camera,
axis_x=axis_x,
axis_y=axis_y,
)
self.timeline = timeline
self.outputs = self.outputs.astype(np.float32)
return self
@property
def time_mission_start(self) -> astropy.time.Time:
"""The :math:`T=0` time of the mission."""
return self.inputs.time.ndarray.min() - self.timeline.timedelta_esis_start
def _index_after(self, timedelta: u.Quantity) -> dict[str, int]:
"""
Return the index of the image after the given mission time.
Parameters
----------
timedelta
The mission time.
"""
time = self.inputs.time_start
if self.axis_channel in time.shape:
time = time.mean(self.axis_channel)
t0 = self.time_mission_start
t = t0 + timedelta
where = time > t
return np.argmax(where)
@property
def _index_lights_start(self) -> dict[str, int]:
"""The index representing the first good image."""
return self._index_after(self.timeline.timedelta_sparcs_rlg_enable)
@property
def _index_lights_stop(self) -> dict[str, int]:
"""One greater than the index representing the last good image."""
return self._index_after(self.timeline.timedelta_sparcs_rlg_disable)
@property
def lights(self) -> Self:
"""
The sequence of solar images taken during the flight.
This uses only the images where the ring-laser gyroscope was enabled,
so this should represent the images with the best-possible pointing stability.
"""
axis_time = self.axis_time
index_start = self._index_lights_start[axis_time].ndarray
index_stop = self._index_lights_stop[axis_time].ndarray
index_lights = {axis_time: slice(index_start, index_stop)}
return self[index_lights]
@property
def darks_up(self) -> Self:
"""
The dark images collected on the upleg of the trajectory.
This considers all the images up until the moment the shutter door
is opened.
Any images without an exposure time close to the median exposure
time are ignored.
This is intended to remove the first 1 or 2 images from the
beginning of each exposure sequence since these images often have
a different exposure time than the rest of the sequence.
"""
axis_time = self.axis_time
dt = self.inputs.timedelta_requested.mean(self.axis_channel)
where = np.abs(dt - dt.median()) < (0.1 * u.s)
index_start = np.argmax(where)[axis_time].ndarray
index_stop = self._index_after(self.timeline.timedelta_shutter_open)[
axis_time
].ndarray
index = {axis_time: slice(index_start, index_stop)}
return self[index]
@property
def darks_down(self) -> Self:
"""
The dark images collected on the downleg of the trajectory.
This considers all the images after the parachute deployment since there
is a transient, anomalous signal that occurs during atmospheric re-entry.
"""
axis_time = self.axis_time
index_start = self._index_after(self.timeline.timedelta_parachute_deploy)[
axis_time
].ndarray
index_stop = None
index = {axis_time: slice(index_start, index_stop)}
return self[index]
@property
def darks(self) -> Self:
"""
The dark images used to construct the master dark image.
This is a concatenation of :attr:`darks_up` and :attr:`darks_down`.
"""
result = np.concatenate(
arrays=[self.darks_up, self.darks_down],
axis=self.axis_time,
)
serial_number = result.inputs.serial_number
for axis in serial_number.shape:
sn0 = serial_number[{axis: 0}]
if np.all(sn0 == serial_number):
serial_number = sn0
result.inputs.serial_number = serial_number
return result
@property
def dark(self) -> Self:
r"""
The master dark image for each channel.
Calculated by taking the mean of :attr:`darks`.\ :attr:`despiked`
along :attr:`axis_time`.
"""
return self.darks.despiked.mean(axis=self.axis_time)
@property
def dark_subtracted(self):
"""Subtract the master :attr:`dark` from each image in the sequence."""
return self - self.dark.outputs
@property
def despiked(self) -> Self:
"""Remove cosmic rays using :func:`astroscrappy.detect_cosmics`."""
return na.despike(
array=self,
sigclip=6,
objlim=3,
axis=(self.axis_x, self.axis_y),
)