Source code for exosim.tasks.instrument.create_focal_plane_array

import gc

import astropy.units as u
import numpy as np

import exosim.tasks.instrument as instrument
from exosim.models.signal import Signal
from exosim.tasks.task import Task
from exosim.utils.klass_factory import find_task


[docs] class CreateFocalPlaneArray(Task): """ It produces the focal plane array Returns ------- :class:`~exosim.models.signal.Signal` focal plane array (no time evolution) """ def __init__(self): """ Parameters __________ parameters: dict channel parameter dictionary. This is usually parsed from :class:`~exosim.tasks.load.load_options.LoadOptions` efficiency: :class:`~exosim.models.signal.Dimensionless` channel efficiency """ self.add_task_param("parameters", "channel parameters dict") self.add_task_param("efficiency", "channel efficiency")
[docs] def execute(self): parameters = self.get_task_param("parameters") focal_plane_dimension = ( parameters["detector"]["spatial_pix"], parameters["detector"]["spectral_pix"], ) self.debug(f"focal plane dimensions: {focal_plane_dimension}") if "oversampling" in parameters["detector"]: if "irf_task" in parameters["detector"]: oversampling = parameters["detector"]["oversampling"] else: self.warning( "The oversampling factor is set but irf_task is not provided in detector configuration. Oversampling factor is set to 1." ) oversampling = 1 else: oversampling = 1 self.debug(f"pixel over sampling factor: {oversampling}") focal_plane_dimension = tuple([d * oversampling for d in focal_plane_dimension]) focal_plane_array = np.zeros(focal_plane_dimension) focal_plane_delta = parameters["detector"]["delta_pix"] / oversampling if parameters["type"].lower() == "spectrometer": # get wavelength solution wl_solution_task = find_task( parameters["wl_solution"]["wl_solution_task"], instrument.LoadWavelengthSolution, ) wl_instance = wl_solution_task() wl_solution = wl_instance(parameters=parameters) # estimate spectral dispersion law spectral_pix_osr = ( np.arange(focal_plane_array.shape[1]) * focal_plane_delta ).to(u.um) spectral_wav_osr = self._wav_osr( wl_solution, "spectral", parameters, spectral_pix_osr ) # estimate spatial dispersion law spatial_pix_osr = ( np.arange(focal_plane_array.shape[0]) * focal_plane_delta ).to(u.um) spatial_wav_osr = self._wav_osr( wl_solution, "spatial", parameters, spatial_pix_osr ) elif parameters["type"].lower() == "photometer": efficiency = self.get_task_param("efficiency") # if we select efficiency > eff_max/e we underestimate the # total flux excluding to many efficiency data idx = np.where( efficiency.data[0, 0] > 0 ) # efficiency.data[0, 0].max() / np.e) x_wav_osr = ( np.linspace( efficiency.spectral[idx].min().item(), efficiency.spectral[idx].max().item(), 32 * oversampling, ) * efficiency.spectral_units ) # 32 is the minimum number of data we want to compute the # derivative. For less data points we have a wavelength # dependent effect if photometers efficiency # x_wav_center = (efficiency.spectral[idx] * # efficiency.data[0, 0, idx]).sum() / \ # efficiency.data[0, 0, idx].sum() spectral_wav_osr = x_wav_osr spatial_wav_osr = x_wav_osr focal_plane_array = Signal( spectral=spectral_wav_osr, spatial=spatial_wav_osr, data=focal_plane_array, metadata={ "focal_plane_delta": focal_plane_delta, "oversampling": oversampling, }, ) try: focal_plane_array.metadata["wl_min"] = parameters["wl_min"] focal_plane_array.metadata["wl_max"] = parameters["wl_max"] except KeyError: pass self.set_output(focal_plane_array) del focal_plane_array gc.collect()
def _wav_osr(self, wl_solution, key, parameters, pix_osr): if np.all(np.asarray(wl_solution[key]) == 0): # no dispersion along this axis: the wavelength is flat wav_osr = np.zeros(pix_osr.size) * u.um else: # Compute the dispersion law (pixel -> wavelength) by fitting # `wl_solution[key]` (pixel positions) -> `wl_solution["wavelength"]`. # To obtain the inverse mapping (wavelength -> pixel) we solve the # polynomial equation p(pixel) = wavelength when needed. par = np.polyfit( wl_solution[key].to(u.um).value, wl_solution["wavelength"].to(u.um).value, 2, ) # Compute the spectral dispersion law spectral_dispersion_law = np.poly1d(par) # estimate center if "center" in parameters["wl_solution"]: pix_osr = self._centering(parameters, wl_solution, pix_osr, key) # walength on each x pixel wav_osr = spectral_dispersion_law(pix_osr.to(u.um).value) * u.um self.debug(f"{key} wavelength solution: {wav_osr}") return wav_osr def _centering(self, parameters, wl_solution, spectral_pix_osr, key="spectral"): """ Shift the pixel array. If "auto" it sets the central wavelength of the channel in the center of the pixel array. If a wavelength is indicated, it centers the wl solution on that wavelength. Else, it shifts the pixel array by the indicated number of pixels. Parameters ---------- parameters : dict Channel parameters dictionary wl_solution : dict Wavelength solution data spectral_pix_osr : astropy.units.Quantity Pixel array to be centered key : str, optional Spectral or spatial key (default: "spectral") Returns ------- astropy.units.Quantity Centered pixel array """ # Get polynomial degree from config or use default poly_degree = parameters["wl_solution"].get("poly_degree", 2) if "center" in parameters["wl_solution"]: # Compute dispersion law (pixel -> wavelength) by fitting # `wl_solution[key]` (pixel positions) -> `wl_solution["wavelength"]`. # To obtain the inverse mapping (wavelength -> pixel) we solve # the polynomial equation p(pixel) = wavelength when needed. par = np.polyfit( wl_solution[key].to(u.um).value, wl_solution["wavelength"].to(u.um).value, poly_degree, ) # Pixel array center position array_center = (spectral_pix_osr[0] + spectral_pix_osr[-1]) / 2 if parameters["wl_solution"]["center"] == "auto": # Auto mode: center on the midpoint of wl_min and wl_max # Solve p(pixel) = wl_min and p(pixel) = wl_max for pixel positions. wl_min_val = parameters["wl_min"].to(u.um).value wl_max_val = parameters["wl_max"].to(u.um).value coeffs_min = par.copy() coeffs_min[-1] -= wl_min_val roots_min = np.roots(coeffs_min) real_min = roots_min[np.isreal(roots_min)].real pix_min_val = real_min if real_min.size else roots_min.real coeffs_max = par.copy() coeffs_max[-1] -= wl_max_val roots_max = np.roots(coeffs_max) real_max = roots_max[np.isreal(roots_max)].real pix_max_val = real_max if real_max.size else roots_max.real # Choose the root closest to the array center in each case center_val = array_center.to(u.um).value first_pixel_val = pix_min_val[ np.argmin(np.abs(pix_min_val - center_val)) ] last_pixel_val = pix_max_val[ np.argmin(np.abs(pix_max_val - center_val)) ] first_pixel = first_pixel_val * u.um last_pixel = last_pixel_val * u.um target_pixel = (first_pixel + last_pixel) / 2 offset = array_center - target_pixel self.debug( "wl solution auto-centering mode: centering on wavelength range midpoint. " f"offset = {offset}" ) elif isinstance(parameters["wl_solution"]["center"], u.Quantity): # Wavelength mode: center on specific wavelength center_wl = parameters["wl_solution"]["center"].to(u.um).value # Validate wavelength is in valid range wl_min = wl_solution["wavelength"].min().to(u.um).value wl_max = wl_solution["wavelength"].max().to(u.um).value if not (wl_min <= center_wl <= wl_max): self.warning( f"Centering wavelength {center_wl} um is outside the " f"wavelength solution range [{wl_min}, {wl_max}] um. " "Extrapolation may be inaccurate." ) # Solve p(pixel) = center_wl for pixel position coeffs = par.copy() coeffs[-1] -= center_wl roots = np.roots(coeffs) real_roots = roots[np.isreal(roots)].real candidates = real_roots if real_roots.size else roots.real center_val = array_center.to(u.um).value chosen_val = candidates[np.argmin(np.abs(candidates - center_val))] target_pixel = chosen_val * u.um offset = array_center - target_pixel self.debug( f"wl solution wavelength-centering mode: centering on {center_wl} um. " f"offset = {offset}" ) else: # Invalid value for center parameter self.error( f"Invalid value for 'center' parameter: {parameters['wl_solution']['center']}. " "Expected 'auto' or astropy Quantity." ) return spectral_pix_osr spectral_pix_osr -= offset else: # Manual mode: apply numeric offset directly center = parameters["wl_solution"][f"{key}_center"] self.debug(f"wl solution manual-centering mode: applying offset {center}") spectral_pix_osr -= center return spectral_pix_osr