Source code for solpolpy.instruments

"""Instrument specific code."""

import astropy.units as u
import numpy as np
from astropy.io import fits
from astropy.wcs import WCS
from ndcube import NDCollection, NDCube

from solpolpy.errors import TooFewFilesError, UnsupportedInstrumentError


[docs] def get_data_angle(header): angle_hdr = header["POLAR"] if isinstance(angle_hdr, float): angle = angle_hdr * u.degree elif isinstance(angle_hdr, str): angle = float(angle_hdr.split("Deg")[0]) * u.degree else: try: angle = float(angle_hdr) * u.degree except ValueError: msg = "Polar angle in the header could not be read for this instrument." raise UnsupportedInstrumentError(msg) return angle
[docs] def load_data(path_list: list[str], mask: np.ndarray | None = None, use_instrument_mask: bool = False, hdu_index: int = 0) -> NDCollection: """Basic loading function. See `load_with_occulter_mask`. Parameters ---------- path_list: List[str] list of paths to FITS files to be loaded mask: Optional[np.ndarray] An optional mask to couple with images. Overrides any mask computed when use_instrument_mask=True. use_instrument_mask: bool If true, loads an instrument mask for common instruments defined in `get_instrument_mask`. hdu_index: int The index of the HDU (zero-based) to load data from. For many spacecraft this should be 0. For PUNCH it should be set to 1. Returns ------- NDCollection The data are loaded as NDCollection object with WCS and header information available. The keys are labeled as 'angle_1', 'angle_2, 'angle_3', ... """ # get length of list to determine how many files to process. if len(path_list) < 2: msg = "Requires at least 2 FITS files" raise TooFewFilesError(msg) data_out = [] for _i, data_path in enumerate(path_list): with fits.open(data_path) as hdul: wcs = WCS(hdul[hdu_index].header) if use_instrument_mask and mask is None: mask = get_instrument_mask(hdul[hdu_index].header) if mask is None: # make a mask of False if none is provided mask = np.zeros(hdul[hdu_index].data.shape, dtype=bool) angle = get_data_angle(hdul[hdu_index].header) data_out.append((str(angle), NDCube(hdul[hdu_index].data, mask=mask, wcs=wcs, meta=dict(hdul[hdu_index].header)))) return NDCollection(data_out, meta={}, aligned_axes="all")
[docs] def construct_mask(inner_radius: float, outer_radius: float, center_x: int, center_y: int, shape: tuple[int, int]) -> np.ndarray: """Constructs a mask where False indicates the pixel is valid and True masks the pixel as invalid. Pixels with radial distance between `inner_radius` and `outer_radius` are valid. Every other pixel is invalid. This is a standard coronograph mask. Parameters ---------- inner_radius : float inner radius of the mask. pixels closer to the center than this are masked as invalid outer_radius : float outer radius of the mask. pixels farther from the center than this are masked as invalid center_x : int center pixel x coordinate center_y : int center pixel y coordinate shape : Tuple[int, int] the image dimensions Returns ------- np.ndarray a coronograph mask that marks invalid pixels (those with radius less than `inner_radius` or greater than `outer_radius`) as True """ xx, yy = np.ogrid[0:shape[0], 0:shape[1]] mask = np.zeros(shape, dtype=bool) mask[(xx - center_x) ** 2 + (yy - center_y) ** 2 < inner_radius] = True mask[(xx - center_x) ** 2 + (yy - center_y) ** 2 > outer_radius] = True return mask
[docs] def get_instrument_mask(header: fits.Header) -> np.ndarray: """Gets a coronograph mask for common instruments. Supports common solar instruments: LASCO, COSMO K, SECCHI Parameters ---------- header : astropy.io.fits.Header a FITS header for the file in question Returns ------- np.ndarray a pixel mask where valid pixels are marked False, masked out invalid pixels are flagged as True """ x_shape = header["NAXIS1"] y_shape = header["NAXIS2"] center_x = header["CRPIX1"] center_y = header["CRPIX2"] R_sun = 960. / header["CDELT1"] # TODO: clarify where 960. comes from radius = 0 if header["INSTRUME"] == "LASCO": if "C2" in header["DETECTOR"]: radius = 2.5 elif "C3" in header["DETECTOR"]: radius = 4.0 else: msg = "The data in this file is not formatted according to a known instrument." raise UnsupportedInstrumentError(msg) elif header["INSTRUME"] == "COSMO K-Coronagraph": radius = 1.15 elif header["INSTRUME"] == "SECCHI": if "COR1" in header["DETECTOR"]: radius = 1.57 elif "COR2" in header["DETECTOR"]: radius = 3.0 else: msg = "The data in this file is not formatted according to a known instrument." raise UnsupportedInstrumentError(msg) else: msg = "The data in this file is not formatted according to a known instrument." raise UnsupportedInstrumentError(msg) y_array = [(j_step - center_y) / R_sun for j_step in range(y_shape)] outer_edge = np.max(y_array) * R_sun return construct_mask((radius * R_sun) ** 2, outer_edge ** 2, center_x, center_y, (x_shape, y_shape))