Calculate exposure time, update docstring, documentation added

This commit is contained in:
Lukas Klass 2020-05-15 14:34:55 +02:00
parent 751d9bd9d3
commit 8845e60654

View File

@ -3,7 +3,7 @@ from .ASensor import ASensor
from ..IRadiant import IRadiant from ..IRadiant import IRadiant
from ..Entry import Entry from ..Entry import Entry
import numpy as np import numpy as np
from typing import Union from typing import Union, Tuple
from ..psf.Airy import Airy from ..psf.Airy import Airy
from ..psf.Zemax import Zemax from ..psf.Zemax import Zemax
from ..SpectralQty import SpectralQty from ..SpectralQty import SpectralQty
@ -92,7 +92,7 @@ class Imager(ASensor):
common_conf.psf.osf, pixel_size) common_conf.psf.osf, pixel_size)
@u.quantity_input(exp_time="time") @u.quantity_input(exp_time="time")
def getSNR(self, exp_time: u.Quantity): def getSNR(self, exp_time: u.Quantity) -> u.Quantity:
""" """
Calculate the signal to background ratio (SNR) for the given exposure time using the CCD-equation. Calculate the signal to background ratio (SNR) for the given exposure time using the CCD-equation.
@ -103,91 +103,158 @@ class Imager(ASensor):
Returns Returns
------- -------
snr : float snr : Quantity
The calculated SNR The calculated SNR as dimensionless quantity
""" """
# Calculate the electron currents
signal_current, background_current, read_noise, dark_current = self.__exposePixels()
# Calculate the number of collected electrons
signal = signal_current * exp_time
background = background_current * exp_time
dark = dark_current * exp_time
return self.__calcSNR(signal, background, read_noise, dark)
snr = self.__calcSNR(*self.__exposePixels(), exp_time) def getExpTime(self, snr: u.Quantity) -> u.Quantity:
return snr.value
def getExpTime(self, snr: float) -> u.Quantity:
""" """
Calculate the necessary exposure time in order to achieve the given SNR. Calculate the necessary exposure time in order to achieve the given SNR.
Parameters Parameters
---------- ----------
snr : float snr : Quantity
The SNR for which the necessary exposure time shall be calculated. The SNR for which the necessary exposure time shall be calculated as dimensionless quantity.
Returns Returns
------- -------
exp_time : Quantity exp_time : Quantity
The necessary exposure time in seconds. The necessary exposure time in seconds.
""" """
# # Calculate the number of exposed pixels which will be used for calculating the noise # Calculate the electron currents
# n_pix_exposed = self.__calcExposedPixels() signal_current, background_current, read_noise, dark_current = self.__exposePixels()
# # Calculate the electron current from the target and the background # Calculate the electron currents for all pixels
# signal_current, background_current = self.__calcElectronCurrent(n_pix_exposed) signal_current_tot = signal_current.sum()
# # Fix the physical units of the SNR background_current_tot = background_current.sum()
# snr = snr * u.electron**0.5 read_noise_tot = read_noise.sum()
# dark_current_tot = dark_current.sum()
# # Calculate the ratio of the background- and dark-current to the signal current as auxiliary variable # Fix the physical units of the SNR
# current_ratio = (background_current + n_pix_exposed * self.__dark_current) / signal_current snr = snr * u.electron**0.5
# # Calculate the necessary exposure time as inverse of the CCD-equation
# exp_time = snr ** 2 * (1 + current_ratio + np.sqrt(
# (1 + current_ratio) ** 2 + 4 * (self.__read_noise * n_pix_exposed) ** 2 / snr ** 2)) /\
# (2 * signal_current)
# return exp_time
@u.quantity_input(signal_current=u.electron / u.s, background_current=u.electron / u.s, # Calculate the ratio of the background- and dark-current to the signal current as auxiliary variable
read_noise=u.electron ** 0.5, dark_current=u.electron / u.s, exp_time="time") current_ratio = (background_current_tot + dark_current_tot) / signal_current_tot
def __calcSNR(self, signal_current: u.Quantity, background_current: u.Quantity, read_noise: u.Quantity, # Calculate the necessary exposure time as inverse of the CCD-equation
dark_current: u.Quantity, exp_time: u.Quantity) -> u.dimensionless_unscaled: exp_time = snr ** 2 * (1 + current_ratio + np.sqrt(
# Calculate the SNR using the CCD-equation (1 + current_ratio) ** 2 + 4 * read_noise_tot ** 2 / snr ** 2)) /\
signal = signal_current * exp_time (2 * signal_current_tot)
background = background_current * exp_time # Calculate the SNR in order to check for overexposed pixels
dark = dark_current * exp_time self.__calcSNR(signal_current * exp_time, background_current * exp_time, read_noise, dark_current * exp_time)
return exp_time
@u.quantity_input(signal=u.electron, background=u.electron, read_noise=u.electron ** 0.5, dark=u.electron)
def __calcSNR(self, signal: u.Quantity, background: u.Quantity, read_noise: u.Quantity,
dark: u.Quantity) -> u.dimensionless_unscaled:
"""
Calculate the signal to noise ratio (SNR) of the given electron counts.
Parameters
----------
signal : Quantity
The collected electrons from the target in electrons.
background : Quantity
The collected electrons from the background in electrons.
read_noise : Quantity
The read noise in electrons.
dark : Quantity
The electrons from the dark current in electrons.
Returns
-------
snr : Quantity
The signal to noise ration as dimensionless quantity
"""
# Calculate the total collected electrons per pixel
total = signal + background + dark total = signal + background + dark
# Check for overexposed pixels
overexposed = total > self.__well_capacity overexposed = total > self.__well_capacity
if np.any(overexposed): if np.any(overexposed):
# Show a warning for the overexposed pixels
warning(str(np.count_nonzero(overexposed)) + " pixels are overexposed.") warning(str(np.count_nonzero(overexposed)) + " pixels are overexposed.")
info("Collected electrons from target: %1.2e electrons" % signal.sum().value) info("Collected electrons from target: %1.2e electrons" % signal.sum().value)
info("Collected electrons from background: %1.2e electrons" % background.sum().value) info("Collected electrons from background: %1.2e electrons" % background.sum().value)
info("Electrons from dark current: %1.2e electrons" % dark.sum().value) info("Electrons from dark current: %1.2e electrons" % dark.sum().value)
info("Read noise: %1.2e electrons" % (read_noise ** 2).sum().value) info("Read noise: %1.2e electrons" % (read_noise ** 2).sum().value)
info("Total collected electrons: %1.2e electrons" % total.sum().value) info("Total collected electrons: %1.2e electrons" % total.sum().value)
# Calculate the SNR using the CCD-equation
snr = signal.sum() / np.sqrt(total.sum() + (read_noise ** 2).sum()) snr = signal.sum() / np.sqrt(total.sum() + (read_noise ** 2).sum())
# Return the value of the SNR, ignoring the physical units (electrons^0.5) # Return the value of the SNR, ignoring the physical units (electrons^0.5)
return snr.value * u.dimensionless_unscaled return snr.value * u.dimensionless_unscaled
def __exposePixels(self) -> (u.electron / u.s, u.electron / u.s, u.electron, u.electron / u.s): def __exposePixels(self) -> Tuple[u.Quantity, u.Quantity, u.Quantity, u.Quantity]:
signal_current, size, obstruction, background_current = self.__calcPixelElectronCurrent() """
Expose the pixels and calculate the signal and noise electron currents per pixel.
Returns
-------
signal_current : Quantity
The electron current from the target as PixelMask in electrons / s
background_current : Quantity
The electron current from the background as PixelMask in electrons / s
read_noise : Quantity
The read noise per pixel in electrons
dark_current : Quantity
The electron current from the dark noise as PixelMask in electrons / s
"""
# Calculate the total incoming electron current
signal_current, size, obstruction, background_current = self.__calcIncomingElectronCurrent()
# Initialize a new PixelMask
mask = PixelMask(self.__pixel_geometry, self.__pixel_size, self.__center_offset) mask = PixelMask(self.__pixel_geometry, self.__pixel_size, self.__center_offset)
if size.lower() == "extended": if size.lower() == "extended":
# Target is extended, a diameter of 0 pixels results in a mask with one pixel marked
d_photometric_ap = 0 * u.pix d_photometric_ap = 0 * u.pix
# Mask the pixels to be exposed
mask.createPhotometricAperture("circle", d_photometric_ap / 2, np.array([0, 0]) << u.pix) mask.createPhotometricAperture("circle", d_photometric_ap / 2, np.array([0, 0]) << u.pix)
else: else:
# Target is a point source
if self.__contained_pixels is not None: if self.__contained_pixels is not None:
# Calculate the diameter of the photometric aperture as square root of the contained pixels
d_photometric_ap = np.sqrt(self.__contained_pixels.value) * u.pix d_photometric_ap = np.sqrt(self.__contained_pixels.value) * u.pix
# Mask the pixels to be exposed
mask.createPhotometricAperture("square", d_photometric_ap / 2, np.array([0, 0]) << u.pix) mask.createPhotometricAperture("square", d_photometric_ap / 2, np.array([0, 0]) << u.pix)
else: else:
# Calculate the diameter of the photometric aperture from the given contained energy
d_photometric_ap = self.__calcPhotometricAperture(obstruction) d_photometric_ap = self.__calcPhotometricAperture(obstruction)
# Mask the pixels to be exposed
mask.createPhotometricAperture(self.__shape, d_photometric_ap / 2) mask.createPhotometricAperture(self.__shape, d_photometric_ap / 2)
background = mask * background_current * u.pix # Calculate the background current PixelMask
background_current = mask * background_current * u.pix
# Calculate the read noise PixelMask
read_noise = mask * self.__read_noise * u.pix read_noise = mask * self.__read_noise * u.pix
# Calculate the dark current PixelMask
dark_current = mask * self.__dark_current * u.pix dark_current = mask * self.__dark_current * u.pix
info("The radius of the photometric aperture is %.2f pixels." % (d_photometric_ap.value / 2)) info("The radius of the photometric aperture is %.2f pixels." % (d_photometric_ap.value / 2))
info("The photometric aperture contains " + str(np.count_nonzero(mask)) + " pixels.") info("The photometric aperture contains " + str(np.count_nonzero(mask)) + " pixels.")
if size.lower() != "extended": if size.lower() != "extended":
# Map the PSF onto the pixel mask in order to get the relative irradiance of each pixel
mask = self.__psf.mapToPixelMask(mask, mask = self.__psf.mapToPixelMask(mask,
getattr(getattr(self.__common_conf, "jitter_sigma", None), "val", None), getattr(getattr(self.__common_conf, "jitter_sigma", None), "val", None),
obstruction) obstruction)
signal = mask * signal_current # Calculate the signal current PixelMask
return signal, background, read_noise, dark_current signal_current = mask * signal_current
return signal_current, background_current, read_noise, dark_current
def __calcPhotometricAperture(self, obstruction: float) -> u.Quantity: def __calcPhotometricAperture(self, obstruction: float) -> u.Quantity:
"""
Calculate the diameter of the photometric aperture
Parameters
----------
obstruction : float
The obstruction factor as A_ob / A_ap.
Returns
-------
d_photometric_ap : Quantity
The diameter of the photometric aperture in pixels.
"""
# Calculate the reduced observation angle # Calculate the reduced observation angle
# jitter_sigma = self.__common_conf.jitter_sigma() if hasattr(self.__common_conf, "jitter_sigma") else None
jitter_sigma = getattr(getattr(self.__common_conf, "jitter_sigma", None), "val", None) jitter_sigma = getattr(getattr(self.__common_conf, "jitter_sigma", None), "val", None)
reduced_observation_angle = self.__psf.calcReducedObservationAngle(self.__contained_energy, jitter_sigma, reduced_observation_angle = self.__psf.calcReducedObservationAngle(self.__contained_energy, jitter_sigma,
obstruction) obstruction)
@ -202,7 +269,7 @@ class Imager(ASensor):
d_photometric_ap = observation_angle / pixel_fov d_photometric_ap = observation_angle / pixel_fov
return d_photometric_ap * u.pix return d_photometric_ap * u.pix
def __calcPixelElectronCurrent(self) -> (u.electron / u.s, str, float, u.electron / (u.pix * u.s)): def __calcIncomingElectronCurrent(self) -> Tuple[u.Quantity, str, float, u.Quantity]:
""" """
Calculate the detected electron current of the signal and the background. Calculate the detected electron current of the signal and the background.
@ -213,7 +280,7 @@ class Imager(ASensor):
size : str size : str
The size of the target. The size of the target.
obstruction : float obstruction : float
The obstruction factor. The obstruction factor as A_ob / A_ap.
background_current : Quantity background_current : Quantity
The electron current on the detector caused by the background in electrons / (s * pix). The electron current on the detector caused by the background in electrons / (s * pix).
""" """