Source code for httm.transformations.electron_flux_slices_to_raw

# HTTM: A transformation library for RAW and Electron Flux TESS Images
# Copyright (C) 2016, 2017 John Doty and Matthew Wampler-Doty of Noqsi Aerospace, Ltd.
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


"""
``httm.transformations.electron_flux_slices_to_raw``
====================================================

Transformation functions for processing slices contained in a
:py:class:`~httm.data_structures.electron_flux_converter.SingleCCDElectronFluxConverter` so that they are suitable
for writing to a simulated raw FITS file.

"""

import numpy

from .constants import FPE_MAX_ADU
from ..data_structures.common import Slice


# noinspection PyProtectedMember


[docs]def simulate_start_of_line_ringing_to_slice(start_of_line_ringing, image_slice): # type: (numpy.ndarray, Slice) -> Slice """ Every row of electron flux pixels (dark or otherwise) in a CCD slice has been empirically observed to start with a fixed pattern. This pattern is referred to as the *start of line ringing*. This transformation introduces the *start of line ringing* pattern to each row. *Start of line ringing* differs from CCD to CCD and slice to slice. It is most prominent in the left dark pixel columns. :param start_of_line_ringing: One dimensional array of floats, representing a fixed pattern \ disturbance in each row of a slice. If it is shorter than a row, it is padded with zeros, \ if longer it is truncated. :type start_of_line_ringing: row: :py:class:`numpy.ndarray` :param image_slice: input slice. Units: electrons :type image_slice: :py:class:`~httm.data_structures.common.Slice` units: electrons :rtype: :py:class:`~httm.data_structures.common.Slice` """ assert image_slice.units == "electrons", "units must be electrons" row_len = len(image_slice.pixels[0]) ringing_row = numpy.resize(numpy.concatenate((start_of_line_ringing, numpy.zeros(row_len))), row_len) # noinspection PyProtectedMember return image_slice._replace(pixels=image_slice.pixels + ringing_row)
[docs]def add_pattern_noise_to_slice(pattern_noise, image_slice): # type: (numpy.ndarray, Slice) -> Slice """ This transformation adds a fixed pattern of noise to a slice. The fixed pattern noise is different for each slice, for each CCD. The default values should be zero, because the expected pattern noise is zero. This transformation is present to accommodate the flight electronics where *pattern noise* may be present. :param pattern_noise: A two dimensional array of floats, representing a fixed pattern \ disturbance in a slice. :type pattern_noise: :py:class:`numpy.ndarray` :param image_slice: The input slice. Units: ADU :type image_slice: :py:class:`~httm.data_structures.common.Slice` :rtype: :py:class:`~httm.data_structures.common.Slice` """ assert image_slice.units == "ADU", "pixel units must be in ADU" assert image_slice.pixels.shape == pattern_noise.shape, \ "Image slice and pattern noise must be the same shape; " \ "image slice shape was {image_shape} and pattern noise shape was {pattern_noise_shape}".format( image_shape=image_slice.pixels.shape, pattern_noise_shape=pattern_noise.shape ) # noinspection PyProtectedMember return image_slice._replace(pixels=image_slice.pixels + pattern_noise)
[docs]def introduce_smear_rows_to_slice(smear_ratio, early_dark_pixel_columns, late_dark_pixel_columns, final_dark_pixel_rows, smear_rows, image_slice): # type: (float, Slice) -> Slice """ This function takes a slice with empty smear rows and populates them, and adds smear to every image pixel (but not dark pixels). See :ref:`ccd-and-slice-layout` for more details. *Smear* is estimated by averaging rows in the image pixels and then multiplying by ``smear_ratio``. For most of the frame cycle, a pixel effectively sits in the imaging area of the CCD, collecting photons from a particular point on the sky for the exposure time. During readout, that pixel moves quickly through the imaging area, exposed to each point on the sky along a column for a short time, the parallel clock period. The ``smear_ratio`` is the ratio of parallel clock period to the nominal exposure time of a single frame. This function first adds up all of the rows in the image pixel subarray of the slice. It multiplies that sum by ``smear_ratio`` to estimate a smear row. It then replaces the corresponding empty smear rows in the slice with the estimated smear row. Finally, it adds the estimated smear row to each image row. The pixels in the resulting smear rows should all be nonzero for reasonable input data, but if this transformation has not been applied, they should always contain zeros for a slice of electron flux data. :param smear_ratio: Dimensionless ratio of smear exposure to nominal exposure :type smear_ratio: float :param early_dark_pixel_columns: The number of dark pixel columns on the left side of the slice :type early_dark_pixel_columns: int :param late_dark_pixel_columns: The number of dark pixel columns on the right side of the slice :type late_dark_pixel_columns: int :param final_dark_pixel_rows: The number of dark pixel rows at the top of the slice :type final_dark_pixel_rows: int :param smear_rows: The number of smear rows :type smear_rows: int :param image_slice: Input slice. Units: electrons :type image_slice: :py:class:`~httm.data_structures.common.Slice` :rtype: :py:class:`~httm.data_structures.common.Slice` """ assert image_slice.units == "electrons", "units must be electrons" top = (final_dark_pixel_rows + smear_rows) smear_pixels = image_slice.pixels[-top:-final_dark_pixel_rows, early_dark_pixel_columns:-late_dark_pixel_columns] # noinspection PyTypeChecker assert numpy.all(smear_pixels == 0), "Smear rows are already introduced (should be set to 0)" image_pixels = image_slice.pixels[0:-top, early_dark_pixel_columns:-late_dark_pixel_columns] estimated_smear = smear_ratio * numpy.sum(image_pixels, 0) working_pixels = numpy.copy(image_slice.pixels) working_pixels[-top:-final_dark_pixel_rows, early_dark_pixel_columns:-late_dark_pixel_columns] = estimated_smear working_pixels[0:-top, early_dark_pixel_columns:-late_dark_pixel_columns] += estimated_smear # noinspection PyProtectedMember return image_slice._replace(pixels=working_pixels)
[docs]def add_shot_noise_to_slice(image_slice): # type: (Slice) -> Slice """ This transformation adds `shot noise <https://en.wikipedia.org/wiki/Shot_noise>`_ to every pixel. Shot noise is a fluctuation in electron counts. It is modeled as a Gaussian distributed error. If the expected electron count in the pixel is :math:`n` the standard deviation :math:`\\sigma` of the shot noise is :math:`\\sqrt{n}` and the expected value :math:`\\mu` is :math:`n`. :param image_slice: An image slice which has electrons as its units. Pixel data should be the *expected* electron \ counts for each pixel. :type image_slice: :py:class:`~httm.data_structures.common.Slice` :rtype: :py:class:`~httm.data_structures.common.Slice` """ assert image_slice.units == "electrons", "units must be electrons" pixels = image_slice.pixels # noinspection PyProtectedMember return image_slice._replace( pixels=pixels + numpy.sqrt(pixels) * numpy.random.normal(loc=0, scale=1, size=pixels.shape))
[docs]def simulate_blooming_on_slice(full_well, blooming_threshold, number_of_exposures, image_slice): # type: (float, float, int, Slice) -> Slice """ This function simulates `blooming <http://hamamatsu.magnet.fsu.edu/articles/ccdsatandblooming.html>`_ along columns in the image pixel array. Blooming is a diffusion process involving thermal excitation of electrons over the potential barriers between pixels. The height of the potential barrier confining electrons to the pixel decreases as the electron count in the pixel increases. The diffusion rate grows exponentially with decreasing barrier height. This process is modeled using three parameters: - :math:`\\mathtt{full\\_well}`, the maximum number of electrons in a pixel. This is the number of electrons \ sufficient to cause such rapid diffusion that the pixel cannot hold any more. - :math:`\\mathtt{blooming\\_threshold}`, the number of electrons in the pixel that suffices to drive \ significant diffusion. - :math:`\\mathtt{number\\_of\\_exposures}`, the number of stacked images in the slice. Blooming is modeled as a diffusion process. In a single step of the diffusion process, those pixels with electrons above :math:`\\mathtt{number\\_of\\_exposures} \\times \\mathtt{blooming\\_threshold}` electrons diffuse charge to neighboring pixels in the same column. A single step of the diffusion process is always performed *at least once*. The single step is repeated until all pixels are below :math:`\\mathtt{number\_of\_exposures} \\times \\mathtt{full\_well}`. This transformation does not have an inverse. :param full_well: The maximum number of electrons in a pixel. :type full_well: float :param blooming_threshold: The number of electrons in the pixel that suffices to drive significant diffusion. :type blooming_threshold: float :param number_of_exposures: The number of stacked images in the slice :type number_of_exposures: int :param image_slice: Input slice. Units: electrons :type image_slice: :py:class:`~httm.data_structures.common.Slice` :rtype: :py:class:`~httm.data_structures.common.Slice` """ assert image_slice.units == "electrons", "units must be electrons" convolutional_kernel = numpy.array([0.3, 0.4, 0.3]) def diffusion_step(column): diffusion_proof_part = numpy.clip(column, 0, number_of_exposures * blooming_threshold) excess = column - diffusion_proof_part # implicitly lose charge from top and bottom diffused_excess = numpy.convolve(excess, convolutional_kernel, mode='same') return diffusion_proof_part + diffused_excess def bloom_column(column): column = diffusion_step(column) while numpy.amax(column) > number_of_exposures * full_well: column = diffusion_step(column) return column working_pixels = numpy.copy(image_slice.pixels) bloomed_pixels = numpy.apply_along_axis(bloom_column, 0, working_pixels) # noinspection PyProtectedMember return image_slice._replace(pixels=bloomed_pixels)
[docs]def add_readout_noise_to_slice(readout_noise_parameter, number_of_exposures, image_slice): """ This transformation a Gaussian random *readout* noise to every pixel. This noise comes from the charge sense transistor and the signal processing electronics. The average value :math:`\\mu` of the noise is `0`. The variance :math:`\\sigma^2` is \ :math:`\\mathtt{readout\_noise\_parameter}^2 \\times \\mathtt{number\_of\_exposures}`. :param readout_noise_parameter: noise standard deviation for one image :type readout_noise_parameter: float :param number_of_exposures: number of stacked images in the slice :type number_of_exposures: int :param image_slice: input slice :type image_slice: :py:class:`~httm.data_structures.common.Slice` :rtype: :py:class:`~httm.data_structures.common.Slice` """ assert image_slice.units == "electrons", "units must be electrons" assert number_of_exposures > 0, "number of exposures must be positive" assert readout_noise_parameter >= 0, "readout noise parameter must be non-negative" if readout_noise_parameter <= 0.0: return image_slice # noinspection PyProtectedMember return image_slice._replace( pixels=numpy.random.normal(loc=image_slice.pixels, scale=readout_noise_parameter * numpy.sqrt(number_of_exposures)))
[docs]def simulate_undershoot_on_slice(undershoot_parameter, image_slice): """ When a CCD reads out a bright pixel, the pixel to the right of it appears artificially dimmer. This is *undershoot*. This function simulates *undershoot* for a slice. It convolves the kernel :math:`\\langle 1, -\\mathtt{undershoot\_parameter} \\rangle` with each input row, yielding an output row of the same length. The convolution is non-cyclic: the input row is implicitly padded with zero at the start to make this true. This transformation is the (approximate) inverse of :py:func:`~httm.transformations.raw_slices_to_calibrated.remove_undershoot_from_slice`. :param undershoot_parameter: Typically `~0.001`, dimensionless :type undershoot_parameter: float :param image_slice: input slice. Units: electrons :type image_slice: :py:class:`~httm.data_structures.common.Slice` :rtype: :py:class:`~httm.data_structures.common.Slice` """ assert image_slice.units == "electrons", "units must be electrons" convolutional_kernel = numpy.array([1.0, -undershoot_parameter]) def convolve_row(row): return numpy.convolve(row, convolutional_kernel, mode='same') # noinspection PyProtectedMember return image_slice._replace( pixels=numpy.apply_along_axis(convolve_row, 1, image_slice.pixels))
[docs]def add_baseline_to_slice(single_frame_baseline_adu, single_frame_baseline_adu_drift_term, number_of_exposures, video_scale, image_slice): """ This transformation adds a scalar random variate, the *baseline electron count*, to every pixel. The baseline electron count is also known as the *video bias*. To avoid clipping for low numbers of estimated electrons (which may even be negative) the ADC output is biased above zero, and this bias is not perfectly stable. Because this bias is not perfectly stable, the baseline electron count is modeled as a Gaussian noise term. The average value :math:`\\mu` of the baseline electron count is :math:`\\displaystyle{ \\frac{\\mathtt{single\\_frame\\_baseline\\_adu} \\times\\mathtt{number\\_of\\_exposures}}{\\mathtt{video\\_scale}}}`. The variance :math:`\\sigma^2` of the baseline electron count is :math:`\\displaystyle{ \\mathtt{single\\_frame\\_baseline\\_adu\\_drift\\_term}^2 / \\mathtt{video\\_scale}^2}`. :param single_frame_baseline_adu: The expected video bias in ADU for a single frame exposure. :type single_frame_baseline_adu: float :param single_frame_baseline_adu_drift_term: A standard deviation in ADU of the baseline electron count random \ variate. :type single_frame_baseline_adu_drift_term: float :param number_of_exposures: Number of stacked images in the slice :type number_of_exposures: int :param video_scale: Constant for converting electron counts to \ *Analogue to Digital Converter Units* (ADU). Units: electrons per ADU :type video_scale: float :param image_slice: input slice :type image_slice: :py:class:`~httm.data_structures.common.Slice` :rtype: :py:class:`~httm.data_structures.common.Slice` """ assert image_slice.units == "electrons", "units must be electrons" assert number_of_exposures > 0, "number of exposures must be positive" assert single_frame_baseline_adu_drift_term >= 0, "readout noise parameter must be non-negative" baseline_electrons = single_frame_baseline_adu * number_of_exposures * video_scale if single_frame_baseline_adu_drift_term <= 0.0: local_baseline_electron_estimate = baseline_electrons # type: float else: local_baseline_electron_estimate = \ numpy.random.normal(loc=baseline_electrons, scale=single_frame_baseline_adu_drift_term * video_scale) # type: float # noinspection PyProtectedMember return image_slice._replace(pixels=image_slice.pixels + local_baseline_electron_estimate)
# noinspection PyUnresolvedReferences
[docs]def convert_slice_electrons_to_adu(gain_loss, number_of_exposures, video_scale, clip_level_adu, image_slice): # type: (float, int, float, float, int, Slice) -> Slice """ This functions simulate various nonlinear effects in the measurement of electrons, before finally yielding output in *Analogue To Digital Converter Units* (ADU). The other transformation functions handle linear and approximately linear effects. This transformation is more complex and has more parameters because nonlinear effects are not so easily disentangled. Define the following: :math:`\\displaystyle{\\mathtt{FPE\\_MAX\\_ADU} := 65535}` :math:`\\displaystyle{\\mathtt{gain\\_loss\\_per\\_adu} := \\frac{\\mathtt{gain\\_loss}} {\\mathtt{number\\_of\\_exposures}\\times\\mathtt{FPE\\_MAX\\_ADU}}}` :math:`\\displaystyle{\\mathtt{gain\\_loss\\_per\\_electron} := \\frac{\\mathtt{gain\\_loss\\_per\\_adu}} {\\mathtt{video\\_scale}}}` :math:`\\displaystyle{\\mathtt{exposure\\_clip\\_level} := \\mathtt{clip\\_level\\_adu} \\times\\mathtt{number\\_of\\_exposures}}` :math:`\\displaystyle{\\mathtt{clip}(x,a,b) := \\max(a,\\min(x,b))}` For each pixel :math:`p`, with units in estimated electrons, this function applies the following transformation: :math:`\\displaystyle{\\mathtt{clip}\\left(\\frac{p}{\\mathtt{video\\_scale} \\times (1 + \\mathtt{gain\\_loss\\_per\\_electron} \\times p)}, 0, \\mathtt{exposure\\_clip\\_level}\\right)}` This transformation affects all pixels, dark, smear or illuminated. This function is the inverse transform of :py:func:`~httm.transformations.raw_slices_to_calibrated.convert_slice_adu_to_electrons`. :param gain_loss: The relative decrease in video gain over the total ADC range :type gain_loss: float :param number_of_exposures: The number of exposures the image comprises. :type number_of_exposures: int :param video_scale: Constant for converting electron counts to \ *Analogue to Digital Converter Units* (ADU). Units: electrons per ADU :type video_scale: float :param clip_level_adu: Maximum analog to digital converter output. Units: ADU :type clip_level_adu: int :param image_slice: Input slice. Units: electrons :type image_slice: :py:class:`~httm.data_structures.common.Slice` :rtype: :py:class:`~httm.data_structures.common.Slice` """ assert image_slice.units == "electrons", "units must be electrons" gain_loss_per_adu = gain_loss / (number_of_exposures * FPE_MAX_ADU) # type: float gain_loss_per_electron = gain_loss_per_adu / video_scale # type: float exposure_clip_level = clip_level_adu * number_of_exposures # type: float def transform_electron_to_adu(electron): from numpy import clip # noinspection PyTypeChecker return clip( electron / (video_scale * (1.0 + gain_loss_per_electron * electron)), 0, exposure_clip_level) return Slice(index=image_slice.index, units='ADU', pixels=transform_electron_to_adu(image_slice.pixels))