"""
Detector class
----
.. include license and copyright
.. include:: ../include/copy.rst
----
.. include common links, assuming primary doc root is up one directory
.. include:: ../include/links.rst
"""
import os
import numpy
from .efficiency import Efficiency
[docs]class Detector(Efficiency):
r"""
Define the detector technical properties.
Args:
shape (:obj:`tuple`, optional):
Dimensions of the **unbinned** detector in number of pixels along
the spectral axis and number of pixels along the spatial axis. Can
be None, but limits use if it is.
pixelsize (:obj:`float`, optional):
The size of the **unbinned** (square) detector pixels in *micron*.
binning (:obj:`int`, optional):
The number of unbinned pixels along one axis included in a square
binned pixel. I.e., if the binning is 4x4, this should be 4.
Currently cannot accommodate binned that is not square.
rn (:obj:`float`, optional):
Read-noise in electrons.
dark (:obj:`float`, optional):
Dark current in electrons per pixel per second.
cic (:obj:`float`, optional):
Clock-induced charge in electrons per pixel.
gain (:obj:`float`, optional):
Gain of detector amplifier in electrons per ADU.
fullwell (:obj:`float`, optional):
The full well of the pixels in electrons.
nonlinear (:obj:`float`, optional):
The fraction of the fullwell above which the detector
response is nonlinear.
qe (:obj:`float`, :class:`Efficiency`, optional):
Detector quantum efficiency.
em_fac (:obj:`float`, optional):
For EMCCDs, this is the multiplicative noise factor due to the
stochasticity in the gain at high gain. The theoretical value for
this is :math:`\sqrt{2}`. Average values for EMCCDs is quoted as
1.3 by `Hamamatsu
<https://hamamatsu.magnet.fsu.edu/articles/emccds.html>`__. This
should be 1 for a normal CCD.
em_gain (:obj:`float`, optional):
For EMCCDs, the readnoise can be provided as either the readnoise
*without* electron multiplication or *with* multiplication. If
provided *with* multiplication, this parameter should be set to 1
and the inclusion of the readnoise in the detector noise is
identical to a normal CCD. If provided *without* electron
multiplication, ``em_gain`` should provide the electron
multiplication gain setting, and the readnoise term included in the
error budget is then ``rn/em_gain``.
fps (:obj:`float`, optional):
Readout time expressed in number of full-frame readouts per second.
"""
# TODO: Allow for multiple amplifiers per detector? Would also need
# to define amplifier section.
# TODO: Define overscan and data sections
def __init__(self, shape=None, pixelsize=15., binning=1, rn=1., dark=0., cic=0., gain=1.,
fullwell=1e4, nonlinear=1., qe=0.9, em_fac=1., em_gain=1., fps=1.):
if not isinstance(qe, (Efficiency, float)):
raise TypeError('Provided quantum efficiency must be type `float` or `Efficiency`.')
if isinstance(qe, float):
super(Detector, self).__init__(qe)
else:
super(Detector, self).__init__(qe.eta, wave=qe.wave)
if shape is not None and len(shape) != 2:
raise ValueError('Shape must contain two integers.')
self.binning = binning
# Shape in binned pixels
self.shape = None if shape is None else numpy.asarray(shape)/self.binning
# Size of binned pixels in microns
self.pixelsize = pixelsize*self.binning
# Size of the detector in both axis in mm
self.size = None if self.shape is None else self.shape*self.pixelsize*1e-3
# Readnoise
self.rn = rn
# Dark current per binned pixel
self.dark = dark*self.binning**2
# Clock-induced charge per binned pixel
self.cic = cic*self.binning**2
self.gain = gain
self.fullwell = fullwell
self.nonlinear = nonlinear
self.em_fac = em_fac
self.em_gain = em_gain
self.fps = fps
[docs] def count_rate(self, wave, photon_rate):
"""
Apply the quantum efficiency to convert a photon rate to an electron
count rate.
Args:
wave (:obj:`float`, array-like):
The wavelength for the incident photons.
photon_rate (:obj:`float`, array-like):
The photon flux in number per second at the provided wavelength.
Returns:
:obj:`float`, `numpy.ndarray`:
Count rate.
"""
single_value = isinstance(photon_rate, float)
_rate = numpy.atleast_1d(photon_rate)
_wave = numpy.atleast_1d(wave)
if _wave.size == 1 and _rate.size > 1:
_wave = numpy.full(_rate.shape, wave)
if _wave.shape != _rate.shape:
raise ValueError('Wavelength and flux array shapes must match.')
cnts = self(wave)*photon_rate
return cnts[0] if single_value else cnts
# TODO: Add digitization noise?
[docs] def variance(self, count_rate, exptime=1.):
"""
Compute the detector variance for a given count rate.
Args:
count_rate (:obj:`float`, `numpy.ndarray`_):
Number of counts per second. The quantum efficiency should
already have been accounted for; see :func:`count_rate`.
exptime (:obj:`float`, optional):
The exposure time in seconds.
Returns:
:obj:`float`, `numpy.ndarray`_: The expected statistical error in
the detector counts. Shape (and type) match ``photon_flux``.
"""
# NOTE: self.dark and self.cic already account for the binning.
return self.em_fac**2 * (count_rate*exptime + self.dark*exptime + self.cic) \
+ (self.rn/self.em_gain)**2
[docs] def t_exp(self, n_exp, t_tot):
"""
Provided a total observing time and a number of exposures, calculate the
maximum integration time per exposure given the detector readout time.
Args:
n_exp (:obj:`int`):
Number of exposures.
t_tot (:obj:`float`):
Total observing allocation in seconds.
Returns:
:obj:`float`: Integration time per exposure to fill to total
observing allocation.
"""
return t_tot/n_exp - 1/self.fps
[docs] def n_exp(self, t_exp, t_tot):
"""
Provided a total observing time and an integration time per exposure,
calculate the maximum number of exposures that be performed given the
detector readout time.
Args:
t_exp (:obj:`int`):
Integration time per exposure.
t_tot (:obj:`float`):
Total observing allocation in seconds.
Returns:
:obj:`float`: Number of exposures possible within the total
observing allocation.
"""
return int(numpy.round(t_tot/(t_exp + 1/self.fps)))
[docs] def t_tot(self, n_exp, t_exp):
"""
Get the total observing time including readout overhead.
"""
return n_exp*(t_exp + 1/self.fps)