Source code for exosim.tasks.subexposures.prepare_instantaneous_read_out

import astropy.units as u
import numpy as np
from numba import jit, prange

import exosim.output as output
from exosim.tasks.subexposures.compute_reading_scheme import ComputeReadingScheme
from exosim.tasks.subexposures.estimate_ch_jitter import EstimateChJitter
from exosim.tasks.task import Task
from exosim.utils.checks import check_units
from exosim.utils.iterators import searchsorted


[docs] class PrepareInstantaneousReadOut(Task): """ This task prepares the instantaneous read out. It calls :class:`~exosim.tasks.subexposures.compute_reading_scheme.ComputeReadingScheme` to compute the ramp sampling scheme, and :class:`~exosim.tasks.subexposures.estimate_ch_jitter.EstimateChJitter` to scale the input pointing jitter to the focal planet pixel units. The jittering is based on the focal plane oversampling factor. Returns -------- dict readout_parameters dict :class:`~astropy.units.Quantity` sub-exposures integration times """ def __init__(self): """ Parameters ---------- main_parameters: dict main parameters dict focal_plane: :class:`~exosim.models.signal.CountsPerSecond` channel focal plane pointing_jitter: (:class:`~astropy.units.Quantity`, :class:`~astropy.units.Quantity`, :class:`~astropy.units.Quantity`) Tuple containing the pointing jitter in the spatial and spectral direction expressed in units of deg, and jitter time expressed as sec. parameters: dict dictionary containing the channel parameters. This is usually parsed from :class:`~exosim.tasks.load.load_options.LoadOptions` output: str or :class:`~exosim.output.hdf5.hdf5.HDF5Output` or :class:`~exosim.output.hdf5.hdf5.HDF5OutputGroup` output file """ self.add_task_param("parameters", "channel parameters dict") self.add_task_param("main_parameters", "channel parameters dict") self.add_task_param("focal_plane", "loaded focal plane") self.add_task_param("pointing_jitter", "") self.add_task_param("output_file", "output file")
[docs] self.store_dict = {}
[docs] def execute(self): parameters = self.get_task_param("parameters") main_parameters = self.get_task_param("main_parameters") pointing_jitter = self.get_task_param("pointing_jitter") focal_plane = self.get_task_param("focal_plane") output_file = self.get_task_param("output_file") osf = focal_plane.metadata["oversampling"] if pointing_jitter != (None, None, None): estimateChJitter = EstimateChJitter() jitter_spe, jitter_spa, y_jit, x_jit, jit_time = estimateChJitter( pointing_jitter=pointing_jitter, parameters=parameters ) new_freq = check_units(jit_time[1] - jit_time[0], "Hz") readout_freq = check_units(parameters["readout"]["readout_frequency"], "Hz") readout_oversampling = new_freq / readout_freq else: readout_oversampling = 1 computeReadingScheme = ComputeReadingScheme() ( clock, base_mask, frame_sequence, number_of_exposures, ) = computeReadingScheme( parameters=parameters, main_parameters=main_parameters, readout_oversampling=readout_oversampling, output_file=output_file, ) # Number of frames contributing to each NDR ndr_end_cumulative_sequence = np.ma.array( frame_sequence.cumsum(), mask=np.tile(np.logical_not(base_mask), number_of_exposures), ).compressed() base_mask_start = base_mask base_mask_start[0] = 1 base_mask_start[-2] = 0 ndr_start_cumulative_sequence = np.ma.array( frame_sequence.cumsum(), mask=np.tile(np.logical_not(base_mask_start), number_of_exposures), ).compressed() ndr_integration_times = ( ndr_end_cumulative_sequence - ndr_start_cumulative_sequence ) * clock focal_time_sampled = (focal_plane.time * focal_plane.time_units).to(u.s) fp_time = searchsorted( focal_time_sampled, ndr_start_cumulative_sequence * clock ) self.store_dict = { "number_of_exposures": number_of_exposures, "frame_sequence": frame_sequence, "ndr_end_cumulative_sequence": ndr_end_cumulative_sequence, "ndr_start_cumulative_sequence": ndr_start_cumulative_sequence, "ndr_integration_times": ndr_integration_times, "simulation_clock": clock, "fp_time": fp_time, } if pointing_jitter != (None, None, None): self.debug("Pointing jitter found") # check if the jitter is well sampled # take min rms if min is not zero jitter_rms_array = np.array( [np.sqrt(np.mean(y_jit**2)), np.sqrt(np.mean(x_jit**2))] ) try: jitter_rms = min(jitter_rms_array[np.nonzero(jitter_rms_array)]) except ValueError: self.debug("jitter rms is zero in both directions") jitter_rms = 0 self.debug(f"jitter rms: {jitter_rms:.2f}") # check if specific resolution is required else set it to 3 by default try: jitter_res = parameters["detector"]["jitter_rms_min_resolution"] self.debug("found jitter rms minimum resolution") except KeyError: jitter_res = 3.0 # check if magnification is wanted or needed mag = 1 # check if magnification is needed if jitter_rms != 0 and jitter_rms < jitter_res: self.debug("jitter rms < 3: focal plane resampling needed") mag = np.ceil(jitter_res / jitter_rms).astype(int) # check if magnification is suggested try: forced_mag = parameters["detector"]["jitter_resampler_mag"] self.debug("found jitter resampler magnification") except KeyError: forced_mag = None # if magnification is suggested choose between the suggested and the computed if forced_mag: if forced_mag < mag: self.warning( f"suggested jitter magnification ({forced_mag}) is too small: {mag} used instead" ) else: mag = forced_mag # producing diagnostic info spe_jit_ave = ( self._average_pointing( jitter_spe.value, ndr_start_cumulative_sequence, ndr_end_cumulative_sequence, ) * jitter_spe.unit ) spa_jit_ave = ( self._average_pointing( jitter_spa.value, ndr_start_cumulative_sequence, ndr_end_cumulative_sequence, ) * jitter_spa.unit ) x_jit_ave = ( self._average_pointing( x_jit, ndr_start_cumulative_sequence, ndr_end_cumulative_sequence, ) / osf * u.pix ) y_jit_ave = ( self._average_pointing( y_jit, ndr_start_cumulative_sequence, ndr_end_cumulative_sequence, ) / osf * u.pix ) jit_indexes = [] for start, stop in zip( ndr_start_cumulative_sequence, ndr_end_cumulative_sequence, strict=False, ): jit_indexes.append(np.arange(start, stop).astype(int)) self.store_dict.update( { "y_jit": y_jit / osf * u.pix, "x_jit": x_jit / osf * u.pix, "y_jit_averaged": y_jit_ave, "x_jit_averaged": x_jit_ave, "spe_jit_averaged": spe_jit_ave, "spa_jit_averaged": spa_jit_ave, "jit_indexes": {"se": jit_indexes}, "mag": mag, "effective_osf": osf * mag, } ) for k, v in self.store_dict.items(): self.debug(f"{k}: {v}") if issubclass(output_file.__class__, output.Output): output_file.store_dictionary( self.store_dict, "instantaneous_readout_params" ) self.set_output([self.store_dict, ndr_integration_times])
@staticmethod @jit(nopython=True, parallel=True) def _average_pointing( jitter: np.ndarray, start: np.ndarray, stop: np.ndarray ) -> np.ndarray: jitter = jitter.astype(np.float64) start = start.astype(np.int64) stop = stop.astype(np.int64) out = np.empty(start.shape[0], dtype=np.float64) for i in prange(start.shape[0]): s = start[i] e = stop[i] acc = 0.0 for j in range(s, e): acc += jitter[j] out[i] = acc / (e - s) return out