Source code for msfc_ccd._sensors

from typing import ClassVar, Literal
import abc
import dataclasses
import numpy as np
import astropy.units as u
import named_arrays as na
import optika

__all__ = [
    "TeledyneCCD230",
]


[docs] @dataclasses.dataclass(repr=False) class AbstractSensor( optika.mixins.Printable, ): """An interface for an imaging sensor or an ensemble of imaging sensors.""" num_tap_x: ClassVar[int] = 2 """The number of taps along the long axis of the CCD sensor.""" num_tap_y: ClassVar[int] = 2 """The number of taps along the short axis of the CCD sensor.""" @property @abc.abstractmethod def manufacturer(self) -> str: """The company which produced the sensor.""" @property @abc.abstractmethod def family(self) -> str: """The model number or product family of this sensor.""" @property @abc.abstractmethod def serial_number(self) -> None | str: """A unique number which identifies this sensor.""" @property @abc.abstractmethod def material(self) -> optika.sensors.materials.AbstractSiliconSensorMaterial: """The light-sensitive material used by this sensor.""" @property @abc.abstractmethod def num_pixel(self) -> na.Cartesian2dVectorArray[int, int]: """The number of pixels along the horizontal and vertical axes.""" @property @abc.abstractmethod def num_pixel_active(self): """The number of pixels that are used to detect light.""" @property @abc.abstractmethod def width_pixel(self) -> u.Quantity: """The physical size of a single pixel on the imaging sensor.""" @property def width_active(self): """The physical size of the light sensitive area of the sensor.""" result = self.width_pixel * self.num_pixel_active return result.to(u.mm) @property @abc.abstractmethod def num_blank(self) -> int: """The number of blank columns at the start of each row.""" @property @abc.abstractmethod def num_overscan(self) -> int: """The number of overscan columns at the end of each row.""" @property @abc.abstractmethod def cte(self) -> u.Quantity: """The charge transfer efficiency of the sensor.""" @property @abc.abstractmethod def readout_noise(self) -> u.Quantity: """The standard deviation of the error on each pixel value.""" @property def temperature(self): """The operating temperature of this sensor.""" return self.material.temperature
[docs] def dark_current( self, temperature: None | u.Quantity | na.AbstractScalar = None, ): """ Calculate the rate of charge accumulation when the sensor is not illuminated. Parameters ---------- temperature The temperature of the sensor. If :obj:`None`, the value of :attr:`temperature` is used. """
[docs] @dataclasses.dataclass(repr=False) class TeledyneCCD230( AbstractSensor, ): """The standard sensor used by the MSFC cameras.""" manufacturer: str = "Teledyne/e2v" """The company which produced the sensor.""" family: str = "CCD230-42" """The model number or product family of this sensor.""" serial_number: None | str = None """A unique number which identifies this sensor.""" grade: None | str = None """ The quality of the device. Grade 0 is the best possible and Grade 5 is the worst possible. """ material: None | optika.sensors.materials.AbstractSiliconSensorMaterial = None """ The light-sensitive material used by this sensor. If :obj:`None`, :func:`optika.sensors.materials.e2v_ccd97` will be used. """ width_pixel: u.Quantity = 15 * u.um """The physical size of a single pixel on the imaging sensor.""" num_pixel_x: int = 2048 """The number of pixels along the horizontal axis of the CCD sensor.""" num_pixel_y: int = 2064 """The number of pixels along the vertical axis of the CCD sensor.""" num_blank: int = 50 """The number of blank columns at the start of each row.""" num_overscan: int = 2 """The number of overscan columns at the end of each row.""" cte: u.Quantity = 99.9995 * u.percent """The charge transfer efficiency of the sensor.""" readout_noise: u.Quantity = 4 * u.electron """The standard deviation of the error on each pixel value.""" readout_mode: Literal["full-frame", "transfer"] = "transfer" """ The frame readout mode of the sensor. Either the entire sensor is read at the same time (``"full-frame"``), or half of the sensor is used for storage (``"transfer"``). """ width_package_x: u.Quantity = 42 * u.mm """The horizontal size of the physical sensor package.""" width_package_y: u.Quantity = 61 * u.mm """The vertical size of the physical sensor package.""" def __post_init__(self): if self.material is None: self.material = optika.sensors.materials.e2v_ccd97() @property def num_pixel(self) -> na.Cartesian2dVectorArray: return na.Cartesian2dVectorArray( x=self.num_pixel_x, y=self.num_pixel_y, ) @property def num_pixel_active(self): """ The number of pixels that are used to detect light. If :attr:`readout_mode` is ``"full-frame"``, then this is the same as :attr:`num_pixel`. If :attr:`readout_mode` is ``"transfer"``, then the vertical component of :attr:`num_pixel` is divided by 2 since half of the sensor is now used for charge storage. """ result = self.num_pixel if self.readout_mode == "transfer": return result.replace(y=result.y // 2) @property def width_package(self) -> na.Cartesian2dVectorArray: """The vertical and horizontal width of the physical sensor package.""" return na.Cartesian2dVectorArray( x=self.width_package_x, y=self.width_package_y, ) @classmethod def _frac_Qd_Qdo(cls, temperature: u.Quantity | na.AbstractScalar): T = temperature return 122 * T * np.square(T) * np.exp(-6400 * u.K / T) / u.K**3
[docs] def dark_current( self, temperature: None | u.Quantity | na.AbstractScalar = None, ): """ Calculate the rate of charge accumulation when the sensor is not illuminated. Parameters ---------- temperature The temperature of the sensor. If :obj:`None`, the value of :attr:`temperature` is used. .. nblinkgallery:: :caption: Examples :name: rst-link-gallery ../reports/dark-current """ if temperature is None: temperature = self.temperature Q_248K = 0.2 * u.electron / u.s f = self._frac_Qd_Qdo(248 * u.K) Q_do = Q_248K / f return Q_do * self._frac_Qd_Qdo(temperature)