# -*- coding: utf-8 -*-
################################################################
# The contents of this file are subject to the BSD 3Clause (New) License
# you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://directory.fsf.org/wiki/License:BSD_3Clause
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
# The Original Code is part of the PyRadi toolkit.
# The Initial Developer of the Original Code is M Konnik and CJ Willers,
# Portions created by CJ Willers are Copyright (C) 2006-2015
# All Rights Reserved.
# Contributor(s): ______________________________________.
################################################################
"""
This module provides a high level model for CCD and CMOS staring array
signal chain modelling. The work is based on a paper and Matlab code by Mikhail Konnik,
available at:
- Paper available at: http://arxiv.org/pdf/1412.4031.pdf
- Matlab code available at: https://bitbucket.org/aorta/highlevelsensorsim
- The paper describing this Python model as published in SPIE Proc 10036 is available here ::
https://github.com/NelisW/pyradi/blob/master/pyradi/documentation/SM200-30-staring-array-modeling.pdf
See the documentation at http://nelisw.github.io/pyradi-docs/_build/html/index.html
or pyradi/doc/rystare.rst for more detail.
"""
__version__ = '0.1.0'
__author__ = 'M Konnik and CJ Willers'
__all__ = ['photosensor', 'set_photosensor_constants', 'check_create_datasets', 'source_follower',
'fixed_pattern_offset', 'cds', 'adc', 'charge_to_voltage',
'sense_node_reset_noise', 'dark_current_and_dark_noises', 'source_follower_noise',
'multiply_detector_area', 'multiply_integration_time', 'convert_to_electrons',
'shotnoise', 'responsivity_FPN_light', 'responsivity_FPN_dark', 'FPN_models',
'nEcntLLightDF', 'nEcntLLightPhotL', 'nElecCntThermalScene', 'nEcntThermalOptics',
'nElecCntReflSun', 'darkcurrentnoise', 'kTCnoiseCsn', 'kTCnoiseGv',
'define_metrics', 'limitzero', 'run_example',
'get_summary_stats',
]
import numpy as np
import scipy.signal as signal
import scipy.constants as const
import pyradi.ryfiles as ryfiles
import pyradi.ryhdf5 as ryhdf5
import pyradi.ryutils as ryutils
import pyradi.ryprob as ryprob
import pyradi.ryplanck as ryplanck
def _ryplanckql(spectral, temperature):
"""Planck photon spectral exitance, wavelength domain, spectral in µm.
Self-contained: constants are computed from scipy.constants on every call
so the result is independent of which pyradi version is installed.
Handles scalar, 1-D, and 2-D column-vector (N,1)/(M,1) inputs, matching
the behaviour of the @fixDimensions-decorated local ryplanck.planckql.
"""
import numpy as _np
import scipy.constants as _sc
_c1ql = 2.0 * _np.pi * _sc.c * (1.0e6) ** 3 # q/(s.m^2.um^4)
_c2l = _sc.h * _sc.c / _sc.k * 1.0e6 # um.K
_lim = 709.7
sp = _np.asarray(spectral, dtype=float)
tp = _np.asarray(temperature, dtype=float)
# (N,1) spectral × (M,1) temperature → (N,M): transpose temp so it broadcasts
if sp.ndim == 2 and sp.shape[1] == 1 and tp.ndim == 2 and tp.shape[1] == 1:
tp = tp.T # (1, M) broadcasts with (N, 1) → (N, M)
exP = _c2l / (sp * tp)
exP2 = _np.where(exP < _lim, exP, 1.0)
p = (_c1ql / (_np.exp(exP2) - 1.0)) / (sp ** 4)
return _np.where(exP < _lim, p, 0.0)
######################################################################################
[docs]
def photosensor(strh5,initialise=True):
"""This routine simulates the behaviour of a CCD/CMOS sensor, performing the conversion
from irradiance to electrons, then volts, and then digital numbers.
The process from incident photons to the digital numbers appeared on the image is outlined.
First of all, the radiometry is considered. Then, the process of conversion from photons to
electrons is outlined. Following that, conversion from electrons to voltage is
described. Finally, the ADC converts the voltage signal into digital numbers.
The whole process is depicted on Figure below.
.. image:: _images/camerascheme-horiz.png
:width: 812px
:align: center
:height: 244px
:alt: camerascheme-horiz.png
:scale: 100 %
Many noise sources contribute to the resulting noise image that is produced by
photosensors. Noise sources can be broadly classified as either
*fixed-pattern (time-invariant)* or *temporal (time-variant)*
noise. Fixed-pattern noise refers to any spatial pattern that does not change
significantly from frame to frame. Temporal noise, on the other hand, changes
from one frame to the next.
Note that in the sequence below we add signal and noise signals linearly together.
For uncorrelated noise sources, the noise power values are added in quadrature, but
that does not apply here, because we are adding instantaneous noise values (per pixel)
so that these noise and signal values add linearly.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
senstype = strh5['rystare/sensortype'][()]
if type(senstype) is not str:
senstype = senstype.decode('UTF-8')
if not senstype in ['CCD', 'CMOS']:
print('Sensor Simulator::: please select the sensor: CMOS or CCD!')
return None
#defining the constants such as speed of light c, Plank's h, and others.
strh5 = set_photosensor_constants(strh5, initialise)
#create the various data arrays
strh5 = check_create_datasets(strh5, initialise)
detarea = strh5['rystare/photondetector/geometry/fillfactor'][()] * strh5['rystare/pixelPitch'][()][0] * \
strh5['rystare/pixelPitch'][()][1]
strh5['rystare/detectorArea'][...] = detarea
if strh5['rystare/darkframe'][()]:
# complete darkness, photon signal already set to zero
strh5['rystare/signal/photonRateIrradianceNoNoise'] = np.zeros(
imghd5['image/PhotonRateRadianceNoNoise'][()].shape)
strh5['rystare/signal/photonRateIrradiance'] = np.zeros(
imghd5['image/PhotonRateRadiance'][()].shape)
else:
# there is light on the sensor
# values already loaded by the time we get here
# signal stored in rystare/signal/photonRateIrradianceNoNoise
# add photon noise and calculate electron counts
# diagram node 1 signal stored in 'rystare/signal/photonRateIrradiance'
#photon rate irradiance in the image ph/(m2.s)
#if shot noise required, use the no noise image to create the noisy image, noise diagram node 1
if strh5['rystare/photonshotnoise/activate']:
strh5['rystare/signal/photonRateIrradiance'][...] = shotnoise(
strh5['rystare/signal/photonRateIrradianceNoNoise'][()])
# adjust 'rystare/signal/photonRateIrradiance' with responsivity non-uniformity (PRNU)
# diagram node 2 PRNU stored in 'rystare/signal/photonRateIrradianceNU
# diagram node 3 photon rate multiplied with PRNU stored in 'rystare/signal/photonRateIrradianceNU'
if strh5['rystare/photondetector/lightPRNU/activate'][()]:
strh5 = responsivity_FPN_light(strh5)
# at this point the photon irradiance is converted to electrons, after all
# responsivity effects but excluding detector area and integration time.
# diagram node 4 quantum efficiency stored in rystare/quantumEfficiency
# diagram node 4 photon rate x mean value of the quantum efficiency
# stored in rystare/signal/electronRateIrradiance
strh5 = convert_to_electrons(strh5)
# multiply with the detector area
# diagram node 5 signal stored in 'rystare/signal/electronRate'
strh5 = multiply_detector_area(strh5)
# multiply with the integration time
# diagram node 6 signal stored in 'rystare/signal/lightelectronsnoshotnoise'
strh5 = multiply_integration_time(strh5)
#now add shot noise on the photoelectrons diagram node 7
strh5['rystare/signal/lightelectrons'][...] = shotnoise(strh5['rystare/signal/lightelectronsnoshotnoise'][()])
# no dark current or dark current effects added yet
# add dark current noise
# diagram node 8 dc average dark current stored in 'rystare/darkcurrentelectronsnonoise' in nA
# diagram node 9 dc dark current with noise image stored in 'rystare/darkcurrentelectrons' in nA
# diagram node 10 dark current nonuniformity stored in 'rystare/photondetector/darkcurrent/fixedPatternNoise/value'
# diagram node 11 dark current electrons stored in 'rystare/signal/darkcurrentelectrons'
if strh5['rystare/photondetector/darkcurrent/activate'][()]:
strh5 = dark_current_and_dark_noises(strh5)
# get total signal by adding the electrons generated by dark signal and electrons generated by light.
# diagram node 12 signal in electrons stored in 'rystare/signal/electrons'
strh5['rystare/signal/electrons'][...] = strh5['rystare/signal/lightelectrons'][()] + \
strh5['rystare/signal/darkcurrentelectrons'][()]
# Full-well check-up and saturate the pixel if there are more electrons than fwell capacity.
# find all of pixels that are saturated (there are more electrons that full-well of the pixel)
strh5['rystare/signal/electronsWell'][...] = np.where(
strh5['rystare/signal/electrons'][()] >
strh5['rystare/sensenode/fullwellelectronselection/fullwellelectrons'][()],
strh5['rystare/sensenode/fullwellelectronselection/fullwellelectrons'][()],
strh5['rystare/signal/electrons'][()])
# find all of pixels that are less than zero and truncate to zero (no negative electron count).
strh5['rystare/signal/electronsWell'][...] = np.where(
strh5['rystare/signal/electronsWell'][()] < 0, 0, strh5['rystare/signal/electronsWell'][()])
# round the number of electrons.
# diagram node 13 signal in electrons after charge well clipping in 'rystare/signal/electronsWell'
strh5['rystare/signal/electronsWell'][...] = np.floor(strh5['rystare/signal/electronsWell'][()])
# Charge-to-Voltage conversion by Sense Node
# diagram node 14 electrons in well converted to voltage Q=CV stored in 'rystare/signal/sensenodevoltageLinear'
# diagram node 15b reset voltage with kTC noise stored in 'rystare/noise/sn_reset/vrefresetpluskTC'
# diagram node 16 signal in volts after nonlinearity (if present) stored in 'rystare/signal/voltage'
strh5 = charge_to_voltage(strh5)
# signal's Voltage amplification by Source Follower
# diagram node 17 source follower nonlinearity stored in 'rystare/sourcefollower/gainA'
# diagram node 18 source follower signal after nonlinearity stored in 'rystare/signal/voltageAfterSF'
strh5 = source_follower(strh5, initialise)
# calculate the source follower noise in volts.
# diagram node 19 source follower noise volts stored in 'rystare/sourcefollower/source_follower_noise'
if strh5['rystare/sourcefollower/noise/activate'][()]:
strh5 = source_follower_noise(strh5, initialise)
#add source follower noise
# diagram node 20 signal in volts stored in 'rystare/signal/voltageAfterSFnoise'
strh5['rystare/signal/voltageAfterSFnoise'][...] = strh5['rystare/signal/voltageAfterSF'][()] + \
strh5['rystare/sourcefollower/source_follower_noise']
# diagram node 21 fixed pattern offset in volts stored in 'rystare/sourcefollower/fpoffset/value'
strh5 = fixed_pattern_offset(strh5)
# diagram node 22 fixed pattern offset added to signal in volts stored in 'rystare/signal/voltagebeforecds'
if (initialise):
strh5['rystare/signal/voltagebeforecds'] = strh5['rystare/signal/voltage'][()] + \
strh5['rystare/sourcefollower/fpoffset/value'][()]
else:
strh5['rystare/signal/voltagebeforecds'][...] = strh5['rystare/signal/voltage'][()] + \
strh5['rystare/sourcefollower/fpoffset/value'][()]
# signal's amplification and de-noising by Correlated Double Sampling
# diagram node 23 fixed pattern offset added to signal in volts stored in 'rystare/signal/voltageaftercds'
strh5 = cds(strh5)
# Analogue-To-Digital Converter
# diagram node 24 ADC integral linearity error stored in 'rystare/ADC/gainILE'
# diagram node 24b ADC gain stored in 'rystare/ADC/gain'
# diagram node 25 signal after ADC stored in 'rystare/signal/DN'
strh5 = adc(strh5)
return strh5
######################################################################################
[docs]
def set_photosensor_constants(strh5, initialise=True):
r"""Defining the constants that are necessary for calculation of photon energy, dark current rate, etc.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
#Sensor material constants
if (initialise):
strh5['rystare/material/Eg-eV'] = 0. #bandgap still to be computed at at temperature
# band gap energy, [eV], Varshni equation
strh5['rystare/material/Eg-eV'][...] = strh5['rystare/photondetector/varshni/Egap0'][()] - \
(strh5['rystare/photondetector/varshni/varA'][()] * (strh5['rystare/photondetector/operatingtemperature'][(
)] ** 2)) /\
(strh5['rystare/photondetector/varshni/varB'][()] + strh5['rystare/photondetector/operatingtemperature'][()])
if (initialise):
# do this always
# only used with 'Janesick-Gaussian'
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/limitnegative'] = True
#Fundamental constants
#Boltzman constant, [eV/K].
strh5['rystare/constants/Boltzman-Constant-eV'] = const.physical_constants['Boltzmann constant in eV/K'][0]
#Boltzman constant, [J/K].
strh5['rystare/constants/Boltzman-Constant-JK'] = const.physical_constants['Boltzmann constant'][0]
strh5['rystare/constants/q'] = const.e # charge of an electron [C], coulomb
return strh5
######################################################################################
[docs]
def check_create_datasets(strh5,initialise=True):
r"""Create the arrays to store the various image-sized variables.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
#determine the size of the sensor.
sensor_size = strh5['rystare/imageSizePixels'][()]
if (initialise):
#pre-allocating the matrices for photons, electrons, voltages and DNs.
# strh5['rystare/signal/photonRateIrradiance'] = np.zeros(sensor_size)
strh5['rystare/signal/photonRateIrradianceNU'] = np.zeros(sensor_size)
strh5['rystare/signal/electronRateIrradiance'] = np.zeros(sensor_size)
strh5['rystare/signal/electronRate'] = np.zeros(sensor_size)
# strh5['rystare/signal/photonRate'] = np.zeros(sensor_size)
# strh5['rystare/signal/photons'] = np.zeros(sensor_size)
strh5['rystare/quantumEfficiency'] = np.zeros(sensor_size)
strh5['rystare/signal/electrons'] = np.zeros(sensor_size)
strh5['rystare/signal/electronsWell'] = np.zeros(sensor_size)
strh5['rystare/signal/darkcurrentelectrons'] = np.zeros(sensor_size)
strh5['rystare/signal/darkcurrentelectronsnoDFPN'] = np.zeros(sensor_size)
# strh5['rystare/signal/light'] = np.zeros(sensor_size)
strh5['rystare/signal/lightelectronsnoshotnoise'] = np.zeros(sensor_size)
strh5['rystare/signal/lightelectrons'] = np.zeros(sensor_size)
strh5['rystare/signal/sensenodevoltageLinear'] = np.zeros(sensor_size)
strh5['rystare/signal/voltage'] = np.zeros(sensor_size)
strh5['rystare/signal/voltageAfterSF'] = np.zeros(sensor_size)
strh5['rystare/signal/voltageAfterSFnoise'] = np.zeros(sensor_size)
strh5['rystare/signal/voltageaftercds'] = np.zeros(sensor_size)
strh5['rystare/signal/DN'] = np.zeros(sensor_size)
strh5['rystare/photondetector/lightPRNU/value'] = np.zeros(sensor_size)
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/value'] = np.zeros(sensor_size)
strh5['rystare/noise/sn_reset/resetnoise'] = np.zeros(sensor_size)
strh5['rystare/noise/sn_reset/vrefreset'] = np.zeros(sensor_size)
strh5['rystare/noise/sn_reset/vrefresetpluskTC'] = np.zeros(sensor_size)
strh5['rystare/sourcefollower/source_follower_noise'] = np.zeros(sensor_size)
strh5['rystare/sourcefollower/fpoffset/value'] = np.ones(sensor_size)
strh5['rystare/signal/voltagebeforeSF'] = np.zeros(sensor_size)
strh5['rystare/ADC/gain'] = np.ones(sensor_size)
strh5['rystare/ADC/gainILE'] = np.ones(sensor_size)
strh5['rystare/sourcefollower/sigma'] = np.zeros((1,1))
strh5['rystare/sensenode/volt-fullwell'] = 0.
strh5['rystare/sensenode/volt-min'] = 0.
strh5['rystare/sensenode/capacitance'] = 0.
strh5['rystare/sensenode/ResetKTC-sigma'] = 0.
strh5['rystare/darkcurrentelectronsnonoise'] = 0.
strh5['rystare/detectorArea'] = -1.
if (strh5['rystare/sensenode/resetnoise/factor'][()] > 1.):
print(
f"Warning! The compensation factor "
f"{strh5['rystare/sensenode/resetnoise/factor'][()]} "
'(strh5["rystare/noise/sn_reset/Factor"]) you entered for the '
'Sense Node Reset Noise cannot be more than 1! '
'The factor is set to 1.')
strh5['rystare/sensenode/resetnoise/factor'][()] = 1.
else:
if (strh5['rystare/sensenode/resetnoise/factor'][()] < 0):
print(
f"Warning! The compensation factor "
f"{strh5['rystare/sensenode/resetnoise/factor'][()]} "
'(strh5["rystare/noise/sn_reset/Factor"]) you entered for the '
'Sense Node Reset Noise negative! '
'The factor is set to 0, SNReset noise is not simulated.')
strh5['rystare/sensenode/resetnoise/factor'][()]=0
else:
#pre-allocating the matrices for photons, electrons, voltages and DNs.
# strh5['rystare/signal/photonRateIrradiance'] = np.zeros(sensor_size)
strh5['rystare/signal/photonRateIrradianceNU'][...] = np.zeros(sensor_size)
strh5['rystare/signal/electronRateIrradiance'][...] = np.zeros(sensor_size)
strh5['rystare/signal/electronRate'][...] = np.zeros(sensor_size)
# strh5['rystare/signal/photonRate'] = np.zeros(sensor_size)
# strh5['rystare/signal/photons'] = np.zeros(sensor_size)
strh5['rystare/quantumEfficiency'][...] = np.zeros(sensor_size)
strh5['rystare/signal/electrons'][...] = np.zeros(sensor_size)
strh5['rystare/signal/electronsWell'][...] = np.zeros(sensor_size)
strh5['rystare/signal/darkcurrentelectrons'][...] = np.zeros(sensor_size)
strh5['rystare/signal/darkcurrentelectronsnoDFPN'][...] = np.zeros(sensor_size)
# strh5['rystare/signal/light'] = np.zeros(sensor_size)
strh5['rystare/signal/lightelectronsnoshotnoise'][...] = np.zeros(sensor_size)
strh5['rystare/signal/lightelectrons'][...] = np.zeros(sensor_size)
strh5['rystare/signal/sensenodevoltageLinear'][...] = np.zeros(sensor_size)
strh5['rystare/signal/voltage'][...] = np.zeros(sensor_size)
strh5['rystare/signal/voltageAfterSF'][...] = np.zeros(sensor_size)
strh5['rystare/signal/voltageAfterSFnoise'][...] = np.zeros(sensor_size)
strh5['rystare/signal/voltageaftercds'][...] = np.zeros(sensor_size)
strh5['rystare/signal/DN'][...] = np.zeros(sensor_size)
strh5['rystare/photondetector/lightPRNU/value'][...] = np.zeros(sensor_size)
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/value'][...] = np.zeros(sensor_size)
strh5['rystare/noise/sn_reset/resetnoise'][...] = np.zeros(sensor_size)
strh5['rystare/noise/sn_reset/vrefreset'][...] = np.zeros(sensor_size)
strh5['rystare/noise/sn_reset/vrefresetpluskTC'][...] = np.zeros(sensor_size)
strh5['rystare/sourcefollower/source_follower_noise'][...] = np.zeros(sensor_size)
strh5['rystare/sourcefollower/fpoffset/value'][...] = np.ones(sensor_size)
strh5['rystare/signal/voltagebeforeSF'][...] = np.zeros(sensor_size)
strh5['rystare/ADC/gain'][...] = np.ones(sensor_size)
strh5['rystare/ADC/gainILE'][...] = np.ones(sensor_size)
strh5['rystare/sourcefollower/sigma'][...] = np.zeros((1,1))
strh5['rystare/sensenode/volt-fullwell'][...] = 0.
strh5['rystare/sensenode/volt-min'][...] = 0.
strh5['rystare/sensenode/capacitance'][...] = 0.
strh5['rystare/sensenode/ResetKTC-sigma'][...] = 0.
strh5['rystare/darkcurrentelectronsnonoise'][...] = 0.
strh5['rystare/detectorArea'][...] = -1.
if (strh5['rystare/sensenode/resetnoise/factor'][()] > 1.):
print(
f"Warning! The compensation factor "
f"{strh5['rystare/sensenode/resetnoise/factor'][()]} "
'(strh5["rystare/noise/sn_reset/Factor"]) you entered for the '
'Sense Node Reset Noise cannot be more than 1! '
'The factor is set to 1.')
strh5['rystare/sensenode/resetnoise/factor'][()] = 1.
else:
if (strh5['rystare/sensenode/resetnoise/factor'][()] < 0):
print(
f"Warning! The compensation factor "
f"{strh5['rystare/sensenode/resetnoise/factor'][()]} "
'(strh5["rystare/noise/sn_reset/Factor"]) you entered for the '
'Sense Node Reset Noise negative! '
'The factor is set to 0, SNReset noise is not simulated.')
strh5['rystare/sensenode/resetnoise/factor'][()]=0
return strh5
######################################################################################
[docs]
def source_follower(strh5,initialise=True):
r"""The amplification of the voltage from Sense Node by Source Follower.
Conventional sensor use a floating-diffusion sense node followed by a
charge-to-voltage amplifier, such as a source follower.
.. image:: _images/source_follower.png
:width: 541px
:height: 793px
:align: center
:scale: 30 %
Source follower is one of basic single-stage field effect transistor (FET)
amplifier topologies that is typically used as a voltage buffer. In such a
circuit, the gate terminal of the transistor serves as the input, the source is
the output, and the drain is common to both input and output. At low
frequencies, the source follower has voltage gain:
:math:`{A_{\text{v}}} = \frac{v_{\text{out}}}{v_{\text{in}}} = \frac{g_m R_{\text{S}}}{g_m R_{\text{S}} + 1}
\approx 1 \qquad (g_m R_{\text{S}} \gg 1)`
Source follower is a voltage follower, its gain is less than 1. Source followers
are used to preserve the linear relationship between incident light, generated
photoelectrons and the output voltage.
The V/V non-linearity affect shot noise (but does not affect FPN curve) and can
cause some shot-noise probability density compression. The V/V
non-linearity non-linearity is caused by non-linear response in ADC or source
follower.
The V/V non-linearity can be simulated as a change in source follower gain
:math:`A_{SF}` as
a linear function of signal:
:math:`A_{SF_{new}} = \alpha \cdot \frac{V_{REF} - S(V_{SF}) }{V_{REF} } + A_{SF},`
where :math:`\alpha = A_{SF}\cdot\frac{\gamma_{nlr} -1}{ V_{FW} }` and
:math:`\gamma_{nlr}` is a non-linearity ratio of :math:`A_{SF}`. In the simulation
we assume :math:`A_{SF} = 1` and :math:`\gamma_{nlr} = 1.05` i.e. 5\% of
non-linearity of :math:`A_{SF}`. Then the voltage is multiplied on the new sense
node gain :math:`A_{SF_{new}}`:
:math:`I_{V} = I_{V}\cdot A_{SF_{new}}`
After that, the voltage goes to ADC for quantisation to digital numbers.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
strh5['rystare/signal/voltagebeforeSF'][...] = strh5['rystare/signal/voltage'][()]
sones = np.ones(strh5['rystare/imageSizePixels'][()])
if (initialise):
strh5['rystare/sourcefollower/gainA'] = strh5['rystare/sourcefollower/gain'][()] * sones
else:
strh5['rystare/sourcefollower/gainA'][...] = strh5['rystare/sourcefollower/gain'][()] * sones
# calculating Source Follower VV non-linearity
if strh5['rystare/sourcefollower/nonlinearity/activate'][()]:
# nonlinearity_alpha = (strh5['rystare/sourcefollower/nonlinearity/ratio'][()] - 1) * \
# (strh5['rystare/sourcefollower/gain'][()] / strh5['rystare/sensenode/volt-fullwell'][()])
# # diagram node 17 source follower nonlinearity stored in 'rystare/sourcefollower/gainA'
# strh5['rystare/sourcefollower/gainA'][...] = sones * nonlinearity_alpha * \
# ((strh5['rystare/sensenode/vrefreset'][()] - strh5['rystare/signal/voltage'][()]) / \
# (strh5['rystare/sensenode/vrefreset'][()])) + \
# (strh5['rystare/sourcefollower/gain'][()])
# diagram node 17 source follower nonlinearity stored in 'rystare/sourcefollower/gainA'
nonlinearity_alpha = sones * (strh5['rystare/sourcefollower/nonlinearity/ratio'][()] - 1.)
strh5['rystare/sourcefollower/gainA'][...] = strh5['rystare/sourcefollower/gain'][()] * \
(sones - nonlinearity_alpha * \
(strh5['rystare/sensenode/vrefreset'][()] - strh5['rystare/signal/voltage'][()]) / \
(strh5['rystare/sensenode/vrefreset'][()] - strh5['rystare/sensenode/volt-fullwell'][()]) )
# #Source Follower signal
# strh5['rystare/signal/voltage'][...] = (
# strh5['rystare/signal/voltage'][()]) * strh5['rystare/sourcefollower/gainA'][()]
strh5['rystare/signal/voltageAfterSF'][...] = strh5['rystare/signal/voltage'][()] * \
strh5['rystare/sourcefollower/gainA'][()]
return strh5
######################################################################################
[docs]
def fixed_pattern_offset(strh5):
"""Add dark fixed pattens offset
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
strh5['rystare/sourcefollower/fpoffset/value'][...] = np.zeros(tuple(strh5['rystare/imageSizePixels'][()]))
senstype = strh5['rystare/sensortype'][()]
if type(senstype) is not str:
senstype = senstype.decode('UTF-8')
if senstype in 'CMOS':
#If the sensor is CMOS and the darkoffset/NU is on
if strh5['rystare/sourcefollower/fpoffset/activate'][()]:
# add dark fixed pattern offset
sigma = strh5['rystare/sensenode/volt-fullwell'][()] * \
strh5['rystare/sourcefollower/fpoffset/sigma'][()]
noisematrix = FPN_models(strh5['rystare/imageSizePixels'][()][0],
strh5['rystare/imageSizePixels'][()][1], 'column',
strh5['rystare/sourcefollower/fpoffset/model'][()], sigma)
strh5['rystare/sourcefollower/fpoffset/value'][...] = noisematrix
# diagram node 21 fixed pattern offset in volts stored in 'rystare/sourcefollower/fpoffset/value'
return strh5
######################################################################################
[docs]
def cds(strh5):
"""Reducing the noise by Correlated Double Sampling, but right now the routine just adds the noise.
Correlated Double Sampling (CDS) is a technique for measuring photo voltage
values that removes an undesired noise. The sensor's output is measured twice.
Correlated Double Sampling is used for compensation of Fixed pattern
noise caused by dark current leakage,
irregular pixel converters and the like. It appears on the same pixels at
different times when images are taken. It can be suppressed with noise reduction
and on-chip noise reduction technology. The main approach is CDS, having one
light signal read by two circuits.
In CDS, a circuit measures the difference between the reset voltage and the
signal voltage for each pixel, and assigns the resulting value of charge to the
pixel. The additional step of measuring the output node reference voltage before
each pixel charge is transferred makes it unnecessary to reset to the same level
for each pixel.
First, only the noise is read. Next, it is read in combination with the light
signal. When the noise component is subtracted from the combined signal, the
fixed-pattern noise can be eliminated.
CDS is commonly used in image sensors to reduce FPN and reset noise. CDS only
reduces offset FPN (gain FPN cannot be reduced using CDS). CDS in CCDs, PPS,
and photogate APS, CDS reduces reset noise, in photodiode APS it increases it
See Janesick's book and especially El Gamal's lectures.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
# diagram node 23 fixed pattern offset added to signal in volts stored in 'rystare/signal/voltageaftercds'
strh5['rystare/signal/voltageaftercds'][...] = \
strh5['rystare/signal/voltagebeforecds'][()] * strh5['rystare/sourcefollower/CDS/gain']
return strh5
######################################################################################
[docs]
def adc(strh5):
r"""An analogue-to-digital converter (ADC) transforms a voltage signal into discrete codes.
An analogue-to-digital converter (ADC) transforms a voltage signal into discrete
codes. An :math:`N`-bit ADC has :math:`2^N` possible output codes with the difference between code
being :math:`V_{ADC.REF}/2^N`. The resolution of the ADC indicates the number of
discrete values that can be produced over the range of analogue values and can
be expressed as:
:math:`K_{ADC} = \frac{V_{ADC.REF} - V_\mathrm {min}}{N_{max}}`
where :math:`V_\mathrm{ADC.REF}` is the maximum voltage that can be quantified,
:math:`V_{min}` is minimum quantifiable voltage, and :math:`N_{max} = 2^N` is the number of
voltage intervals. Therefore, the output of an ADC can be represented as:
:math:`ADC_{Code} = \textrm{round}\left( \frac{V_{input}-V_{min}}{K_{ADC}} \right)`
The lower the reference voltage :math:`V_{ADC.REF}`, the smaller the range of the
voltages one can measure.
After the electron matrix has been converted to voltages, the sense node reset
noise and offset FPN noise are added, the V/V gain non-linearity is applied (if
desired), the ADC non-linearity is applied (if necessary). Finally the result is
multiplied by ADC gain and rounded to produce the signal as a digital number:
:math:`I_{DN} = \textrm{round} (A_{ADC}\cdot I_{total.V}),`
where :math:`I_\textrm{total.V} = (V_{ADC.REF} - I_{V})` is the total voltage signal
accumulated during one frame acquisition, :math:`V_{ADC.REF}` is the maximum voltage
that can be quantified by an ADC, and :math:`I_V` is the total voltage signal
accumulated by the end of the exposure (integration) time and conversion.
Usually :math:`I_V = I_{SN.V}` after the optional V/V non-linearity is applied. In
this case, the conversion from voltages to digital signal is linear. The
adcnonlinearity "non-linear ADC case is considered below".
In terms of the ADC, the following non-linearity and noise should be considered
for the simulations of the photosensors: Integral Linearity Error, Differential
Linearity Error, quantisation error, and ADC offset.
The DLE indicates the deviation from the ideal 1 LSB (Least Significant Bit)
step size of the analogue input signal corresponding to a code-to-code
increment. Assume that the voltage that corresponds to a step of 1 LSB is
:math:`V_{LSB}`. In the ideal case, a change in the input voltage of :math:`V_{LSB}` causes
a change in the digital code of 1 LSB. If an input voltage that is more than
:math:`V_{LSB}` is required to change a digital code by 1 LSB, then the ADC has DLE
error. In this case, the digital output remains constant when the input
voltage changes from, for example, :math:`2 V_{LSB}` to :math:`4 V_{LSB}`, therefore
corresponding the digital code can never appear at the output. That is, that
code is missing.
.. image:: _images/dle.png
:width: 1360px
:height: 793px
:align: center
:scale: 40 %
In the illustration above, each input step should be precisely 1/8 of reference
voltage. The first code transition from 000 to 001 is caused by an input change
of 1 LSB as it should be. The second transition, from 001 to 010, has an input
change that is 1.2 LSB, so is too large by 0.2 LSB. The input change for the
third transition is exactly the right size. The digital output remains
constant when the input voltage changes from 4 LSB to 5 LSB, therefore the code
101 can never appear at the output.
The ILE is the maximum deviation of the input/output characteristic from a
straight line passed through its end points. For each voltage in the ADC input,
there is a corresponding code at the ADC output. If an ADC transfer function is
ideal, the steps are perfectly superimposed on a line. However, most real ADC's
exhibit deviation from the straight line, which can be expressed in percentage
of the reference voltage or in LSBs. Therefore, ILE is a measure of the
straightness of the transfer function and can be greater than the differential
non-linearity. Taking the ILE into account is important because it cannot be
calibrated out.
.. image:: _images/ILE.png
:width: 1360px
:height: 715px
:align: center
:scale: 40 %
For each voltage in the ADC input there is a corresponding word at the ADC
output. If an ADC is ideal, the steps are perfectly superimposed on a line. But
most of real ADC exhibit deviation from the straight line, which can be
expressed in percentage of the reference voltage or in LSBs.
In our model, we simulate the Integral Linearity Error (ILE) of the ADC as a
dependency of ADC gain :math:`A_{ADC.linear}` on the signal value. Denote
:math:`\gamma_{ADC.nonlin}` as an ADC non-linearity ratio (e.g., :math:`\gamma_{ADC.nonlin}
= 1.04`). The linear ADC gain can be calculated from Eq.~\ref{eq:kadc} as
:math:`A_{ADC} = 1/K_{ADC}` and used as :math:`A_{ADC.linear}`. The non-linearity
coefficient :math:`\alpha_{ADC}` is calculated as:
:math:`\alpha_{ADC} = \frac{1}{V_{ADC.REF}} \left( \frac{ \log(\gamma_{ADC.nonlin}
\cdot A_{ADC.linear} )}{\log(A_{ADC.linear})} - 1 \right)`
where :math:`V_\mathrm{ADC.REF}` is the maximum voltage that can be quantified by an
ADC:
:math:`A_{ADC.nonlin} = A_{ADC.linear}^{1-\alpha_{ADC} I_{total.V}},`
where :math:`A_{ADC.linear}` is the linear ADC gain. The new non-linear ADC conversion
gain :math:`A_{ADC.nonlin}` is then used for the simulations.
Quantisation errors are caused by the rounding, since an ADC has a
finite precision. The probability distribution of quantisation noise is
generally assumed to be uniform. Hence we use the uniform distribution to model
the rounding errors.
It is assumed that the quantisation error is uniformly distributed between -0.5
and +0.5 of the LSB and uncorrelated with the signal. Denote :math:`q_{ADC}` the
quantising step of the ADC. For the ideal DC, the quantisation noise is:
:math:`\sigma_{ADC} = \sqrt{ \frac{q_{ADC}^2 }{12}}.`
If :math:`q_{ADC} = 1` then the quantisation noise is :math:`\sigma_{ADC} = 0.29` DN. The
quantisation error has a uniform distribution. We do not assume any particular
architecture of the ADC in our high-level sensor model.
This routine performs analogue-to-digital convertation of volts to DN.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
# maximum number of DN.
N_max = 2 ** (strh5['rystare/ADC/num-bits'][()])
# gain in DN/v
adc_gain = N_max / \
(strh5['rystare/sensenode/volt-fullwell'][()] - strh5['rystare/sensenode/volt-min'][()])
# before this multiplication strh5['rystare/ADC/gain'][()] is unity
strh5['rystare/ADC/gain'][...] = adc_gain * strh5['rystare/ADC/gain'][()]
# Removing the reference Voltage
signal = strh5['rystare/sensenode/vrefreset'][()] - strh5['rystare/signal/voltageaftercds'][()]
if strh5['rystare/ADC/nonlinearity/activate'][()]:
# account for non-linearity
numerator = strh5['rystare/ADC/nonlinearity/ratio'][()] * adc_gain
nonlinearity_alpha = (np.log(numerator) / np.log(adc_gain) - 1) \
/ strh5['rystare/sensenode/volt-fullwell'][()]
strh5['rystare/ADC/gain'][...] = strh5['rystare/ADC/gain'][()] ** (1 - nonlinearity_alpha * signal)
# diagram node 24 ADC integral linearity error stored in 'rystare/ADC/gainILE'
strh5['rystare/ADC/gainILE'][...] = strh5['rystare/ADC/gain'][()] / (adc_gain * \
np.ones(strh5['rystare/imageSizePixels'][()]))
# diagram node 24b ADC gain stored in 'rystare/ADC/gain'
#DN given by offset plus signal * gain
signalDN = np.round(strh5['rystare/ADC/offset'][()] + strh5['rystare/ADC/gain'][()] * signal)
# Truncating the numbers that are less than 0 or larger than N-max
# diagram node 25 signal after ADC stored in 'rystare/signal/DN'
signalDN[signalDN <= 0] = 0
signalDN[signalDN >= N_max] = N_max
strh5['rystare/signal/DN'][...] = signalDN
return strh5
######################################################################################
[docs]
def charge_to_voltage(strh5):
r"""The charge to voltage conversion occurs inside this routine
V/e nonlinearity is small for CCD detetors, but can be very high for some CMOS
architectures (up to 200%) [from Janesick p87]
A new matrix strh5['rystare/signal/voltage'] is created and the raw
voltage signal is stored.
After the charge is generated in the pixel by photo-effect, it is moved
row-by-row to the sense amplifier that is separated from the pixels in case of
CCD. The packets of charge are being shifted to the output sense node,
where electrons are converted to voltage. The typical sense node region is
presented on Figure below.
.. image:: _images/CCD-sensenoderegion.png
:width: 812px
:align: center
:scale: 50 %
The sense node is the final collecting point at the end of the horizontal
register of the CCD sensor. The CCD pixels are made with MOS devices used as
reverse biased capacitors. The charge is readout by a MOSFET based charge to
voltage amplifier. The output voltage is inversely proportional to the sense
node capacitor. Typical example is that the sense node capacitor of the order
:math:`50fF`, which produces a gain of :math:`3.2 \mu V/ e^-`. It is also important
to minimize the noise of the output amplifier, \textbf{typically the largest
noise source in the system}. Sense node converts charge to voltage with typical
sensitivities :math:`1\dots 4 \mu V/e^-`.
The charge collected in each pixel of a sensor array is converted to voltage
by sense capacitor and source-follower amplifier.
Reset noise is induced during such conversion. Prior to the measurement
of each pixel's charge, the CCD sense capacitor is reset to a reference level.
Sense node converts charge to voltage with typical sensitivities :math:`1\dots 4 \mu V/e^-`.
The charge collected in each pixel of a sensor array is converted to
voltage by sense capacitor and source-follower amplifier.
Reset noise is induced during such conversion. Prior to the measurement of each
pixel's charge, the CCD sense node capacitor is reset to a reference level.
Sense Node gain non-linearity, or V/e non-linearity
The V/:math:`e^-` non-linearity affect both FPN and shot noise and can cause
some shot-noise probability density compression. This type of non-linearity is
due to sense node gain non-linearity. Then sense node sensitivity became
non-linear (see Janesick's book):
:math:`S_{SN} ( V_{SN}/e^- ) = \frac{S(V_{SN}) }{(k_1/q) \ln( V_{REF}/[V_{REF} - S(V_{SN})] )}`
The V/:math:`e^-` non-linearity can be expressed as a non-linear dependency of
signals in electron and a sense-node voltage:
:math:`S[e^-] = \frac{k1}{q} \ln \left[ \frac{V_{REF}}{ V_{REF} - S(V_{SN}) } \right]`
The V/:math:`e^-` non-linearity affects photon shot noise and skews the
distribution, however this is a minor effect. The V/:math:`e^-` non-linearity can
also be thought as a sense node capacitor non-linearity: when a small signal is
measured, :math:`C_{SN}` is fixed or changes negligible; on the other hand,
:math:`C_{SN}` changes significantly and that can affect the signal being
measured.
For the simulation purpose, the V/:math:`e^-` non-linearity can be expressed as:
:math:`V_{SN} = V_{REF} - S(V_{SN}) = V_{REF}\exp\left[ - \frac{\cdot S[e^-]\cdot q }{k1} \right]`
where :math:`k1=10.909*10^{-15}` and :math:`q` is the charge of an electron.
The nonlinear capacitance is given by C = k1/V
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
#input from diagram node 13 signal in electrons after charge well clipping in 'rystare/signal/electronsWell'
# this is the signal electrons to voltage, if linear
# diagram node 14 electrons in well converted to voltage Q=CV stored in 'rystare/signal/sensenodevoltageLinear'
strh5['rystare/signal/sensenodevoltageLinear'][...] = strh5['rystare/signal/electronsWell'][()] * \
strh5['rystare/sensenode/gain'][()]
# Sense node capacitance, parameter, [F] Farad
strh5['rystare/sensenode/capacitance'][...] = strh5['rystare/constants/q'][()] / strh5['rystare/sensenode/gain'][()]
#no Sense Node Reset Noise, ony vref for CCD, later overwritten for CMOS
# this is the zero level voltage, typically 3 V
# for CCD kTC remains zero, fr cmos overwrite just now
# diagram node 15b reset voltage with kTC noise stored in 'rystare/noise/sn_reset/vrefresetpluskTC'
strh5['rystare/noise/sn_reset/vrefresetpluskTC'][...] = strh5['rystare/sensenode/vrefreset'][()]
senstype = strh5['rystare/sensortype'][()]
if type(senstype) is not str:
senstype = senstype.decode('UTF-8')
if senstype in ['CMOS']:
if strh5['rystare/sensenode/resetnoise/activate'][()]:
# Obtain the matrix for the Sense Node Reset Noise, saved in strh5['rystare/noise/sn_reset/resetnoise']
# sigma is stored in 'rystare/sensenode/ResetKTC-sigma'
# diagram node 15 reset noise stored in 'rystare/noise/sn_reset/resetnoise'
strh5 = sense_node_reset_noise(strh5,seed=strh5['rystare/sensenode/resetnoise/seed'][()])
# diagram node 15b reset voltage with kTC noise stored in 'rystare/noise/sn_reset/vrefresetpluskTC'
strh5['rystare/noise/sn_reset/vrefresetpluskTC'][...] = strh5['rystare/sensenode/vrefreset'][()] + \
strh5['rystare/sensenode/resetnoise/factor'][()] * strh5['rystare/noise/sn_reset/resetnoise'][()]
#voltage on the Full Well
strh5['rystare/sensenode/volt-fullwell'][...] = strh5['rystare/sensenode/gain'][()] * \
strh5['rystare/sensenode/fullwellelectronselection/fullwellelectrons'][()]
strh5['rystare/sensenode/volt-min'][...] = strh5['rystare/sensenode/gain'][()] * 1.0
else: # CCD
#voltage on the Full Well
strh5['rystare/sensenode/volt-fullwell'][...] = strh5['rystare/sensenode/gain'][()] * \
strh5['rystare/sensenode/fullwellelectronselection/fullwellelectrons'][()]
strh5['rystare/sensenode/volt-min'][...] = strh5['rystare/sensenode/gain'][()] * 1.0
if strh5['rystare/sensenode/gainresponse/type'][()] in ['nonlinear']:
strh5['rystare/signal/voltage'][...] = strh5['rystare/noise/sn_reset/vrefresetpluskTC'][()] * \
(np.exp(- strh5['rystare/constants/q'][()] * \
strh5['rystare/signal/electronsWell'][()] / strh5['rystare/sensenode/gainresponse/k1'][()]))
else:
strh5['rystare/signal/voltage'][...] = strh5['rystare/noise/sn_reset/vrefresetpluskTC'][()] -\
strh5['rystare/signal/sensenodevoltageLinear'][()]
# diagram node 16 signal in volts after nonlinearity (if present) stored in 'rystare/signal/voltage'
return strh5
######################################################################################
[docs]
def sense_node_reset_noise(strh5,seed=None):
r"""This routine calculates the noise standard deviation for the sense node reset noise.
Sense node Reset noise (kTC noise)
Prior to the measurement of each pixel's charge packet, the sense node capacitor
is reset to a reference voltage level. Noise is generated at the sense node by
an uncertainty in the reference voltage level due to thermal variations in the
channel resistance of the MOSFET reset transistor. The reference level of the
sense capacitor is therefore different from pixel to pixel.
Because reset noise can be significant (about 50 rms electrons), most
high-performance photosensors incorporate a noise-reduction mechanism such as
correlated double sampling (CDS).
kTC noise occurs in CMOS sensors, while for CCD sensors the sense
node reset noise is removed~ (see Janesick's book) by Correlated Double
Sampling (CDS). Random fluctuations of charge on the sense node during the reset
stage result in a corresponding photodiode reset voltage fluctuation. The sense
node reset noise (in volt units) is given by:
:math:`\sigma_{RESET}=\sqrt{\frac{k_B T}{C_{SN}}}`
By the relationship Q=CV it can be shown that the kTC noise can be expressed
as electron count by
:math:`\sigma_{RESET}=\frac{\sqrt{k_B T C_{SN}}}{q} = \frac{k_B T}{q A_{SN}}`
see also https://en.wikipedia.org/wiki/Johnson%E2%80%93Nyquist_noise
The simulation of the sense node reset noise may be performed as an addition of
non-symmetric probability distribution to the reference voltage :math:`V_{REF}`.
However, the form of distribution depends on the sensor's architecture and the
reset technique. An Inverse-Gaussian distribution can be
used for the simulation of kTC noise that corresponds to a hard reset technique
in the CMOS sensor, and the Log-Normal distribution can be used for soft-reset
technique. The sense node reset noise can be simulated for each :math:`(i,j)`-th pixel
for the soft-reset case as:
:math:`I_{SN.reset.V}=ln\mathcal{N}(0,\sigma_{RESET}^2)`
then added to the matrix :math:`I_{REF.V}` in Volts that corresponds to the reference voltage.
Note: For CCD, the sense node reset noise is entirely removed by CDS.
Note: In CMOS photosensors, it is difficult to remove the reset noise for the specific CMOS pixels
architectures even after application of CDS. Specifically, the difficulties
arise in 'rolling shutter' and 'snap' readout modes.
The reset noise is increasing after CDS by a factor of :math:`\sqrt{2}`.
Elimination of reset noise in CMOS is quite challenging.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
#in [V], see Janesick, Photon Transfer, page 166.
strh5['rystare/sensenode/ResetKTC-sigma'][...] = \
np.sqrt((strh5['rystare/constants/Boltzman-Constant-JK'][()]) * \
(strh5['rystare/photondetector/operatingtemperature'][()]) / \
(strh5['rystare/sensenode/capacitance'][()]))
# #randomising the state of the noise generators
#If seed is omitted or None, current system time is used
np.random.seed(seed)
# Soft-Reset prob distribution for the CMOS noise
# use the exp() to get log normal, then subtract 1 to move the mean value to zero.
# for the small values used here, it is pretty much gaussian anyway!
strh5['rystare/noise/sn_reset/resetnoise'][...] = \
np.exp(strh5['rystare/sensenode/ResetKTC-sigma'][()] * \
np.random.standard_normal(tuple(strh5['rystare/imageSizePixels'][()]))) - 1.
# diagram node 15 reset noise in electrons stored in 'rystare/noise/sn_reset/resetnoise'
return strh5
######################################################################################
[docs]
def dark_current_and_dark_noises(strh5):
r"""This routine for adding dark current signals and noise, including dark FPN and dark shot noise.
This model is taken from Janesick's 'Photon Transfer' book, page 168, which in turn is taken from
Janesick's 'Scientific Charge-Coupled Devices' book, page 622.
The dark signal is calculated for all pixels in the model. It is implemented using `ones` function in
MATLAB as a matrix of the same size as the simulated photosensor. For each :math:`(i,j)`-th pixel we have:
:math:`I_{dc.e^-} = t_I\cdot D_R,`
where :math:`D_R` is the average dark current (originally derived for silicon):
:math:`D_R = 2.55\cdot10^{15}P_A D_{FM} T^{1.5} \exp\left[-\frac{E_{gap}}{2\cdot k\cdot T}\right],`
where:
:math:`D_R` is in units of [e\ :sup:`-1`\ /s],
:math:`P_A` is the pixel's area [cm\ :sup:`2`\ ];
:math:`D_{FM}` is the dark current figure-of-merit in units of [nA/cm\ :sup:`2`\ ] at 300K,
varies significantly with detector material and sensor manufacturer, and used in this simulations
as 0.5 nA/cm\ :sup:`2` for silicon;
:math:`E_{gap}` is the bandgap energy of the semiconductor which also varies with temperature;
:math:`k` is Boltzman's constant that is :math:`8.617\cdot10^{-5} [eV/K].`
The relationship between band gap energy and temperature can be described by Varshni's empirical expression,
:math:`E_{gap}(T)=E_{gap}(0)-\frac{\alpha T^2}{T+\beta},`
where :math:`E_{gap}(0)`, :math:`\alpha` and :math:`\beta` are material constants. The energy bandgap of
semiconductors tends to decrease as the temperature is increased. This behaviour can be better understood
if one considers that the inter-atomic spacing increases when the amplitude of the atomic vibrations
increases due to the increased thermal energy. This effect is quantified by the linear expansion
coefficient of a material.
For the Silicon: :math:`E_{gap}(0) = 1.1557 [eV]`, :math:`\alpha = 7.021*10^{-4}` [eV/K], and
:math:`\beta = 1108` [K].
It appears that fill factor does not apply to dark noise (Janesick book p168 and Konnik's code
does not show this).
According to Janesick's Photon transfer book p169 the dark current FPN standard deviation is around
10% (CCD) and 40% (CMOS) of the dark current. Note that 'dark' FPN (DN) is much greater than 'light'
FPN (PN) by approximately 10 to 40 times.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields, dark current in electrons
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
# Dark current generation, caused over full detector pitch area, not active area only
# translating the size to square centimeters, as in Janesick book.
detarea = strh5['rystare/pixelPitch'][()][0] * strh5['rystare/pixelPitch'][()][0]
#average quantity of dark current that is thermally generated [e] !!! This is Janesick equations 11.15 and 11.16
# diagram node 8 dc average dark current stored in 'rystare/darkcurrentelectronsnonoise' in nA
# parameters must be adjusted for the detector material as applicable
strh5['rystare/darkcurrentelectronsnonoise'][...] = \
strh5['rystare/photondetector/integrationtime'][()] * (
strh5['rystare/photondetector/darkcurrent/ca'][()] / const.e) * \
detarea * strh5['rystare/photondetector/darkcurrent/densityAm2'][()] * \
(strh5['rystare/photondetector/operatingtemperature'][()] ** 1.5) * \
np.exp(- strh5['rystare/material/Eg-eV'][()] / \
(strh5['rystare/photondetector/darkcurrent/ed'][()] * \
strh5['rystare/constants/Boltzman-Constant-eV'][()] * \
strh5['rystare/photondetector/operatingtemperature'][()]))
#creating an image with the dark current value
strh5['rystare/signal/darkcurrentelectrons'][...] = strh5['rystare/darkcurrentelectronsnonoise'][()] * \
np.ones(strh5['rystare/signal/electrons'][()].shape)
# add shot noise, based on average value
# diagram node 9 dc dark current with noise image stored in 'rystare/darkcurrentelectrons' in nA
if strh5['rystare/photondetector/darkcurrent/shotnoise/activate'][()]:
strh5['rystare/signal/darkcurrentelectrons'][...] = shotnoise(strh5['rystare/signal/darkcurrentelectrons'][()])
strh5['rystare/signal/darkcurrentelectronsnoDFPN'][...] = strh5['rystare/signal/darkcurrentelectrons'][()]
# multiply with dark current FPN
if strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/activate'][()]:
strh5 = responsivity_FPN_dark(strh5)
return strh5
######################################################################################
[docs]
def source_follower_noise(strh5,initialise=True):
r"""The source follower noise routine, calculates noise in volts.
The pixel's source follower noise limits the read noise, however in high-end CCD and CMOS cameras the source
follower noise has been driven down to one electron rms.
Pixel source follower MOSFET noise consists of three types of noise:
- white noise;
- flicker noise;
- random telegraph noise (RTS).
Each type of noise has its own physics that will be briefly sketched below.
*Johnson noise (white noise)*
Similarly to the reset noise in sense node, the source-follower amplifier MOSFET has a resistance that
generates thermal noise whose value is governed by the Johnson white noise equation.
It is therefore either referred to as Johnson noise or simply as white noise,
since its magnitude is independent of frequency.
If the effective resistance is considered to be the output impedance of the source-follower amplifier,
the white noise,
in volts, is determined by the following equation:
:math:`N_{white} (V_{SF}) = \sqrt{4kTBR_{SF}}`
where :math:`k` is Boltzmann's constant (J/K), :math:`T` is temperature [K],
:math:`B` refers to the noise power bandwidth [Hz],
and :math:`R_{SF}` is the output impedance of the source-follower amplifier.
*Flicker noise*
The flicker noise is commonly referred to as :math:`1/f` noise because of its approximate
inverse dependence on frequency.
For cameras in which pixels are read out at less than approximately 1 megahertz,
and with a characteristic :math:`1/f` noise
spectrum, the read noise floor is usually determined by 1/f noise. Note that the noise continues to decrease at this
rate until it levels off, at a frequency referred to as the :math:`1/f` corner frequency. For the typical MOSFET
amplifier, the white noise floor occurs at approximately 4.5 :math:`nV/Hz^{1/2}`.
Prominent sources of :math:`1/f` noise in an image sensor are pink-coloured noise generated in the photo-diodes and
the low-bandwidth analogue operation of MOS transistors due to imperfect contacts between two
materials. Flicker noise is generally accepted to originate due to the existence of
interface states in the image sensor
silicon that turn on and off randomly according to different time constants. All systems exhibiting 1/f behaviour
have a similar collection of randomly-switching states. In the MOSFET, the states are traps at the silicon-oxide
interface, which arise because of disruptions in the silicon lattice at the surface. The level of :math:`1/f` noise
in a CCD sensor depends on the pixel sampling rate and from certain crystallographic orientations of silicon
wafer.
*Random Telegraph Signal (RTS) noise*
As the CCD and CMOS pixels are shrinking in dimensions, the low-frequency noise increases.
In such devices, the low-frequency noise performance is dominated by Random Telegraph Signals (RTS) on top
of the 1/f noise. The origin of such an RTS is attributed to the random trapping and
de-trapping of mobile charge carriers
in traps located in the oxide or at the interface. The RTS is observed in MOSFETs as a fluctuation in the drain
current. A pure two-level RTS is represented in the frequency domain by a Lorentzian spectrum.
Mathematically the source follower's noise power spectrum can be described as:
:math:`S_{SF}(f) = W(f)^2 \cdot \left(1 + \frac{f_c}{f}\right)+S_{RTS}(f),`
where :math:`W(f)` is the thermal white noise [:math:`V/Hz^{1/2}`, typically :math:`15 nV/Hz^{1/2}` ], flicker noise
corner frequency :math:`f_c` in [Hz] (flicker noise corner frequency is the frequency
where power spectrum of white and flicker noise are equal),
and the RTS power spectrum is given (see Janesick's book):
:math:`S_{RTS}(f) = \frac{2\Delta I^2 \tau_{RTS}}{4+(2\pi f \tau_{RTS})^2},`
where :math:`\tau_{RTS}` is the RTS characteristic time constant [sec] and
:math:`\Delta I` is the source follower current modulation induced by RTS [A].
The source follower noise can be approximated as:
:math:`\sigma_{SF} = \frac{N}{D}`
where:
- :math:`\sigma_{SF}` is the source follower noise [e- rms]
- :math:`N=\sqrt{\int\limits_{0}^{\infty} S_{SF}(f) H_{CDS}(f) df }`
- :math:`D=A_{SN}A_{SF}(1-\exp^{-t_s/\tau_D})`
- :math:`f` is the electrical frequency [Hz]
- :math:`t_s` is the CDS sample-to-sampling time [sec]
- :math:`\tau_D` is the CDS dominant time constant usually set as :math:`\tau_D = 0.5t_s` [sec].
The :math:`H_{CDS}(f)` function is the CDS transfer function is (see Janesick's book):
:math:`H_{CDS}(f) = \frac{1}{1+(2\pi f \tau_D)^2} \cdot [2-2\cos(2\pi f t_s)]`
First term sets the CDS bandwidth for the white noise rejection before sampling takes place
through :math:`B = 1/(4\tau_D)`, where :math:`B` is defined as the noise equivalent bandwidth [Hz].
Note: In CCD photosensors, source follower noise is typically limited by the flicker noise.
Note: In CMOS photosensors, source follower noise is typically limited by the RTS noise.
As a side note, such subtle kind of noises is visible only on high-end ADC like 16 bit and more.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
# this is the CDS dominant time constant usually set as :math:`\tau_D = 0.5t_s` [sec].
tau_D = 0.5 * (strh5['rystare/sourcefollower/CDS/sampletosamplingtime'][()])
tau_RTN = 0.1 * tau_D
#frequency, with delta_f as a spacing.
numFsamp = strh5['rystare/sourcefollower/dataclockspeed'][(
)] / strh5['rystare/sourcefollower/freqsamplingdelta'][()]
f = np.linspace(1., strh5['rystare/sourcefollower/dataclockspeed'][()], int(numFsamp)).reshape(1,-1)
if (initialise):
strh5['rystare/sourcefollower/noise/spectralfreq'] = f
else:
strh5['rystare/sourcefollower/noise/spectralfreq'][...] = f
#CDS transfer function
H_CDS = (2. - 2. * np.cos(2. * np.pi * strh5['rystare/sourcefollower/CDS/sampletosamplingtime'][()] * \
f)) / (1. + (2. * np.pi * tau_D * f) ** 2)
if (initialise):
strh5['rystare/sourcefollower/noise/cdsgain'] = H_CDS
else:
strh5['rystare/sourcefollower/noise/cdsgain'][...] = H_CDS
# RTN noise only in CMOS photosensors
S_RTN = np.zeros(f.shape)
senstype = strh5['rystare/sensortype'][()]
if type(senstype) is not str:
senstype = senstype.decode('UTF-8')
if senstype in ['CMOS']:
S_RTN = (2. * ((strh5['rystare/sourcefollower/noise/deltaindmodulation'][()]) ** 2) * tau_RTN) / (4 + (2 * \
np.pi * tau_RTN *f) ** 2)
if (initialise):
strh5['rystare/sourcefollower/noise/spectrumRTN'] = S_RTN
else:
strh5['rystare/sourcefollower/noise/spectrumRTN'][...] = S_RTN
# white and 1/f noise
W_SF = (strh5['rystare/sourcefollower/noise/whitenoisedensity'][()] ** 2) * (1. + \
strh5['rystare/sourcefollower/noise/flickerCornerHz'][()] / f)
if (initialise):
strh5['rystare/sourcefollower/noise/spectrumwhiteflicker'] = W_SF
else:
strh5['rystare/sourcefollower/noise/spectrumwhiteflicker'][...] = W_SF
#total noise
S_SF = W_SF + S_RTN
if (initialise):
strh5['rystare/sourcefollower/noise/spectrumtotal'] = S_SF
else:
strh5['rystare/sourcefollower/noise/spectrumtotal'][...] = S_SF
#Calculating the std of SF noise:
nomin = np.sqrt(strh5['rystare/sourcefollower/freqsamplingdelta'][()] * np.dot(S_SF, H_CDS.T))
denomin = (1 - np.exp(- (strh5['rystare/sourcefollower/CDS/sampletosamplingtime'][()]) / tau_D)).reshape(-1,
1) #\
#the resulting source follower std noise
strh5['rystare/sourcefollower/sigma'][...] = nomin / denomin
#randomising the state of the noise generators
# diagram node 19 source follower noise stored in 'rystare/sourcefollower/source_follower_noise'
# If seed is omitted or None, current system time is used
np.random.seed(strh5['rystare/sourcefollower/noise/seed'][()])
strh5['rystare/sourcefollower/source_follower_noise'][...] =\
strh5['rystare/sourcefollower/sigma'][()] * np.random.randn(strh5['rystare/imageSizePixels'][()][0],
strh5['rystare/imageSizePixels'][()][1])
return strh5
######################################################################################################
[docs]
def multiply_detector_area(strh5):
r"""This routine multiplies detector area
The input to the model of the photosensor is assumed to be a matrix :math:`E_{q}\in R^{N\times M}`
that has been converted to electronrate irradiance, corresponding to electron rate [e/(m2.s)].
The electron rate irriance is converted to electron rate into the pixel by accounting for
detector area:
:math:`\Phi_{q} = \textrm{round} \left( E_{q} \cdot P_A \right),`
where :math:`P_A` is the area of a pixel [m2].
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
# calculate radiant flux [W] from irradiance [W/m^2] and area
strh5['rystare/signal/electronRate'][...] = strh5['rystare/detectorArea'][()] * \
strh5['rystare/signal/electronRateIrradiance'][()]
return strh5
######################################################################################################
[docs]
def multiply_integration_time(strh5):
r"""This routine multiplies with integration time
The input to the model of the photosensor is assumed to be a matrix :math:`E_{q}\in R^{N\times M}`
that has been converted to electrons, corresponding to electron rate [e/s].
The electron rate is converted to electron count into the pixel by accounting for
detector integration time:
:math:`\Phi_{q} = \textrm{round} \left( E_{q} \cdot t_I \right),`
where :math:`t_{I}` is integration (exposure) time.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
#the number of electrons accumulated during the integration time (rounded).
strh5['rystare/signal/lightelectronsnoshotnoise'][...] = np.round(strh5['rystare/signal/electronRate'][()] * \
strh5['rystare/photondetector/integrationtime'][()])
return strh5
######################################################################################################
[docs]
def convert_to_electrons(strh5):
r"""This routine converts photon rate irradiance to electron rate irradiance
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
# Converting the signal from Photons to Electrons
# Quantum Efficiency = Quantum Efficiency Interaction X Quantum Yield Gain.
# diagram node 4 quantum efficiency stored in rystare/quantumEfficiency
strh5['rystare/quantumEfficiency'][...] = strh5['rystare/photondetector/externalquantumeff'][()] * \
strh5['rystare/photondetector/quantumyield'][()]
# number of electrons [e] generated in detector
strh5['rystare/signal/electronRateIrradiance'][...] = strh5['rystare/signal/photonRateIrradianceNU'][()] * \
strh5['rystare/quantumEfficiency'][()]
# diagram node 4 photon rate x mean value of the quantum efficiency stored in rystare/signal/electronRateIrradiance
return strh5
######################################################################################################
[docs]
def shotnoise(sensor_signal_in):
r"""This routine adds photon shot noise to the signal of the photosensor that is in photons.
The photon shot noise is due to the random arrival of photons and can be
described by a Poisson process. Therefore, for each :math:`(i,j)`-th element of
the matrix :math:`\Phi_{q}` that contains the number of collected photons, a photon
shot noise is simulated as a Poisson process :math:`\mathcal{P}` with mean
:math:`\Lambda`:
:math:`\Phi_{ph.shot}=\mathcal{P}(\Lambda), \,\,\,\,\mbox{ where } \Lambda = \Phi_{q}.`
We use the `ryutils.poissonarray` function that generates Poisson random numbers
with mean :math:`\Lambda`. That is, the number of collected photons in
:math:`(i,j)`-th pixel of the simulated photosensor in the matrix :math:`\Phi_{q}` is
used as the mean :math:`\Lambda` for the generation of Poisson random numbers to
simulate the photon shot noise. The input of the `ryutils.poissonarray` function will
be the matrix :math:`\Phi_{q}` that contains the number of collected photons. The
output will be the matrix :math:`\Phi_{ph.shot} \rightarrow \Phi_{q}`, i.e., the signal
with added photon shot noise. The matrix :math:`\Phi_{ph.shot}` is recalculated
each time the simulations are started, which corresponds to the temporal nature
of the photon shot noise.
Args:
| sensor_signal_in (np.array[N,M]): photon irradiance in, in photons
Returns:
| sensor_signal_out (np.array[N,M]): photon signal out, in photons
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
# Since this is a shot noise, it must be random every time we apply it,
# unlike the Fixed Pattern Noise (PRNU or DSNU).
# Inline Poisson sampling — avoids dependency on ryutils.poissonarray (not present in all installed versions).
# For large means (>1000) the normal approximation is used to avoid Poisson overflow.
# seedval=0 matches the original call signature.
np.random.seed(0)
inp = sensor_signal_in
_tpoint = 1000
_sdelta = 1e-10
outp = (inp <= _tpoint) * np.random.poisson(inp * (inp <= _tpoint)) \
+ ((inp > _tpoint) & (inp != 0)) * np.random.normal(loc=inp, scale=np.sqrt(inp + _sdelta))
return np.where(inp == 0, 0.0, outp)
######################################################################################################
[docs]
def responsivity_FPN_light(strh5):
r"""Multiploying the photon signal with the PRNU.
The Photo Response Non-Uniformity (PRNU) is the spatial variation in pixel
conversion gain (from photons to electrons). When viewing a uniform scene the pixel signals will
differ because of the PRNU, mainly due to variations in the individual pixel's characteristics
such as detector area and spectral response. These variations occur during the manufacture
of the substrate and the detector device.
The PRNU is signal-dependent (proportional to the input signal) and is
fixed-pattern (time-invariant). For visual (silicon) sensors the PRNU factor is typically
:math:`0.01\dots 0.05`\ , but for HgCdTe sensors it can be as large as :math:`0.02\dots 0.25`\ .
It varies from sensor to sensor, even within the same manufacturing batch.
The photo response non-uniformity (PRNU) is considered as a temporally-fixed light
signal non-uniformity. The PRNU is modelled using a Gaussian distribution for each
:math:`(i,j)`-th pixel of the matrix :math:`I_{e^-}`,
as :math:`I_{PRNU.e^-}=I_{e^-}(1+\mathcal{N}(0,\sigma_{PRNU}^2))`
where :math:`\sigma_{PRNU}` is the PRNU factor value.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
#the random generator seed is fixed with value from input
np.random.seed(strh5['rystare/photondetector/lightPRNU/seed'][()])
#matrix for the PRNU
normalisedVariation = FPN_models(
strh5['rystare/imageSizePixels'][()][0], strh5['rystare/imageSizePixels'][()][1],
'pixel', strh5['rystare/photondetector/lightPRNU/model'][(
)], strh5['rystare/photondetector/lightPRNU/sigma'][()])
# diagram node 2 NU stored in 'rystare/photondetector/lightPRNU/value'
#np.random.randn has mean=0, variance = 1, so we multiply with variance and add to mean
strh5['rystare/photondetector/lightPRNU/value'][...] = (1 + normalisedVariation)
# diagram node 3 photon rate multiplied with PRNU stored in 'rystare/signal/photonRateIrradianceNU'
#apply the PRNU noise to the light signal of the photosensor.
strh5['rystare/signal/photonRateIrradianceNU'][...] = strh5['rystare/signal/photonRateIrradiance'][()] * \
strh5['rystare/photondetector/lightPRNU/value'][()]
return strh5
######################################################################################################
[docs]
def responsivity_FPN_dark(strh5):
r"""Add dark current noises that consist of Dark FPN and Dark shot noise.
Pixels in a hardware photosensor cannot be manufactured exactly the same from
perfectly pure materials. There will always be variations in the photo detector
area that are spatially uncorrelated, surface defects at
the :math:`SiO_2/Si` interface (see Sakaguchi paper on dark current reduction),
and discrete randomly-distributed charge generation centres. These
defects provide a mechanism for thermally-excited carriers to move between the
valence and conduction bands. Consequently, the average dark signal is not
uniform but has a spatially-random and fixed-pattern noise (FPN) structure. The
dark current FPN can be expressed as follows:
:math:`\sigma_{d.FPN} = t_I D_R \cdot D_N,`
where :math:`t_I` is the integration time, :math:`D_R` is the average dark current,
and :math:`D_N` is the dark current FPN factor that is typically :math:`0.1\dots 0.4` for CCD and CMOS sensors.
There are also so called 'outliers' or 'dark spikes'; that is, some pixels generate a dark
signal values much higher than the mean value of the dark signal. The mechanism
of such 'dark spikes' or 'outliers' can be described by the Poole-Frenkel
effect (increase in emission rate from a defect in the presence of an electric field).
*Simulation of dark current fixed pattern noise*
The dark current Fixed
Pattern Noise (FPN) is simulated using non-symmetric distributions to account
for the 'outliers' or 'hot pixels'. It is usually assumed that the dark
current FPN can be described by Gaussian distribution. However, such an
assumption provides a poor approximation of a complicated noise picture.
Studies show that a
more adequate model of dark current FPN is to use non-symmetric probability
distributions. The concept is to use two distributions to describe very
'leaky' pixels that exhibit higher noise level than others. The first
distribution is used for the main body of the dark current FPN, with a uniform
distribution superimposed to model 'leaky' pixels. For simulations at
room-temperature (:math:`25^\circ` C) authors use a
logistic distribution, where a higher proportion of the population is
distributed in the tails. For higher
temperatures, inverse Gaussian and
Log-Normal distributions have been proposed. The Log-Normal distribution works well for
conventional 3T APS CMOS sensors with comparatively high dark current.
In our simulations we use the Log-Normal distribution for the simulation of dark
current FPN in the case of short integration times, and superimposing other
distributions for long integration times. The actual simulation code implements
various models, including Log-Normal, Gaussian, and Wald distribution to emulate
the dark current FPN noise for short- and long-term integration times.
The dark current FPN for each pixel of the matrix :math:`I_{dc.shot.e^-}` is computed as:
:math:`I_{dc.FPN.e^-} = I_{dc.shot.e^-} + I_{dc.shot.e^-} \cdot ln\mathcal{N}(0,\sigma_{dc.FPN.e^-}^2)`
where :math:`\sigma_{dc.FPN.e^-} = t_I D_R D_N`, :math:`D_R` is the average dark current,
and :math:`D_N` is the dark current FPN factor.
Since the dark current FPN does not change from one frame to the next, the
matrix :math:`ln \mathcal{N}` is calculated once and then can be re-used similar to
the PRNU simulations.
The experimental results confirm
that non-symmetric models, and in particular the Log-Normal distribution,
adequately describe the dark current FPN in CMOS sensors, especially in the
case of a long integration time (longer than 30-60 seconds). For long-exposure case, one
needs to superimpose two (or more, depending on the sensor) probability
distributions.
Args:
| strh5 (hdf5 file): hdf5 file that defines all simulation parameters
Returns:
| in strh5: (hdf5 file) updated data fields
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
#get the initial deviation from the mean
#handle gauss and nongaussian different
if strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/model'][()] in ['Janesick-Gaussian', 'AR-ElGamal']:
if 'rystare/photondetector/darkcurrent/fixedPatternNoise/filter_params' in strh5:
filter_params = strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/filter_params'][()]
else:
filter_params = None
darksignalnoisematrix = FPN_models(
strh5['rystare/imageSizePixels'][()][0], strh5['rystare/imageSizePixels'][()][1],
'pixel', strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/model'][()],
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/sigma'][()],
filter_params=filter_params)
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/value'][...] = (1 + darksignalnoisematrix)
# gaussian noise values may be negative, here we limit them if desired
# only 'Janesick-Gaussian' was tested, 'AR-ElGamal' limitnegative not yet tested,
# so negative-going values are allowed.
if strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/model'][()] in ['Janesick-Gaussian']:
if strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/limitnegative'][()]:
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/value'][...] = limitzero(
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/value'][()], thr=0.6)
#this would be Wald and lognormal
else:
darksignalnoisematrix = FPN_models(
strh5['rystare/imageSizePixels'][()][0], strh5['rystare/imageSizePixels'][()][1],
'pixel', strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/model'][()],
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/sigma'][()])
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/value'][...] = (1 + darksignalnoisematrix)
# diagram node 10 dark current nonuniformity stored in 'rystare/photondetector/darkcurrent/fixedPatternNoise/value'
#apply the darkFPN noise to the dark_signal.
# diagram node 11 dark current electrons stored in 'rystare/signal/darkcurrentelectrons'
strh5['rystare/signal/darkcurrentelectrons'][...] = strh5['rystare/signal/darkcurrentelectrons'][()] * \
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/value'][()]
return strh5
######################################################################################################
[docs]
def FPN_models(sensor_signal_rows, sensor_signal_columns, noisetype, noisedistribution,
spread, filter_params=None):
r"""The routine contains various models on simulation of Fixed Pattern Noise.
There are many models for simulation of the FPN: some of the models are suitable
for short-exposure time modelling (Gaussian), while other models are more suitable
for log-exposure modelling of dark current FPN.
*Gaussian model (Janesick-Gaussian)*
Fixed-pattern noise (FPN) arises from changes in dark currents due to variations
in pixel geometry during fabrication of the sensor. FPN increases exponentially
with temperature and can be measured in dark conditions.
Column FPN is caused by offset in the integrating amplifier,
size variations in the integrating capacitor CF, channel charge injection from
reset circuit. FPN components that are reduced by CDS.
Dark current FPN can be expressed as:
:math:`\sigma_{D_{FPN}} = D\cdot D_N,`
where :math:`D_N` is the dark current FPN quality, which is typically between 10\%
and 40\% for CCD and CMOS sensors (see Janesick's book), and :math:`D = t_I D_R`.
There are other models of dark FPN, for instance as a autoregressive process.
*El Gamal model of FPN with Autoregressive process*
To capture the structure of FPN in a CMOS sensor we express :math:`F_{i,j}` as the
sum of a column FPN component :math:`Y_j` and a pixel FPN component :math:`X_{i,j}`.
Thus, :math:`F_{i,j} = Y_j + X_{i,j},` where the :math:`Y_j`'s and the
:math:`X_{i,j}`'s are zero mean random variables.
The first assumption is that the random processes
:math:`Y_{j}` and :math:`X_{i,j}` are uncorrelated. This assumption is reasonable
since the column and pixel FPN are caused by different device parameter
variations. We further assume that the column (and pixel) FPN processes are
isotropic.
The idea to use autoregressive processes to model FPN was proposed because their
parameters can be easily and efficiently estimated from data.
The simplest model, namely first order isotropic
autoregressive processes is considered. This model can be extended to higher order
models, however, the results suggest that additional model complexity may not be
warranted.
The model assumes that the column FPN process :math:`Y_{j}` is a first order
isotropic autoregressive process of the form:
:math:`Y_j = a(Y_{j-1}+Y_{j+1}) + U_j`
where the :math:`U_j` s are zero mean, uncorrelated random variables with the same
variance :math:`\sigma_U` , and :math:`0 \leq a \leq 1` is a parameter that
characterises the dependency of :math:`Y_{j}` on its two neighbours.
The model assumes that the pixel FPN process :math:`X_{i,j}` is a two dimensional
first order isotropic autoregressive process of the form:
:math:`X_{i,j} = b(X_{i-1,j} + X_{i+1,j} + X_{i,j-1} + X_{i,j+1} ) + V_{i,j}`
where the :math:`V_{i,j}` s are zero mean uncorrelated random variables with the
same variance :math:`\sigma_V` , and :math:`0 \leq b \leq 1` is a parameter that
characterises the dependency of :math:`X_{i,j}` on its four neighbours.
Args:
| sensor_signal_rows(int): number of rows in the signal matrix
| sensor_signal_columns(int): number of columns in the signal matrix
| noisetype(string): type of noise to generate: ['pixel' or 'column']
| noisedistribution(string): the probability distribution name
| ['AR-ElGamal', 'Janesick-Gaussian', 'Wald', 'LogNormal']
| spread(float): spread around mean value (sigma/chi/lambda) for the probability distribution
| filter_params([nd.array]): a vector of parameters for the probability filter
Returns:
| noiseout (np.array[N,M]): generated noise of FPN.
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
if type(noisetype) is not str:
noisetype = noisetype.decode('UTF-8')
if type(noisedistribution) is not str:
noisedistribution = noisedistribution.decode('UTF-8')
if noisedistribution in ['AR-ElGamal']: # AR-ElGamal FPN model
if not filter_params:
print('When using AR-ElGamal the filter parameters must be defined')
return None
if noisetype in ['pixel']:
# uniformly distributed White Gaussian Noise.
x2 = np.random.randn(sensor_signal_rows, sensor_signal_columns)
# Matlab's filter operates on the first dimension of the array, while
# scipy.signal.lfilter by default operates on the last dimension.
# http://stackoverflow.com/questions/16936558/matlab-filter-not-compatible-with-python-lfilter
# here y is observed (filtered) signal. Any WSS process y[n] can be of
noiseout = signal.lfilter(1, filter_params, x2, axis=0)
if noisetype in ['column']:
x = signal.lfilter(1, filter_params, np.random.randn(1, sensor_signal_columns)) # AR(1) model
# numpy equivalent of matlab repmat(a, m, n) is tile(a, (m, n))
# http://stackoverflow.com/questions/1721802/what-is-the-equivalent-of-matlabs-repmat-in-numpy
# noiseout = repmat_(x,sensor_signal_rows,1) # making PRNU as a ROW-repeated noise, just like light FPN
noiseout = np.tile(x, (sensor_signal_rows, 1)) # making PRNU as a ROW-repeated noise, just like light FPN
elif noisedistribution in ['Janesick-Gaussian']:
#Janesick-Gaussian FPN model np.random.randn has mean=0, variance = 1
#multiply with spread to get stddev equal to spread
if spread is None:
print('When using Janesick-Gaussian the spread must be defined')
return None
if noisetype in ['pixel']:
# here y is observed (filtered) signal. Any WSS process y[n] can be of
noiseout = np.random.randn(sensor_signal_rows,sensor_signal_columns)
if noisetype in ['column']:
x = np.random.randn(1,sensor_signal_columns) # dark FPN [e] <------ Konnik
noiseout = np.tile(x, (sensor_signal_rows, 1)) # making PRNU as a ROW-repeated noise, just like light FPN
if noisetype in ['row']:
x = np.random.randn(sensor_signal_rows,1) #dark FPN [e] <------ Konnik
noiseout = np.tile(x, (1, sensor_signal_columns)) #making PRNU as a ROW-repeated noise, just like light FPN
noiseout = noiseout * spread
elif noisedistribution in ['Wald']: # Wald FPN model
if spread is None:
print('When using Wald the spread must be defined')
return None
if noisetype in ['pixel']:
noiseout = ryprob.distributions_generator('wald',spread,[sensor_signal_rows,
sensor_signal_columns]) + np.random.randn(sensor_signal_rows,sensor_signal_columns)
if noisetype in ['column']:
x = ryprob.distributions_generator('wald',spread,[1,sensor_signal_columns]) + np.random.randn(1,
sensor_signal_columns)
noiseout = np.tile(x, (sensor_signal_rows, 1)) #making PRNU as a ROW-repeated noise, just like light FPN
elif noisedistribution in ['LogNormal']:
if spread is None:
print('When using LogNormal the spread must be defined')
return None
if noisetype in ['pixel']:
noiseout = ryprob.distributions_generator('lognorm',[0.0,spread],[sensor_signal_rows,sensor_signal_columns])
if noisetype in ['column']:
x = ryprob.distributions_generator('lognorm',[0.0,spread],[1,sensor_signal_columns])
noiseout = np.tile(x, (sensor_signal_rows, 1))
return noiseout
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
######################################################################################
"""
The following functions are not yet integrated into the the rystare model,
maybe one day.....
"""
################################################################
##
# to calculate the scene electron count from the low light table
[docs]
def nEcntLLightDF(tauAtmo, tauFilt, tauOpt, quantEff, rhoTarg, cosTarg,
inttime, pfrac, detarea, fno, scenario, specBand, dfPhotRates):
""" Calculate the number of electrons in a detector given sensor parameters
and photon radiance dataframe
All values in base SI units
Args:
| tauAtmo (scalar or nd.array): atmosphere transmittance
| tauFilt (scalar or nd.array): sensor filter transmittance
| tauOpt (scalar or nd.array): sensor optics transmittance
| quantEff (scalar or nd.array): sensor detector quantum efficiency
| rhoTarg (scalar or nd.array): target diffuse reflectance
| cosTarg (scalar): cos of illuminator angle wrt normal vector
| inttime (scalar): integration time s
| pfrac (scalar): fraction of clear optics
| detarea (scalar): detector area m2
| fno (scalar): f number
| scenario (str): Scenario as key to rypflux.py dataframe
| specBand (str): Spectral band as key to rypflux.py dataframe
| dfPhotRadiance (pd.DataFrame): rypflux.py dataframe radiance in q/(s.m2.sr)
Returns:
| n (float): number of electrons in charge well
Raises:
| No exception is raised.
Author: CJ Willers
"""
L = quantEff * tauOpt * tauFilt * tauAtmo * \
rhoTarg * dfPhotRates.loc[scenario][specBand]
n = np.pi * inttime * pfrac * detarea * L * cosTarg / (4 * fno**2)
return n
################################################################
##
# to calculate the scene electron count from the low light table
[docs]
def nEcntLLightPhotL(tauAtmo, tauFilt, tauOpt, quantEff, rhoTarg, cosTarg,
inttime, pfrac, detarea, fno, photRadiance):
""" Calculate the number of electrons in a detector given sensor parameters
and photon radiance
All values in base SI units
Args:
| tauAtmo (scalar or nd.array): atmosphere transmittance
| tauFilt (scalar or nd.array): sensor filter transmittance
| tauOpt (scalar or nd.array): sensor optics transmittance
| quantEff (scalar or nd.array): sensor detector quantum efficiency
| rhoTarg (scalar or nd.array): target diffuse reflectance
| cosTarg (scalar): cos of illuminator angle wrt normal vector
| inttime (scalar): integration time s
| pfrac (scalar): fraction of clear optics
| detarea (scalar): detector area m2
| fno (scalar): f number
| photRadiance (scalar): in-band photon radiance q/(s.m2.sr)
Returns:
| n (float): number of electrons in charge well
Raises:
| No exception is raised.
Author: CJ Willers
"""
L = quantEff * tauOpt * tauFilt * tauAtmo * rhoTarg * photRadiance
n = np.pi * inttime * pfrac * detarea * L * cosTarg / (4 * fno**2)
return n
################################################################
##
# to calculate the electron count in the detector from a thermal source only
[docs]
def nElecCntThermalScene(wl, tmptr, emis, tauAtmo, tauFilt, tauOpt, quantEff, inttime, pfrac, detarea, fno):
""" Calculate the number of electrons in a detector from a thermal source
All values in base SI units except ``wl`` — see Note.
Args:
| wl (np.array): wavelength vector **in micrometres (µm)**
| tmptr (scalar): source temperature in K
| emis (np.array of scalar): source emissivity
| tauAtmo (scalar or nd.array): atmosphere transmittance
| tauFilt (scalar or nd.array): sensor filter transmittance
| tauOpt (scalar or nd.array): sensor optics transmittance
| quantEff (scalar or nd.array): sensor detector quantum efficiency
| inttime (scalar): integration time s
| pfrac (scalar): fraction of clear optics
| detarea (scalar): detector area m2
| fno (scalar): f number
Returns:
| n (float): number of electrons in charge well
Raises:
| No exception is raised.
Note:
Pre-existing units inconsistency: the docstring header says "SI units"
but ``wl`` must be in **micrometres** because the implementation passes
it directly to ``ryplanck.planck(..., type='ql')`` (i.e. ``planckql``),
which expects µm. Passing SI metres gives Planck values of zero due to
exponent overflow. Behaviour is preserved unchanged from the original.
Author: CJ Willers
"""
L = emis * tauAtmo * tauFilt * tauOpt * quantEff * \
_ryplanckql(wl, tmptr)/np.pi
L = np.trapezoid( L, x=wl,axis=0)
n = np.pi * inttime * pfrac * detarea * L / (4 * fno**2)
return n
################################################################
##
# to calculate the electron count in the detector from a thermal source only
[docs]
def nEcntThermalOptics(wl, tmptrOpt, tauFilt, tauOpt, quantEff, inttime, pfrac, detarea, fno):
""" Calculate the number of electrons in a detector from hot optics
All values in base SI units except ``wl`` — see Note.
Args:
| wl (np.array): wavelength vector **in micrometres (µm)**
| tmptrOpt (scalar): optics temperature in K
| tauFilt (scalar or nd.array): sensor filter transmittance
| tauOpt (scalar or nd.array): sensor optics transmittance
| quantEff (scalar or nd.array): sensor detector quantum efficiency
| inttime (scalar): integration time s
| pfrac (scalar): fraction of clear optics
| detarea (scalar): detector area m2
| fno (scalar): f number
Returns:
| n (float): number of electrons in charge well
Raises:
| No exception is raised.
Note:
Pre-existing units inconsistency: ``wl`` must be in **micrometres**
because the implementation calls ``ryplanck.planck(..., type='ql')``
(``planckql``), which expects µm. Behaviour is preserved unchanged.
Author: CJ Willers
"""
L = tauFilt * (1.0 - tauOpt) * quantEff * \
_ryplanckql(wl, tmptrOpt)/np.pi
L = np.trapezoid( L, x=wl,axis=0)
n = np.pi * inttime * pfrac * detarea * L / (4 * fno**2)
return n
############################################################
##
[docs]
def nElecCntReflSun(wl, tauSun, tauAtmo=1, tauFilt=1, tauOpt=1, quantEff=1,
rhoTarg=1, cosTarg=1, inttime=1, pfrac=1, detarea=1, fno=0.8862269255, emissun=1.0, tmprt=6000.):
""" Calculate the number of electrons in a detector or photon radiance for reflected sunlight
All values in base SI units.
By using the default values when calling the function the radiance at the
source can be calculated.
Args:
| wl (np.array (N,) or (N,1)): wavelength **in micrometres (µm)**
| tauSun (np.array (N,) or (N,1)): transmittance between the scene and sun
| tauAtmo (np.array (N,) or (N,1)): transmittance between the scene and sensor
| tauFilt (np.array (N,) or (N,1)): sensor filter transmittance
| tauOpt (np.array (N,) or (N,1)): sensor optics transmittance
| quantEff (np.array (N,) or (N,1)): detector quantum efficiency
| rhoTarg (np.array (N,) or (N,1)): target diffuse surface reflectance
| cosTarg (scalar): cosine between surface normal and sun/moon direction
| inttime (scalar): detector integration time
| pfrac (scalar): fraction of optics clear aperture
| detarea (scalar): detector area
| fno (scalar): optics fnumber
| emissun (scalar): sun surface emissivity
| tmprt (scalar): sun surface temperature
Returns:
| n (scalar): number of electrons accumulated during integration time
Raises:
| No exception is raised.
Note:
Pre-existing units inconsistency: ``wl`` must be in **micrometres**
because the implementation calls ``ryplanck.planck(..., type='ql')``
(``planckql``), which expects µm. Behaviour is preserved unchanged.
"""
L = emissun * tauAtmo * tauFilt * tauOpt * tauSun * quantEff * \
rhoTarg * _ryplanckql(wl, tmprt)/np.pi
L = np.trapezoid( L, x=wl,axis=0)
n = np.pi * inttime * pfrac * detarea * L * 2.17e-5 * cosTarg / (4 * fno**2)
return n
############################################################
##
# calculate the dark current noise
[docs]
def darkcurrentnoise(inttime, detarea,temptr, Egap, DFM=0.5e-5):
"""Calculate the dark current noise given detector parameters
Args:
| inttime (scalar): integration time in seconds
| detarea (scalar): detector area in m2
| temptr (scalar): temperature in K
| Egap (scalar): bandgap in eV
| DFM (scalar): in units of nA/m2
Returns:
| n (scalar): dark current noise as number of electrons
Raises:
| No exception is raised.
"""
keV = const.physical_constants['Boltzmann constant in eV/K'][0]
ndarkcur = inttime * 2.55e15 * detarea * DFM * (temptr ** 1.5) * np.exp(-Egap/(2 * keV * temptr) )
return np.sqrt(ndarkcur)
############################################################
##
[docs]
def kTCnoiseCsn(temptr, sensecapacity):
"""Calculate kTC reset noise in electrons given sense-node capacitance.
Computes ``sqrt(k * T * Csn) / e`` — the thermal charge fluctuation on a
capacitor of value ``sensecapacity`` at temperature ``temptr``.
Args:
| temptr (scalar): temperature in K
| sensecapacity (scalar): sense node capacitance in Farads
Returns:
| n (scalar): kTC noise as number of electrons (rms)
Raises:
| No exception is raised.
"""
return np.sqrt(const.k * temptr * sensecapacity) / const.e
############################################################
##
[docs]
def kTCnoiseGv(temptr, gv):
"""Calculate kTC reset noise in electrons given sense-node gain.
Computes ``sqrt(k * T / (e * gv))`` — equivalent to the kTC noise expressed
through the sense-node conversion gain ``gv`` (V/e) rather than capacitance.
Args:
| temptr (scalar): temperature in K
| gv (scalar): sense node gain in V/e
Returns:
| n (scalar): kTC noise as number of electrons (rms)
Raises:
| No exception is raised.
"""
return np.sqrt(const.k * temptr / (const.e * gv))
############################################################
##
#def
"""
Args:
| ():
| ():
| ():
| ():
| ():
| ():
| ():
Returns:
| n (scalar): noise as number of electrons
Raises:
| No exception is raised.
"""
######################################################################################
[docs]
def define_metrics():
r"""This simple routine defines various handy shorthand for cm and mm in the code.
The code defines a number of scaling factors to convert to metres and radians
Args:
| None
Returns:
| scaling factors.
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
m = 1
cm = 0.01 * m
mm = 0.001 * m
mum = 1e-06 * m
nm = 1e-09 * m
rad = 1
mrad = 0.001 * rad
return m,cm,mm,mum,nm,rad,mrad
######################################################################################
[docs]
def limitzero(a, thr=0.6):
r"""Performs an asymetric clipping to prevent negative values.
The lower-end values are clumped up towards the lower positive values, while
upper-end values are not affected.
This function is used to prevent negative random variables for wide sigma and low
mean value, e.g., N(1,.5). If the random variables are passed through this function
The resulting distribution is not normal any more, and has no known analytical form.
A threshold value of around 0.6 was found to work well for N(1,small) up to N(1,.5).
Before you use this function, first check the results using the code below in the main
body of this file.
Args:
| a (np.array): an array of floats,
Returns:
| scaling factors.
Raises:
| No exception is raised.
Author: CJ Willers
"""
ashape = a.shape
a = a.flatten()
a = np.where(a<thr, thr * np.exp(( a - thr) / (1 * thr)), 0) + np.where(a >= thr, a, 0)
return a.reshape(ashape)
################################################################
[docs]
def run_example(doTest='Advanced', outfilename='Output', pathtoimage=None,
doPlots=False, doHisto=False, doImages=False):
"""This code provides examples of use of the pyradi.rystare model for
a CMOS/CCD photosensor.
Two models are provided 'simple' and 'advanced'
doTest can be 'Simple' or 'Advanced'
Args:
| doTest (string): which example to run 'Simple', or 'Advanced'
| outfilename (string): filename for output files
| pathtoimage (string): fully qualified path to where the image is located
| doPlots (boolean): flag to control the creation of false colour image plots with colour bars
| doHisto (boolean): flag to control the creation of image histogram plots
| doImages (boolean): flag to control the creation of monochrome image plots
Returns:
| hdffilename (string): output HDF filename
Raises:
| No exception is raised.
Author: Mikhail V. Konnik, revised/ported by CJ Willers
Original source: http://arxiv.org/pdf/1412.4031.pdf
"""
import os.path
from matplotlib import cm as mcm
import matplotlib.mlab as mlab
import pyradi.ryfiles as ryfiles
import pyradi.ryhdf5 as ryhdf5
import pyradi.ryutils as ryutils
if type(doTest) is not str:
doTest = doTest.decode('UTF-8')
if type(outfilename) is not str:
outfilename = outfilename.decode('UTF-8')
if type(pathtoimage) is not str:
pathtoimage = pathtoimage.decode('UTF-8')
if doTest in ['Simple']:
prefix = 'PS'
elif doTest in ['Advanced']:
prefix = 'PA'
else:
exit('Undefined test')
[m, cm, mm, mum, nm, rad, mrad] = define_metrics()
#open the file to create data structure and store the results, remove if exists
hdffilename = f'{prefix}{outfilename}.hdf5'
if os.path.isfile(hdffilename):
os.remove(hdffilename)
strh5 = ryhdf5.open_HDF(hdffilename,'w')
# Optics parameters to convert from radiance to irradiance in the image plane
strh5['rystare/fnumber'] = 3.2
# solid angle = pi*sin^2(theta), sin(theta) = 1 / (2*fno)
# solid angle optics from detector
strh5['rystare/fnumberConeSr'] = np.pi / ((2. * strh5['rystare/fnumber'][()])**2.)
strh5['rystare/pixelPitch'] = [5e-6,5e-6]
# Light Noise parameters
strh5['rystare/photonshotnoise/activate'] = True #photon shot noise.
#sensor parameters
strh5['rystare/sensortype'] = 'CCD' # CCD / CMOS must be in capitals
strh5['rystare/photondetector/operatingtemperature'] = 300. # operating temperature, [K]
# full-frame CCD sensors has 100% fil factor (Janesick: 'Scientific Charge-Coupled Devices')
senstype = strh5['rystare/sensortype'][()]
if type(senstype) is not str:
senstype = senstype.decode('UTF-8')
if senstype in ['CMOS']:
strh5['rystare/photondetector/geometry/fillfactor'] = 0.5 # Pixel Fill Factor for CMOS photo sensors.
else:
strh5['rystare/photondetector/geometry/fillfactor'] = 0.95 # Pixel Fill Factor for full-frame CCD photo sensors.
strh5['rystare/photondetector/integrationtime'] = 0.035 # Exposure/Integration time, [sec].
strh5['rystare/photondetector/externalquantumeff'] = 0.8 # external quantum efficiency, fraction not reflected.
strh5['rystare/photondetector/quantumyield'] = 1. # number of electrons absorbed per one photon into material bulk
# photo response non-uniformity noise (PRNU), or also called light Fixed Pattern Noise (light FPN)
strh5['rystare/photondetector/lightPRNU/activate'] = True
strh5['rystare/photondetector/lightPRNU/seed'] = 362436069
strh5['rystare/photondetector/lightPRNU/model'] = 'Janesick-Gaussian'
strh5['rystare/photondetector/lightPRNU/sigma'] = 0.01 # sigma [about 1\% for CCD and up to 5% for CMOS]
# detector material bandgap properties
strh5['rystare/photondetector/varshni/Egap0'] = 1.166 #bandgap energy for 0 degrees of K. [For Silicon, eV]
strh5['rystare/photondetector/varshni/varA'] = 5.5e-04 #Si material parameter, [eV/K].
strh5['rystare/photondetector/varshni/varB'] = 636. #Si material parameter, [K].
# Dark Current Noise parameters
strh5['rystare/photondetector/darkcurrent/activate'] = True
strh5['rystare/photondetector/darkcurrent/densityAm2'] = 1. * 1e-9 * 1e4 # dark current density [A/m2].
strh5['rystare/photondetector/darkcurrent/ca'] = 4.31e5 # for density in m2
strh5['rystare/photondetector/darkcurrent/ed'] = 2.
# dark current shot noise
strh5['rystare/photondetector/darkcurrent/shotnoise/activate'] = True
strh5['rystare/photondetector/darkcurrent/shotnoise/seed'] = 6214069
strh5['rystare/photondetector/darkcurrent/shotnoise/model'] = 'Gaussian'
#dark current Fixed Pattern Noise
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/activate'] = True
# Janesick's book: dark current FPN quality factor is typically between 10\% and 40\% for CCD and CMOS sensors
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/seed'] = 362436128
if doTest in ['Simple']:
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/model'] = 'Janesick-Gaussian'
#0.3-0.4 sigma for dark current signal (Janesick's book)
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/sigma'] = 0.3
elif doTest in ['Advanced']:
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/model'] = 'LogNormal' #suitable for long exposures
strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/sigma'] = 0.4 # lognorm_sigma.
else:
pass
# #alternative model
# strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/model'] = 'Wald'
# strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/sigma'] = 2.0
# # small parameters (w<1) → extremely narrow distribution; large (w>10) → large tail.
# #alternative model
# strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/model'] = 'AR-ElGamal'
# strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/filter_params'] = [1., 0.5]
# # see matlab filter or scipy lfilter functions for details
#sense node charge to voltage
strh5['rystare/sensenode/gain'] = 5e-6 # Sense node gain, A_SN [V/e]
strh5['rystare/sensenode/vrefreset'] = 3.1 # Reference voltage to reset the sense node. [V] typically 3-10 V.
strh5['rystare/sensenode/vsnmin'] = 0.5 # Minimum voltage on sense node, max well charge [V] typically < 1 V.
strh5['rystare/sensenode/gainresponse/type'] = 'linear'
strh5['rystare/sensenode/gainresponse/k1'] = 1.090900000e-14 # nonlinear capacitance is given by C = k1/V
if strh5['rystare/sensenode/gainresponse/type'] in ['nonlinear']:
strh5['rystare/sensenode/fullwellelectronselection/fullwellelectrons'] = \
-(strh5['rystare/sensenode/gainresponse/k1'][()]/const.e) * \
np.log(strh5['rystare/sensenode/vsnmin'][()]/strh5['rystare/sensenode/vrefreset'][()])
else:
# full well of the pixel (how many electrons can be stored in one pixel), [e]
strh5['rystare/sensenode/fullwellelectronselection/fullwellelectrons'] = 2e4
strh5['rystare/sensenode/resetnoise/activate'] = True
strh5['rystare/sensenode/resetnoise/factor'] = 0.8 # the compensation factor of the Sense Node Reset Noise:
# 1 - no compensation from CDS for Sense node reset noise.
# 0 - fully compensated SN reset noise by CDS.
strh5['rystare/sensenode/resetnoise/seed'] = 2154069
strh5['rystare/sensenode/resetnoise/model'] = 'Gaussian'
#source follower
strh5['rystare/sourcefollower/gain'] = 1. # Source follower gain, [V/V], lower means amplify the noise.
#source follower
strh5['rystare/sourcefollower/nonlinearity/activate'] = True # VV non-linearity
strh5['rystare/sourcefollower/nonlinearity/ratio'] = 1.05 # > 1 for lower signal, < 1 for higher signal
# flicker noise corner frequency $f_c$ [Hz]: where white and flicker noise power spectra are equal
strh5['rystare/sourcefollower/noise/flickerCornerHz'] = 1e6
# thermal white noise [V/Hz^(1/2), typically 15 nV/Hz^(1/2)]
strh5['rystare/sourcefollower/noise/whitenoisedensity'] = 15e-9
# [A] source follower current modulation induced by RTS [CMOS ONLY]
strh5['rystare/sourcefollower/noise/deltaindmodulation'] = 1e-8
strh5['rystare/sourcefollower/dataclockspeed'] = 20e6 # MHz data rate clocking speed
# sampling spacing for the frequencies (e.g., sample every 10kHz)
strh5['rystare/sourcefollower/freqsamplingdelta'] = 10000.
strh5['rystare/sourcefollower/noise/seed'] = 2154069
if doTest in ['Simple']:
strh5['rystare/sourcefollower/noise/activate'] = False
elif doTest in ['Advanced']:
strh5['rystare/sourcefollower/noise/activate'] = True
#dark current Offset Fixed Pattern Noise
strh5['rystare/sourcefollower/fpoffset/activate'] = True
strh5['rystare/sourcefollower/fpoffset/model'] = 'Janesick-Gaussian'
strh5['rystare/sourcefollower/fpoffset/sigma'] = 0.0005 # percentage of (V_REF - V_SN)
strh5['rystare/sourcefollower/fpoffset/seed'] = 362436042
# Correlated Double Sampling (CDS)
if doTest in ['Simple']:
strh5['rystare/sourcefollower/CDS/sampletosamplingtime'] = 0 #not used
elif doTest in ['Advanced']:
strh5['rystare/sourcefollower/CDS/sampletosamplingtime'] = 1e-6 #CDS sample-to-sampling time [sec].
else:
pass
strh5['rystare/sourcefollower/CDS/gain'] = 1. # CDS gain, [V/V], lower means amplify the noise.
# Analogue-to-Digital Converter (ADC)
strh5['rystare/ADC/num-bits'] = 12. # noise is more apparent on high Bits
strh5['rystare/ADC/offset'] = 0. # Offset of the ADC, in DN
strh5['rystare/ADC/nonlinearity/activate'] = False
strh5['rystare/ADC/nonlinearity/ratio'] = 1.1
#Sensor noises and signal visualisation
strh5['rystare/flag/plots/doPlots'] = False
strh5['rystare/flag/plots/plotLogs'] = False
#For testing and measurements only:
strh5['rystare/darkframe'] = False # True if no signal, only dark
#=============================================================================
if strh5['rystare/darkframe'][()]: # we have zero light illumination
imagehdffilename = 'data/image-Zero-256-256.hdf5'
else: # load an image, nonzero illumination
imagehdffilename = 'data/image-Disk-256-256.hdf5'
# imagehdffilename = 'data/image-Uniform-256-256.hdf5'
# low light case
# imagehdffilename = 'data/image-Stairslin-LowLight-40-100-520.hdf5'
scaleInput = 1
if pathtoimage is None:
pathtoimage = os.path.join(os.path.dirname(ryprob.__file__), imagehdffilename)
imghd5 = ryhdf5.open_HDF(pathtoimage)
#images must be in photon rate irradiance units q/(s.m2)
strh5['rystare/equivalentSignal'] = scaleInput * imghd5['image/equivalentSignal'][()]
strh5['rystare/signal/photonRateRadianceNoNoise'] = scaleInput * imghd5['image/PhotonRateRadianceNoNoise'][()]
strh5['rystare/signal/photonRateRadiance'] = scaleInput * imghd5['image/PhotonRateRadiance'][()]
strh5['rystare/signal/photonRateIrradianceNoNoise'] = scaleInput * strh5['rystare/fnumberConeSr'][()] * \
imghd5['image/PhotonRateRadianceNoNoise'][()]
strh5['rystare/signal/photonRateIrradiance'] = scaleInput * strh5['rystare/fnumberConeSr'][()] * \
imghd5['image/PhotonRateRadiance'][()]
if 'image/pixelPitch' in imghd5:
strh5['rystare/pixelPitch'] = imghd5['image/pixelPitch'][()]
strh5['rystare/imageSizePixels'] = imghd5['image/imageSizePixels'][...]
numPixels = imghd5['image/imageSizePixels']
strh5['rystare/imageName'] = imghd5['image/imageName'][()]
strh5['rystare/imageFilename'] = imghd5['image/imageFilename'][()]
strh5['rystare/imageSizePixels'] = imghd5['image/imageSizePixels'][()]
strh5['rystare/wavelength'] = imghd5['image/wavelength'][()]
strh5['rystare/imageSizeRows'] = imghd5['image/imageSizeRows'][()]
strh5['rystare/imageSizeCols'] = imghd5['image/imageSizeCols'][()]
pixelPitch = strh5['rystare/pixelPitch'][()]
numPixels = imghd5['image/imageSizePixels'][()]
strh5['rystare/imageSizeDiagonal'] = np.sqrt((pixelPitch[0] * numPixels[0]) ** 2. + (pixelPitch[1] * \
numPixels[1]) ** 2)
# strh5['rystare/imageSizeDiagonal'] = imghd5['image/imageSizeDiagonal'][()]
strh5['rystare/equivalentSignalUnit'] = imghd5['image/equivalentSignalUnit'][()]
strh5['rystare/equivalentSignalType'] = imghd5['image/equivalentSignalType'][()]
if 'image/EinUnits' in imghd5:
strh5['rystare/EinUnits'] = imghd5['image/EinUnits'][()]
if 'W' in imghd5['image/EinUnits'][()]:
strh5['rystare/LinUnits'] = 'W/(m2.sr)'
elif 'q' in imghd5['image/EinUnits'][()]:
strh5['rystare/LinUnits'] = 'q/(s.m2.sr)'
else:
strh5['rystare/LinUnits'] = imghd5['image/LinUnits'][()]
if 'W' in imghd5['image/LinUnits'][()]:
strh5['rystare/EinUnits'] = 'W/(m2)'
elif 'q' in imghd5['image/LinUnits'][()]:
strh5['rystare/EinUnits'] = 'q/(s.m2)'
else:
imghd5 = ryhdf5.open_HDF(pathtoimage)
strh5 = ryhdf5.open_HDF(hdffilename,'r+')
#images must be in photon rate irradiance units q/(s.m2)
strh5['rystare/equivalentSignal'] = scaleInput * imghd5['image/equivalentSignal'][...]
strh5['rystare/signal/photonRateRadianceNoNoise'] = scaleInput * imghd5['image/PhotonRateRadianceNoNoise'][...]
strh5['rystare/signal/photonRateRadiance'] = scaleInput * imghd5['image/PhotonRateRadiance'][...]
strh5['rystare/signal/photonRateIrradianceNoNoise'] = scaleInput * strh5['rystare/fnumberConeSr'][...] * \
imghd5['image/PhotonRateRadianceNoNoise'][...]
strh5['rystare/signal/photonRateIrradiance'] = scaleInput * strh5['rystare/fnumberConeSr'][...] * \
imghd5['image/PhotonRateRadiance'][...]
if 'image/pixelPitch' in imghd5:
strh5['rystare/pixelPitch'] = imghd5['image/pixelPitch'][...]
strh5['rystare/imageName'] = imghd5['image/imageName'][...]
strh5['rystare/imageFilename'] = imghd5['image/imageFilename'][...]
strh5['rystare/imageSizePixels'] = imghd5['image/imageSizePixels'][...]
strh5['rystare/wavelength'] = imghd5['image/wavelength'][...]
strh5['rystare/imageSizeRows'] = imghd5['image/imageSizeRows'][...]
strh5['rystare/imageSizeCols'] = imghd5['image/imageSizeCols'][...]
pixelPitch = strh5['rystare/pixelPitch'][...]
numPixels = imghd5['image/imageSizePixels']
strh5['rystare/imageSizeDiagonal'] = np.sqrt((pixelPitch[0] * numPixels[0]) ** 2. + (pixelPitch[1] * \
numPixels[1]) ** 2)
# strh5['rystare/imageSizeDiagonal'] = imghd5['image/imageSizeDiagonal'][...]
strh5['rystare/equivalentSignalUnit'] = imghd5['image/equivalentSignalUnit'][...]
strh5['rystare/equivalentSignalType'] = imghd5['image/equivalentSignalType'][...]
if 'image/EinUnits' in imghd5:
strh5['rystare/EinUnits'] = imghd5['image/EinUnits'][...]
if 'W' in imghd5['image/EinUnits'][...]:
strh5['rystare/LinUnits'] = 'W/(m2.sr)'
elif 'q' in imghd5['image/EinUnits'][...]:
strh5['rystare/LinUnits'] = 'q/(s.m2.sr)'
else:
strh5['rystare/LinUnits'] = imghd5['image/LinUnits'][...]
if 'W' in imghd5['image/LinUnits'][...]:
strh5['rystare/EinUnits'] = 'W/(m2)'
elif 'q' in imghd5['image/LinUnits'][...]:
strh5['rystare/EinUnits'] = 'q/(s.m2)'
#calculate the noise and final images
strh5 = photosensor(strh5) # here the Photon-to-electron conversion occurred.
with open(f'{prefix}{outfilename}.txt', 'wt') as fo:
fo.write(f"{'SignalPhotonRateIrradiance':26}, "
f"{np.mean(strh5['rystare/signal/photonRateIrradiance'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/photonRateIrradiance'][()]):.5e}\n")
fo.write(f"{'signalphotonRateIrradianceNU':26}, "
f"{np.mean(strh5['rystare/signal/photonRateIrradianceNU'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/photonRateIrradianceNU'][()]):.5e}\n")
fo.write(f"{'signalelectronRateIrradiance':26}, "
f"{np.mean(strh5['rystare/signal/electronRateIrradiance'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/electronRateIrradiance'][()]):.5e}\n")
fo.write(f"{'SignalelectronRate':26}, "
f"{np.mean(strh5['rystare/signal/electronRate'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/electronRate'][()]):.5e}\n")
fo.write(f"{'signallightelectronsNoShotNoise':26}, "
f"{np.mean(strh5['rystare/signal/lightelectronsnoshotnoise'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/lightelectronsnoshotnoise'][()]):.5e}\n")
fo.write(f"{'signallightelectrons':26}, "
f"{np.mean(strh5['rystare/signal/lightelectrons'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/lightelectrons'][()]):.5e}\n")
fo.write(f"{'signalDark':26}, "
f"{np.mean(strh5['rystare/signal/darkcurrentelectrons'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/darkcurrentelectrons'][()]):.5e}\n")
fo.write(f"{'source_follower_noise':26}, "
f"{np.mean(strh5['rystare/sourcefollower/source_follower_noise'][()]):.5e}, "
f"{np.var(strh5['rystare/sourcefollower/source_follower_noise'][()]):.5e}\n")
fo.write(f"{'SignalElectrons':26}, "
f"{np.mean(strh5['rystare/signal/electrons'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/electrons'][()]):.5e}\n")
fo.write(f"{'voltagebeforeSF':26}, "
f"{np.mean(strh5['rystare/signal/voltagebeforeSF'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/voltagebeforeSF'][()]):.5e}\n")
fo.write(f"{'voltagebeforecds':26}, "
f"{np.mean(strh5['rystare/signal/voltagebeforecds'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/voltagebeforecds'][()]):.5e}\n")
# fo.write('{:26}, {:.5e}, {:.5e}\n'.format(
# 'voltagebeforeadc',
# np.mean(strh5['rystare/signal/voltagebeforeadc'][()]),
# np.var(strh5['rystare/signal/voltage'][()])))
# fo.write('{:26}, {:.5e}, {:.5e}\n'.format(
# 'SignalVoltage',
# np.mean(strh5['rystare/signal/voltage'][()]),
# np.var(strh5['rystare/signal/voltagebeforeadc'][()])))
fo.write(f"{'SignalDN':26}, "
f"{np.mean(strh5['rystare/signal/DN'][()]):.5e}, "
f"{np.var(strh5['rystare/signal/DN'][()]):.5e}\n")
lstimgs = ['rystare/signal/photonRateIrradianceNoNoise', 'rystare/quantumEfficiency',
'rystare/signal/photonRateIrradiance','rystare/photondetector/lightPRNU/value',
'rystare/signal/photonRateIrradianceNU','rystare/signal/electronRateIrradiance',
'rystare/signal/electronRate', 'rystare/signal/lightelectronsnoshotnoise','rystare/signal/lightelectrons',
'rystare/darkcurrentelectronsnonoise', 'rystare/signal/darkcurrentelectrons',
'rystare/photondetector/darkcurrent/fixedPatternNoise/value',
'rystare/signal/darkcurrentelectrons',
'rystare/signal/electrons','rystare/signal/electronsWell',
'rystare/signal/sensenodevoltageLinear','rystare/noise/sn_reset/resetnoise',
'rystare/noise/sn_reset/vrefresetpluskTC','rystare/sensenode/vrefreset',
'rystare/signal/voltage','rystare/sourcefollower/gainA','rystare/signal/voltageAfterSF',
'rystare/sourcefollower/source_follower_noise','rystare/signal/voltageAfterSFnoise',
'rystare/sourcefollower/fpoffset/value','rystare/signal/voltagebeforecds',
'rystare/signal/voltageaftercds','rystare/ADC/gainILE','rystare/ADC/gain','rystare/signal/DN']
if doPlots:
ryhdf5.plotHDF5Images(strh5, prefix=prefix, colormap=mcm.jet, lstimgs=lstimgs,
logscale=strh5['rystare/flag/plots/plotLogs'][()], debug=False)
if doHisto:
ryhdf5.plotHDF5Histograms(strh5, prefix, bins=100, lstimgs=lstimgs)
if doImages:
ryhdf5.plotHDF5Bitmaps(strh5, prefix, pformat='png', lstimgs=lstimgs,debug=False)
strh5.flush()
strh5.close()
return hdffilename
################################################################
[docs]
def get_summary_stats(hdffilename):
"""Return a string with all the summary input and results data.
Args:
| hdffilename (string): filename for input HDF file
Returns:
| Returns a string with summary data.
Raises:
| No exception is raised.
Note:
Pre-existing bug: a ``StringIO`` buffer is created and assigned to
``output``, but all data is routed through ``print()`` to stdout
rather than written to ``output``. Consequently
``output.getvalue()`` always returns ``''`` regardless of the
input, so the returned ``contents`` is always an empty string.
The correct fix would be to redirect ``print()`` calls to
``output`` (e.g. ``print(..., file=output)``), but that change
is deferred to avoid altering observable behaviour.
Author: CJ Willers
"""
from io import StringIO
output = StringIO()
if hdffilename is not None:
strh5 = ryhdf5.open_HDF(hdffilename)
print(hdffilename)
print(f"Image file name : {strh5['rystare/imageFilename'][()]}")
print(f"Image name : {strh5['rystare/imageName'][()]}")
print(f"Input rystare/LinUnits : {strh5['rystare/LinUnits'][()]}")
# print('Input rystare/EinUnit : {}'.format(strh5['rystare/EinUnits'][()]))
print(f"Sensor type : {strh5['rystare/sensortype'][()]} ")
print(f"F-number : {strh5['rystare/fnumber'][()]} ")
print(f"F-number cone : {strh5['rystare/fnumberConeSr'][()]} sr")
print(f"Pixel pitch : {strh5['rystare/pixelPitch'][()]} m")
print(f"Detector area : {strh5['rystare/detectorArea'][()]} m")
if 'rystare/imageSizeDiagonal' in strh5:
print(f"Image size diagonal : {strh5['rystare/imageSizeDiagonal'][()]:.3e} m")
print(f"Image size pixels : {strh5['rystare/imageSizePixels'][()]} ")
print(f"Fill factor : {strh5['rystare/photondetector/geometry/fillfactor'][()]}")
print(f"Full well electrons : "
f"{strh5['rystare/sensenode/fullwellelectronselection/fullwellelectrons'][()]} e")
print(f"Integration time : {strh5['rystare/photondetector/integrationtime'][()]} s")
print(f"Wavelength : {strh5['rystare/wavelength'][()]} m")
print(f"Operating temperature : {strh5['rystare/photondetector/operatingtemperature'][()]} K")
print(f"Max equivalent input signal : {np.max(strh5['rystare/equivalentSignal'][()])} "
f"{strh5['rystare/equivalentSignalUnit'][()]}")
print(f"Min equivalent input signal : {np.min(strh5['rystare/equivalentSignal'][()])} "
f"{strh5['rystare/equivalentSignalUnit'][()]}")
print(f"{'PhotonRateRadianceNoNoise':28}: q/(m2.s) "
f"mean={np.mean(strh5['rystare/signal/photonRateRadianceNoNoise'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/photonRateRadianceNoNoise'][()]):.5e}")
print(f"{'PhotonRateIrradianceNoNoise':28}: q/(m2.s) "
f"mean={np.mean(strh5['rystare/signal/photonRateIrradianceNoNoise'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/photonRateIrradianceNoNoise'][()]):.5e}")
print(f"{'SignalPhotonRateIrradiance':28}: q/(m2.s) "
f"mean={np.mean(strh5['rystare/signal/photonRateIrradiance'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/photonRateIrradiance'][()]):.5e}")
print(f"{'SignalPhotonsNU':28}: q/(m2.s) "
f"mean={np.mean(strh5['rystare/signal/photonRateIrradianceNU'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/photonRateIrradianceNU'][()]):.5e}")
print(f"{'signalLightNoShotNoise':28}: e "
f"mean={np.mean(strh5['rystare/signal/lightelectronsnoshotnoise'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/lightelectronsnoshotnoise'][()]):.5e}")
print(f"{'signalLight':28}: e "
f"mean={np.mean(strh5['rystare/signal/lightelectrons'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/lightelectrons'][()]):.5e}")
print(f"{'signalDarkNoNoise':28}: e "
f"mean={np.mean(strh5['rystare/darkcurrentelectronsnonoise'][()]):.5e}, "
f"var={np.var(strh5['rystare/darkcurrentelectronsnonoise'][()]):.5e}")
print(f"{'signalDark':28}: e "
f"mean={np.mean(strh5['rystare/signal/darkcurrentelectrons'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/darkcurrentelectrons'][()]):.5e}")
print(f"{'SignalElectrons':28}: e "
f"mean={np.mean(strh5['rystare/signal/electrons'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/electrons'][()]):.5e}")
print(f"{'source_follower_noise':28}: v "
f"mean={np.mean(strh5['rystare/sourcefollower/source_follower_noise'][()]):.5e}, "
f"var={np.var(strh5['rystare/sourcefollower/source_follower_noise'][()]):.5e}")
print(f"{'SignalVoltage':28}: v "
f"mean={np.mean(strh5['rystare/signal/voltage'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/voltage'][()]):.5e}")
print(f"{'SignalDN':28}: DN "
f"mean={np.mean(strh5['rystare/signal/DN'][()]):.5e}, "
f"var={np.var(strh5['rystare/signal/DN'][()]):.5e}")
print(f"{'ADC gain':28}: DN "
f"mean={np.mean(strh5['rystare/ADC/gain'][()]):.5e}, "
f"var={np.var(strh5['rystare/ADC/gain'][()]):.5e}")
print(f"ADC : DN/v bits={strh5['rystare/ADC/num-bits'][()]} "
f"offset={strh5['rystare/ADC/offset'][()]} DN")
print(f"Dark current density : {strh5['rystare/photondetector/darkcurrent/densityAm2'][()]} A/m2")
print(f"Dark current coeff ca : {strh5['rystare/photondetector/darkcurrent/ca'][()]} (A)")
print(f"Dark current coeff ed : {strh5['rystare/photondetector/darkcurrent/ed'][()]} (A)")
print(f"Quantum external efficiency : {strh5['rystare/photondetector/externalquantumeff'][()]} ")
print(f"Quantum yield : {strh5['rystare/photondetector/quantumyield'][()]} ")
print(f"Sense node gain : {strh5['rystare/sensenode/gain'][()]} v/e")
print(f"Sense reset Vref : {strh5['rystare/sensenode/vrefreset'][()]} v")
print(f"Source follower gain : {strh5['rystare/sourcefollower/gain'][()]} ")
if strh5['rystare/sourcefollower/fpoffset/activate'][()]:
print(f"Dark offset model : {strh5['rystare/sourcefollower/fpoffset/model'][()]}")
print(f"Dark offset spread : {strh5['rystare/sourcefollower/fpoffset/sigma'][()]} e")
print(f"Dark response model : "
f"{strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/model'][()]}")
print(f"Dark response seed : {strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/seed'][()]}")
print(f"Dark response spread : "
f"{strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/sigma'][()]} ")
print(f"Dark response neg lim flag : "
f"{strh5['rystare/photondetector/darkcurrent/fixedPatternNoise/limitnegative'][()]}")
if strh5['rystare/photondetector/lightPRNU/activate'][()]:
print(f"Detector response mode : {strh5['rystare/photondetector/lightPRNU/model'][()]}")
print(f"Detector response seed : {strh5['rystare/photondetector/lightPRNU/seed'][()]}")
print(f"Detector response spread : {strh5['rystare/photondetector/lightPRNU/sigma'][()]}")
print(f"Material Eg 0 K : {strh5['rystare/photondetector/varshni/Egap0'][()]:.5f} eV")
print(f"Material Eg : {strh5['rystare/material/Eg-eV'][()]:.5f} eV")
print(f"Material alpha : {strh5['rystare/photondetector/varshni/varA'][()]} ")
print(f"Material beta : {strh5['rystare/photondetector/varshni/varB'][()]} ")
print(f"Sense node reset noise factr: {strh5['rystare/sensenode/resetnoise/factor'][()]} ")
print(f"Sense node reset kTC sigma : {strh5['rystare/sensenode/ResetKTC-sigma'][()]} v")
print(f"Sense node capacitance : {1000000000000000.0 * strh5['rystare/sensenode/capacitance'][()]:.5e} fF")
print(f"k1 constant, Csn = k1/V : {strh5['rystare/sensenode/gainresponse/k1'][()]} ")
print(f"Sense node signal full well : {strh5['rystare/sensenode/volt-fullwell'][()]} V")
print(f"Sense node signal minimum : {strh5['rystare/sensenode/volt-min'][()]} V")
print(f"Sense node reset noise : {strh5['rystare/sensenode/resetnoise/activate'][()]} ")
print(f"CDS gain : {strh5['rystare/sourcefollower/CDS/gain'][()]} ")
print(f"CDS sample-to-sampling time : {strh5['rystare/sourcefollower/CDS/sampletosamplingtime'][()]} s")
print(f"Delta induced modulation : {strh5['rystare/sourcefollower/noise/deltaindmodulation'][()]} ")
print(f"Data clock speed : {strh5['rystare/sourcefollower/dataclockspeed'][()]} Hz")
print(f"Frequency sampling delta : {strh5['rystare/sourcefollower/freqsamplingdelta'][()]} Hz")
print(f"White noise density : {strh5['rystare/sourcefollower/noise/whitenoisedensity'][()]} V/rtHz")
print(f"Flicker corner : {strh5['rystare/sourcefollower/noise/flickerCornerHz'][()]} Hz")
print(f"{'Source follower sigma':28}: v "
f"mean={np.mean(strh5['rystare/sourcefollower/sigma'][()]):.5e}, "
f"var={np.var(strh5['rystare/sourcefollower/sigma'][()]):.5e}")
strh5.flush()
strh5.close()
# Retrieve file contents
contents = output.getvalue()
# Close object and discard memory buffer
output.close()
return contents
################################################################
################################################################
##
##
if __name__ == '__init__':
pass
if __name__ == '__main__':
import os.path
import pyradi.ryfiles as ryfiles
import pyradi.ryutils as ryutils
doAll = True
#---------- test the limitzero function ---------------------
if doAll:
import numpy as np
import pyradi.ryplot as ryplot
from scipy.stats import norm
thres = 0.6
rv = norm.rvs(loc=1, scale=0.4, size=10000)
hist, bins = np.histogram(rv, bins=100)
a = np.linspace(-10, 2, 100)
with ryplot.savePlot(1,1,1,figsize=(12,4), saveName=['limitzero01.png']) as p:
p.plot(1,a,limitzero(a, thres),'Clipping function','Input','Output')
with ryplot.savePlot(2,1,1,figsize=(12,6), saveName=['limitzero02.png']) as q:
q.plot(1,(bins[1:]+bins[:-1])/2.,hist /(bins[1]-bins[0]),'Histrograms','Value','Count',label=['No clip'])
for sigma in [0.2, 0.3, 0.4, 0.5]:
# for sigma in [0.1]:
rv = norm.rvs(loc=1, scale=sigma, size=10000)
histl, binsl = np.histogram(limitzero(rv,thres), bins=100)
q.plot(1, (binsl[1:]+binsl[:-1])/2., histl/(binsl[1]-binsl[0]),
label=[f'sigma={sigma}'])