Source code for irispy.meta

import textwrap

import numpy as np

import astropy.units as u
from astropy.constants import R_sun as _R_SUN
from astropy.coordinates import SkyCoord
from astropy.time import Time

from ndcube.meta import NDMeta
from sunpy.coordinates import Helioprojective
from sunraster.meta import RemoteSensorMetaABC, SlitSpectrographMetaABC

from irispy.utils.constants import SPECTRAL_BAND

__all__ = ["BaseMeta", "SGMeta", "SJIMeta"]


[docs] class BaseMeta(NDMeta): def __init__(self, header, **kwargs) -> None: super().__init__(header, **kwargs) def __repr__(self) -> str: return f"{object.__repr__(self)}\n{self!s}" def _construct_time(self, key): val = self.get(key) if val is not None: val = Time(val, format="fits", scale="utc") return val @property def fits_header(self): return self._fits_header @property def spectral_window(self): return self.get(f"TDESC{self._iwin}") @property def detector(self): return self.get(f"TDET{self._iwin}") @property def instrument(self): return self.get("INSTRUME") @property def observatory(self): return self.get("TELESCOP") @property def processing_level(self): return int(self.get("DATA_LEV")) @property def camera(self): """ IRIS camera ID - 1 for FUV, 2 for NUV/SJI. """ return self.get("CAMERA") @property def sun_angular_radius(self): """ Apparent angular radius of the Sun at the observer location. """ rsun = self.get("RSUN_OBS") if rsun is not None: return float(rsun) * u.arcsec return (np.arctan(_R_SUN.to(u.m).value / float(self.get("DSUN_OBS"))) * u.rad).to(u.arcsec) @property def observer_radial_velocity(self): """ Radial velocity of the observer relative to the Sun (m/s). """ return float(self.get("OBS_VR")) * u.m / u.s @property def distance_to_sun(self): return (self.get("DSUN_OBS") * u.m).to(u.AU) @property def date_reference(self): return self._construct_time("DATE_OBS") @property def date_start(self): return self.date_reference @property def date_end(self): return self._construct_time("DATE_END") @property def temporal_cadence(self): """ Average time between exposures. """ return float(self.get("CADEX_AV")) * u.s @property def observing_mode_id(self): return int(self.get("OBSID")) # ---------- IRIS-specific metadata properties ---------- @property def observing_mode_description(self): return self.get("OBS_DESC") @property def observing_campaign_start(self): """ Start time of observing campaign. """ return self._construct_time("STARTOBS") @property def observing_campaign_end(self): """ End time of observing mode. """ return self._construct_time("ENDOBS") @property def observation_includes_saa(self): """ Whether IRIS passed through SAA during observations. """ return bool(self.get("SAA")) @property def satellite_rotation(self): """ Satellite roll from solar north. """ return self.get("SAT_ROT") * u.deg @property def exposure_control_triggers_in_observation(self): """ Number of times automatic exposure control triggered during observing campaign. """ return self.get("AECNOBS") @property def exposure_control_triggers_in_raster(self): """ Number of times automatic exposure control was triggered during this raster. """ return self.get("AECNRAS") @property def number_of_unique_raster_positions(self): """ Number of unique positions in raster. """ return self.get("NRASTERP") @property def number_of_raster_positions(self): """ Number of positions in raster. """ return self.get("RASNRPT") @property def spectral_range(self): """ The spectral range of the spectral window. """ return [self.get(f"TWMIN{self._iwin}"), self.get(f"TWMAX{self._iwin}")] * u.AA @property def spectral_band(self): """ The spectral band of the spectral window. """ return SPECTRAL_BAND.get(self.spectral_window, self.spectral_window) @property def detector_band(self): """ Detector band - ``'FUV'``, ``'NUV'``, or ``'SJI'``. """ det_upper = self.detector.upper() return next( (band for band in ("FUV", "NUV", "SJI") if det_upper.startswith(band)), det_upper, ) @property def rest_wavelength(self): """ Rest wavelength of the spectral line for this window. """ return (float(self.get(f"TWAVE{self._iwin}")) * u.AA).to(u.nm) @property def raster_fov_width_y(self): """ Width of the field of view of the raster in the Y (slit) direction. """ return self.get("FOVY") * u.arcsec @property def raster_fov_width_x(self): """ Width of the field of view of the raster in the X (rastering) direction. """ return self.get("FOVX") * u.arcsec @property def fov_center(self): """ Location of the center of the field of view. """ return SkyCoord( Tx=self.get("XCEN"), Ty=self.get("YCEN"), unit=u.arcsec, frame=Helioprojective, ) @property def automatic_exposure_control_enabled(self): return bool(self.get("IAECFLAG")) @property def tracking_mode_enabled(self): return bool(self.get("TR_MODE")) @property def observatory_at_high_latitude(self): """ Whether IRIS passed through high Earth latitude during observations. """ return bool(self.get("HLZ")) @property def spatial_summing_factor(self): """ Number of pixels summed together in the spatial (Y/slit) direction. """ return self.get("SUMSPAT") @property def spectral_summing_factor(self): """ Number of pixels summed together in the spectral direction. """ if "fuv" in self.detector.lower(): return self.get("SUMSPTRF") return self.get("SUMSPTRN") @property def exposure_time(self): """ Mean exposure duration (shutter open time). """ return float(self.get("EXPTIME")) * u.s @property def exposure_time_min(self): """ Minimum exposure duration in this raster/SJI. """ return float(self.get("EXPMIN")) * u.s @property def exposure_time_max(self): """ Maximum exposure duration in this raster/SJI. """ return float(self.get("EXPMAX")) * u.s @property def data_type(self): """ Type of data, e.g. ``'Intensity'``. """ return self.get("BTYPE") @property def data_unit(self): """ Unit of the data values. """ return self.get("BUNIT") @property def data_status(self): """ Processing status. """ return self.get("STATUS") @property def build_version(self): """ Build version from ``jsoc_version.h``. """ return self.get("BLD_VERS") @property def reformat_version(self): """ Version of the software that reformatted the data to Level 2. """ return self.get("VER_RF2") @property def reformat_date(self): """ Date of reformatting to Level 2. """ return self._construct_time("DATE_RF2") @property def observing_label(self): """ Observing list string. """ return self.get("OBSLABEL") @property def observing_title(self): """ Title given by the planner. """ return self.get("OBSTITLE") @property def lut_id(self): """ Look-up table ID. """ return self.get("LUTID") @property def number_of_exposures(self): """ Number of exposures in this raster/SJI. """ return self.get("NEXP") @property def number_of_exposures_planned(self): """ Number of planned exposures in this raster/SJI. """ return self.get("NEXP_PRP") @property def number_of_exposures_observation(self): """ Expected total number of exposures in the whole observation. """ return self.get("NEXPOBS") @property def number_of_saturated_pixels(self): """ Number of saturated pixels. """ return self.get("NSATPIX") @property def number_of_spikes(self): """ Number of pixels identified as noise (cosmic-ray) spikes. """ return self.get("NSPIKES") @property def percent_data(self): """ Percentage of valid data values. """ return self.get("PERCENTD") @property def data_mean(self): """ Mean value of all pixels. """ return self.get("DATAMEAN") @property def data_rms(self): """ RMS deviation from the mean value of all pixels. """ return self.get("DATARMS") @property def data_median(self): """ Median value of all pixels. """ return self.get("DATAMEDN") @property def data_min(self): """ Minimum value of all pixels. """ return self.get("DATAMIN") @property def data_max(self): """ Maximum value of all pixels. """ return self.get("DATAMAX")
[docs] class SJIMeta(BaseMeta, RemoteSensorMetaABC): """ Metadata class for IRIS slit-jaw images. """ def __init__(self, header, **kwargs) -> None: super().__init__(header, **kwargs) self._iwin = 1 self._fits_header = header def __str__(self) -> str: return textwrap.dedent( f""" SJIMeta ------- Observatory: {self.observatory} Instrument: {self.instrument} Detector: {self.detector} Spectral Window: {self.spectral_window} Spectral Range: {self.spectral_range} Spectral Band: {self.spectral_band} Dimensions: {self.data_shape} Date: {self.date_reference} OBS ID: {self.observing_mode_id} OBS Description: {self.observing_mode_description} """, ) @property def spectral_window(self): return super().spectral_window.replace("SJI_", "")
[docs] class SGMeta(BaseMeta, SlitSpectrographMetaABC): """ Metadata class for IRIS slit spectrograph data. """ def __init__(self, header, spectral_window, **kwargs) -> None: super().__init__(header, **kwargs) spectral_windows = np.array([self[f"TDESC{i}"] for i in range(1, self["NWIN"] + 1)]) window_mask = np.array([spectral_window in window for window in spectral_windows]) if window_mask.sum() < 1: msg = ( "Spectral window not found. " f"Input spectral window: {spectral_window}; " f"Spectral windows in header: {spectral_windows}" ) raise ValueError( msg, ) if window_mask.sum() > 1: msg = ( "Spectral window must be unique. " f"Input spectral window: {spectral_window}; " f"Ambiguous spectral windows in header: {spectral_windows[window_mask]}" ) raise ValueError( msg, ) self._iwin = np.arange(len(spectral_windows))[window_mask][0] + 1 self._fits_header = header @property def number_of_spectral_windows(self): """ Number of spectral windows in this observation. """ return self.get("NWIN") @property def raster_repetition(self): """ Current raster repetition counter. """ return self.get("RASRPT") @property def step_size_average(self): """ Average of the basic raster step size. """ return self.get("STEPS_AV") @property def step_size_stddev(self): """ Standard deviation of the basic raster step size. """ return self.get("STEPS_DV") @property def step_time_average(self): """ Average of the basic raster step time. """ return self.get("STEPT_AV") @property def step_time_stddev(self): """ Standard deviation of the basic raster step time. """ return self.get("STEPT_DV") @property def cadence_planned_average(self): """ Mean cadence of the raster as planned. """ return float(self.get("CADPL_AV")) * u.s @property def cadence_planned_stddev(self): """ Standard deviation of the planned raster cadence. """ return float(self.get("CADPL_DV")) * u.s @property def cadence_executed_stddev(self): """ Standard deviation of the executed raster cadence. """ return float(self.get("CADEX_DV")) * u.s @property def raster_type_index(self): """ Raster type number. """ return self.get("RASTYPDX") @property def raster_type_total(self): """ Total number of raster types. """ return self.get("RASTYPNX") @property def number_of_missing_raster_files(self): """ Number of missing Level 1 files in this raster. """ return self.get("MISSRAS") @property def number_of_missing_observation_files(self): """ Number of missing Level 1 files in the whole observation. """ return self.get("MISSOBS") @property def window_mean(self): """ Mean value of all pixels in this spectral window. """ return self.get(f"TDMEAN{self._iwin}") @property def window_rms(self): """ RMS deviation from the mean value of all pixels in this spectral window. """ return self.get(f"TDRMS{self._iwin}") @property def window_median(self): """ Median value of all pixels in this spectral window. """ return self.get(f"TDMEDN{self._iwin}") @property def window_min(self): """ Minimum value of all pixels in this spectral window. """ return self.get(f"TDMIN{self._iwin}") @property def window_max(self): """ Maximum value of all pixels in this spectral window. """ return self.get(f"TDMAX{self._iwin}") @property def window_saturated_pixels(self): """ Number of saturated pixels in this spectral window. """ return self.get(f"TSATPX{self._iwin}") @property def window_spikes(self): """ Number of pixels identified as noise spikes in this spectral window. """ return self.get(f"TSPIKE{self._iwin}") def __str__(self) -> str: return textwrap.dedent( f""" SGMeta ------ Observatory: {self.observatory} Instrument: {self.instrument} Detector: {self.detector} Spectral Window: {self.spectral_window} Spectral Range: {self.spectral_range} Spectral Band: {self.spectral_band} Dimensions: {self.data_shape} Date: {self.date_reference} OBS ID: {self.observing_mode_id} OBS Description: {self.observing_mode_description} """, )