ESBO-ETC/esbo_etc/classes/sensor/Imager.py
2020-10-05 20:45:13 +02:00

576 lines
30 KiB
Python

from astropy import units as u
from .ASensor import ASensor
from ..IRadiant import IRadiant
from ..Entry import Entry
import numpy as np
from typing import Union, Tuple
from ..psf.Airy import Airy
from ..psf.Zemax import Zemax
from ..SpectralQty import SpectralQty
from .PixelMask import PixelMask
from ...lib.logger import logger
import astropy.constants as const
import os
import astropy.io.fits as fits
class Imager(ASensor):
"""
A class for modelling a Image-sensor
"""
__encircled_energy: Union[str, float, u.Quantity]
@u.quantity_input(pixel_geometry=u.pixel, pixel_size="length", sigma_read_out=u.electron ** 0.5 / u.pix,
center_offset=u.pix, dark_current=u.electron / u.pix / u.second, well_capacity=u.electron)
def __init__(self, parent: IRadiant, quantum_efficiency: Union[str, u.Quantity],
pixel_geometry: u.Quantity, pixel_size: u.Quantity, sigma_read_out: u.Quantity, dark_current: u.Quantity,
well_capacity: u.Quantity, f_number: Union[int, float], common_conf: Entry,
center_offset: u.Quantity = np.array([0, 0]) << u.pix, shape: str = "circle",
contained_energy: Union[str, int, float] = "FWHM", aperture_size: u.Quantity = None):
"""
Initialize a new Image-sensor model.
Parameters
----------
parent : IRadiant
The parent element of the optical component from which the electromagnetic radiation is received.
quantum_efficiency : Union[str, u.Quantity]
The quantum efficiency of the detector. This can be either the path to the file containing the values of
the spectral quantum efficiency or the overall quantum efficiency as astropy quantity.
pixel_geometry : u.Quantity
The geometry of the pixel array as Quantity in pixels with two entries:
[number of pixels in x-direction, number of pixels in y-direction]
pixel_size : length-Quantity
The edge length of a pixel (assumed to be square).
sigma_read_out : Quantity
The RMS-read noise per detector pixel in electrons^0.5 / pixel.
dark_current : Quantity
The dark current of a detector pixel in electrons / (pixels * s).
well_capacity : Quantity
The pixel's well capacity in electrons.
f_number : Union[int, float]
The f-number of the optical system.
common_conf : Entry
The common-Entry of the configuration.
center_offset : u.Quantity
The offset of the PSF-center relative to the center of the detector array as length-quantity with two
entries: [offset in x-direction, offset in y-direction]
shape : str
The shape of the photometric aperture. Can be either square or circle
contained_energy : Union[str, int, float]
The energy contained within the photometric aperture.
aperture_size : u.Quantity
The radius respectively the edge length of the photometric aperture.
"""
super().__init__(parent)
if type(quantum_efficiency) == str:
self.__quantum_efficiency = SpectralQty.fromFile(quantum_efficiency, u.nm, u.electron / u.photon)
else:
self.__quantum_efficiency = quantum_efficiency
self.__pixel_geometry = pixel_geometry
self.__pixel_size = pixel_size
self.__sigma_read_out = sigma_read_out
self.__dark_current = dark_current
self.__well_capacity = well_capacity
self.__f_number = f_number
self.__center_offset = center_offset
self.__shape = shape
self.__contained_energy = contained_energy
self.__aperture_size = aperture_size
self.__common_conf = common_conf
# Calculate central wavelength
self.__central_wl = self.__common_conf.wl_min() + (
self.__common_conf.wl_max() - self.__common_conf.wl_min()) / 2
# Parse PSF
if hasattr(common_conf, "psf") and common_conf.psf().lower() == "airy":
# Use an airy disk as PSF
self.__psf = Airy(self.__f_number, self.__central_wl, common_conf.d_aperture(), common_conf.psf.osf,
pixel_size)
else:
# Read PSF from Zemax file
self.__psf = Zemax(common_conf.psf(), self.__f_number, self.__central_wl, common_conf.d_aperture(),
common_conf.psf.osf, pixel_size)
@u.quantity_input(exp_time="time")
def calcSNR(self, background: SpectralQty, signal: SpectralQty, obstruction: float,
exp_time: u.Quantity) -> u.dimensionless_unscaled:
"""
Calculate the signal to background ratio (SNR) for the given exposure time using the CCD-equation.
Parameters
----------
background : SpectralQty
The received background radiation
signal : SpectralQty
The received signal radiation
obstruction : float
The obstruction factor of the aperture as ratio A_ob / A_ap
exp_time : time-Quantity
The exposure time to calculate the SNR for.
Returns
-------
snr : Quantity
The calculated SNR as dimensionless quantity
"""
# Calculate the electron currents
signal_current, background_current, read_noise, dark_current = self.__exposePixels(background, signal,
obstruction)
# Calculate the SNR using the CCD-equation
logger.info("Calculating the SNR...", extra={"spinning": True})
snr = signal_current.sum() * exp_time / np.sqrt(
(signal_current + background_current + dark_current).sum() * exp_time + (read_noise ** 2).sum())
# Print information
for exp_time_ in exp_time if exp_time.size > 1 else [exp_time]:
self.__printDetails(signal_current * exp_time_, background_current * exp_time_, read_noise,
dark_current * exp_time_, "t_exp=%.2f s: " % exp_time_.value)
self.__output(signal_current * exp_time_, background_current * exp_time_, read_noise,
dark_current * exp_time_, "texp_%.2f" % exp_time_.value)
# Return the value of the SNR, ignoring the physical units (electrons^0.5)
return snr.value * u.dimensionless_unscaled
@u.quantity_input(snr=u.dimensionless_unscaled)
def calcExpTime(self, background: SpectralQty, signal: SpectralQty, obstruction: float, snr: u.Quantity) -> u.s:
"""
Calculate the necessary exposure time in order to achieve the given SNR.
Parameters
----------
background : SpectralQty
The received background radiation
signal : SpectralQty
The received signal radiation
obstruction : float
The obstruction factor of the aperture as ratio A_ob / A_ap
snr : Quantity
The SNR for which the necessary exposure time shall be calculated as dimensionless quantity.
Returns
-------
exp_time : Quantity
The necessary exposure time in seconds.
"""
# Calculate the electron currents
signal_current, background_current, read_noise, dark_current = self.__exposePixels(background, signal,
obstruction)
logger.info("Calculating the exposure time...", extra={"spinning": True})
# Calculate the electron currents for all pixels
signal_current_tot = signal_current.sum()
# Fix the physical units of the SNR
snr = snr * u.electron ** 0.5
# Calculate the ratio of the background- and dark-current to the signal current as auxiliary variable
current_ratio = (background_current.sum() + dark_current.sum()) / signal_current_tot
# Calculate the necessary exposure time as inverse of the CCD-equation
exp_time = snr ** 2 * (
1 + current_ratio + np.sqrt((1 + current_ratio) ** 2 + 4 * (read_noise ** 2).sum() / snr ** 2)) / (
2 * signal_current_tot)
# Print information
for snr_, exp_time_ in zip(snr, exp_time) if snr.size > 1 else zip([snr], [exp_time]):
self.__printDetails(signal_current * exp_time_, background_current * exp_time_, read_noise,
dark_current * exp_time_, "SNR=%.2f: " % snr_.value)
self.__output(signal_current * exp_time_, background_current * exp_time_, read_noise,
dark_current * exp_time_, "snr_%.2f" % snr_.value)
return exp_time
# @u.quantity_input(exp_time="time", snr=u.dimensionless_unscaled, target_brightness=[u.mag, u.mag / u.sr])
def calcSensitivity(self, background: SpectralQty, signal: SpectralQty, obstruction: float, exp_time: u.Quantity,
snr: u.Quantity, target_brightness: u.Quantity) -> [u.mag, u.mag / u.sr]:
"""
Calculate the sensitivity of the telescope detector combination.
Parameters
----------
background : SpectralQty
The received background radiation
signal : SpectralQty
The received signal radiation
obstruction : float
The obstruction factor of the aperture as ratio A_ob / A_ap
exp_time : Quantity
The exposure time in seconds.
snr : Quantity
The SNR for which the sensitivity time shall be calculated.
target_brightness : Quantity
The target brightness in mag or mag / sr.
Returns
-------
sensitivity: Quantity
The sensitivity as limiting apparent star magnitude in mag.
"""
# Calculate the electron currents
signal_current, background_current, read_noise, dark_current = self.__exposePixels(background, signal,
obstruction)
logger.info("Calculating the sensitivity...", extra={"spinning": True})
# Fix the physical units of the SNR
snr = snr * u.electron ** 0.5
signal_current_lim = snr * (snr + np.sqrt(
snr ** 2 + 4 * (exp_time * (background_current.sum() + dark_current.sum()) +
(read_noise ** 2).sum()))) / (2 * exp_time)
# Print information
for snr_, exp_time_, signal_current_lim_ in zip(snr, exp_time, signal_current_lim) if snr.size > 1 else zip(
[snr], [exp_time], [signal_current_lim]):
self.__printDetails(signal_current_lim_ * exp_time_, background_current * exp_time_, read_noise,
dark_current * exp_time_, "SNR=%.2f t_exp=%.2f s: " % (snr_.value, exp_time_.value))
self.__output(signal_current * signal_current_lim_ / signal_current.sum() * exp_time_,
background_current * exp_time_, read_noise, dark_current * exp_time_,
"snr_%.2f_texp_%.2f" % (snr_.value, exp_time_.value))
return target_brightness - 2.5 * np.log10(signal_current_lim / signal_current.sum()) * target_brightness.unit
@u.quantity_input(signal=u.electron, background=u.electron, read_noise=u.electron ** 0.5, dark=u.electron)
def __printDetails(self, signal: u.Quantity, background: u.Quantity, read_noise: u.Quantity,
dark: u.Quantity, prefix: str = ""):
"""
Print details on the signal and noise composition.
Parameters
----------
signal : Quantity
The collected electrons from the target in electrons.
background : Quantity
The collected electrons from the background in electrons.
read_noise : Quantity
The read noise in electrons.
dark : Quantity
The electrons from the dark current in electrons.
prefix : str
The prefix to be used for printing
Returns
-------
"""
# Calculate the total collected electrons per pixel
total = signal + background + dark
# Check for overexposed pixels
overexposed = total > self.__well_capacity
if np.any(overexposed):
# Show a warning for the overexposed pixels
logger.warning(prefix + str(np.count_nonzero(overexposed)) + " pixels are overexposed.")
logger.info("-------------------------------------------------------------------------------------------------")
logger.info(prefix + "Collected electrons from target: %1.2e electrons" % signal.sum().value)
logger.info(prefix + "Collected electrons from background: %1.2e electrons" % background.sum().value)
logger.info(prefix + "Electrons from dark current: %1.2e electrons" % dark.sum().value)
logger.info(prefix + "Read noise: %1.2e electrons" % (read_noise ** 2).sum().value)
logger.info(prefix + "Total collected electrons: %1.2e electrons" % total.sum().value)
logger.info("-------------------------------------------------------------------------------------------------")
@u.quantity_input(signal=u.electron, background=u.electron, read_noise=u.electron ** 0.5, dark=u.electron)
def __output(self, signal: u.Quantity, background: u.Quantity, read_noise: u.Quantity,
dark: u.Quantity, name: str):
"""
Write the signal and the noise in electrons to files.
Parameters
----------
signal : Quantity
The collected electrons from the target in electrons.
background : Quantity
The collected electrons from the background in electrons.
read_noise : Quantity
The read noise in electrons.
dark : Quantity
The electrons from the dark current in electrons.
name : str
The name of the configuration.
Returns
-------
"""
# Concatenate the paths
path = os.path.join(self.__common_conf.output.path, name)
try:
os.makedirs(path, exist_ok=True)
except FileExistsError:
logger.warning("Output directory '" + path + "' already exists.")
# Calculate the indices of nonzero values and create a bounding rectangle
y, x = np.nonzero(signal)
y_min = min(y)
y_max = max(y)
x_min = min(x)
x_max = max(x)
# Write arrays to file
if self.__common_conf.output.format.lower() == "csv":
mes = "Range reduced to nonzero values.\nThe origin is in the top left corner, starting with 0.\n" + \
"Column index range: %d - %d\nRow index range: %d - %d\n" % (y_min, y_max, x_min, x_max)
np.savetxt(os.path.join(path, "signal.csv"), signal[y_min:(y_max + 1), x_min:(x_max + 1)].value,
delimiter=",", header="Signal in electrons\n" + mes)
np.savetxt(os.path.join(path, "background.csv"), background[y_min:(y_max + 1), x_min:(x_max + 1)].value,
delimiter=",", header="Background in electrons\n" + mes)
np.savetxt(os.path.join(path, "read_noise.csv"), read_noise[y_min:(y_max + 1), x_min:(x_max + 1)].value,
delimiter=",", header="Read noise in electrons\n" + mes)
np.savetxt(os.path.join(path, "dark_noise.csv"), dark[y_min:(y_max + 1), x_min:(x_max + 1)].value,
delimiter=",", header="Dark noise in electrons\n" + mes)
elif self.__common_conf.output.format.lower() == "fits":
mes = "Range reduced to nonzero values. The origin is in the top left corner, starting with 0. " + \
"Column index range: %d - %d Row index range: %d - %d " % (y_min, y_max, x_min, x_max)
hdu = fits.PrimaryHDU(header=fits.Header(dict(COMMENT="Simulation data created by ESBO-ETC.",
TELESCOP="ESBO-ETC")))
signal_hdu = fits.ImageHDU(signal[y_min:(y_max + 1), x_min:(x_max + 1)].value, name="signal",
header=fits.Header(dict(COMMENT="Signal in electrons. " + mes,
TELESCOP="ESBO-ETC")))
background_hdu = fits.ImageHDU(background[y_min:(y_max + 1), x_min:(x_max + 1)].value, name="background",
header=fits.Header(dict(COMMENT="Background in electrons. " + mes,
TELESCOP="ESBO-ETC")))
read_noise_hdu = fits.ImageHDU(read_noise[y_min:(y_max + 1), x_min:(x_max + 1)].value, name="read noise",
header=fits.Header(dict(COMMENT="Read noise in electrons. " + mes,
TELESCOP="ESBO-ETC")))
dark_hdu = fits.ImageHDU(dark[y_min:(y_max + 1), x_min:(x_max + 1)].value, name="dark noise",
header=fits.Header(dict(COMMENT="Dark noise in electrons. " + mes,
TELESCOP="ESBO-ETC")))
hdul = fits.HDUList([hdu, signal_hdu, background_hdu, read_noise_hdu, dark_hdu])
hdul.writeto(os.path.join(path, "results.fits"), overwrite=True)
def __exposePixels(self, background: SpectralQty, signal: SpectralQty,
obstruction: float) -> Tuple[u.Quantity, u.Quantity, u.Quantity, u.Quantity]:
"""
Expose the pixels and calculate the signal and noise electron currents per pixel.
Parameters
----------
background : SpectralQty
The received background radiation
signal : SpectralQty
The received signal radiation
obstruction : float
The obstruction factor of the aperture as ratio A_ob / A_ap
Returns
-------
signal_current : Quantity
The electron current from the target as PixelMask in electrons / s
background_current : Quantity
The electron current from the background as PixelMask in electrons / s
read_noise : Quantity
The read noise per pixel in electrons
dark_current : Quantity
The electron current from the dark noise as PixelMask in electrons / s
"""
# Calculate the total incoming electron current
logger.info("Calculating incoming electron current...", extra={"spinning": True})
signal_current, size, obstruction, background_current = self.__calcIncomingElectronCurrent(background, signal,
obstruction)
# info("Finished calculating incoming electron current", extra={"spinning": False})
# Initialize a new PixelMask
mask = PixelMask(self.__pixel_geometry, self.__pixel_size, self.__center_offset)
if size.lower() == "extended":
# Target is extended, a diameter of 0 pixels results in a mask with one pixel marked
d_photometric_ap = 0 * u.pix
# Mask the pixels to be exposed
mask.createPhotometricAperture("circle", d_photometric_ap / 2, np.array([0, 0]) << u.pix)
else:
# Target is a point source
if self.__aperture_size is not None:
# Us ethe aperture size as diameter
d_photometric_ap = self.__aperture_size
else:
# Calculate the diameter of the photometric aperture from the given contained energy
logger.info("Calculating the diameter of the photometric aperture...",
extra={"spinning": True})
d_photometric_ap = self.__calcPhotometricAperture(obstruction)
# Mask the pixels to be exposed
mask.createPhotometricAperture(self.__shape, d_photometric_ap / 2)
# Calculate the background current PixelMask
background_current = mask * background_current * u.pix
# Calculate the read noise PixelMask
read_noise = mask * self.__sigma_read_out * u.pix
# Calculate the dark current PixelMask
dark_current = mask * self.__dark_current * u.pix
if self.__aperture_size is None and size.lower() != "extended":
if type(self.__contained_energy) == str:
if self.__contained_energy.lower() == "peak":
logger.info("The radius of the photometric aperture is %.2f pixels. This equals the peak value" % (
d_photometric_ap.value / 2))
elif self.__contained_energy.lower() == "fwhm":
logger.info("The radius of the photometric aperture is %.2f pixels. This equals the FWHM" % (
d_photometric_ap.value / 2))
elif self.__contained_energy.lower() == "min":
logger.info(
"The radius of the photometric aperture is %.2f pixels. This equals the first minimum" % (
d_photometric_ap.value / 2))
else:
logger.info(
"The radius of the photometric aperture is %.2f pixels. This equals %.0f%% encircled energy" % (
d_photometric_ap.value / 2, self.__contained_energy))
logger.info("The photometric aperture contains " + str(np.count_nonzero(mask)) + " pixels.")
if size.lower() != "extended":
# Map the PSF onto the pixel mask in order to get the relative irradiance of each pixel
logger.info("Mapping the PSF onto the pixel grid...", extra={"spinning": True})
mask = self.__psf.mapToPixelMask(mask,
getattr(getattr(self.__common_conf, "jitter_sigma", None), "val", None),
obstruction)
# Calculate the signal current PixelMask
signal_current = mask * signal_current
return signal_current, background_current, read_noise, dark_current
def __calcPhotometricAperture(self, obstruction: float) -> u.Quantity:
"""
Calculate the diameter of the photometric aperture
Parameters
----------
obstruction : float
The obstruction factor as A_ob / A_ap.
Returns
-------
d_photometric_ap : Quantity
The diameter of the photometric aperture in pixels.
"""
# Calculate the reduced observation angle
jitter_sigma = getattr(getattr(self.__common_conf, "jitter_sigma", None), "val", None)
reduced_observation_angle = self.__psf.calcReducedObservationAngle(self.__contained_energy, jitter_sigma,
obstruction)
logger.debug("Reduced observation angle: %.2f" % reduced_observation_angle.value)
# Calculate angular width of PSF
observation_angle = (reduced_observation_angle * self.__central_wl / self.__common_conf.d_aperture() *
180.0 / np.pi * 3600).decompose() * u.arcsec
# Calculate FOV of a single pixel
pixel_fov = (self.__pixel_size / (self.__f_number * self.__common_conf.d_aperture()) * 180.0 /
np.pi * 3600).decompose() * u.arcsec
# Calculate the radius of the photometric aperture in pixels
d_photometric_ap = observation_angle / pixel_fov
return d_photometric_ap * u.pix
def __calcIncomingElectronCurrent(self, background: SpectralQty, signal: SpectralQty,
obstruction: float) -> Tuple[u.Quantity, str, float, u.Quantity]:
"""
Calculate the detected electron current of the signal and the background.
Parameters
----------
background : SpectralQty
The received background radiation
signal : SpectralQty
The received signal radiation
obstruction : float
The obstruction factor of the aperture as ratio A_ob / A_ap
Returns
-------
signal_current : Quantity
The electron current on the detector caused by the target in electrons / s.
size : str
The size of the target.
obstruction : float
The obstruction factor as A_ob / A_ap.
background_current : Quantity
The electron current on the detector caused by the background in electrons / (s * pix).
"""
# Calculate the photon current of the background
logger.info("Calculating the background photon current.")
background_photon_current = background * np.pi * (
self.__pixel_size.to(u.m) ** 2 / u.pix) / (4 * self.__f_number ** 2 + 1) * (1 * u.sr)
# Calculate the incoming photon current of the target
logger.info("Calculating the signal photon current.")
size = "extended" if signal.qty.unit.is_equivalent(u.W / (u.m ** 2 * u.nm * u.sr)) else "point"
if size == "point":
signal_photon_current = signal * np.pi * (self.__common_conf.d_aperture() / 2) ** 2
else:
signal_photon_current = signal * np.pi * self.__pixel_size.to(u.m) ** 2 / (
4 * self.__f_number ** 2 + 1) * (1 * u.sr)
# Calculate the electron current of the background and thereby handling the photon energy as lambda-function
background_current = (
background_photon_current / (lambda wl: (const.h * const.c / wl).to(u.W * u.s) / u.photon) *
self.__quantum_efficiency).integrate().decompose()
# Calculate the electron current of the signal and thereby handling the photon energy as lambda-function
signal_current = (signal_photon_current / (lambda wl: (const.h * const.c / wl).to(u.W * u.s) / u.photon) *
self.__quantum_efficiency).integrate().decompose()
logger.debug("Signal current: %1.2e e-/s" % signal_current.value)
logger.debug("Target size: " + size)
logger.debug("Obstruction: %.2f" % obstruction)
logger.debug("Background current: %1.2e e-/s" % background_current.value)
return signal_current, size, obstruction, background_current
@staticmethod
def check_config(sensor: Entry, conf: Entry) -> Union[None, str]:
"""
Check the configuration for this class
Parameters
----------
sensor : Entry
The configuration entry to be checked.
conf: Entry
The complete configuration.
Returns
-------
mes : Union[None, str]
The error message of the check. This will be None if the check was successful.
"""
if not hasattr(sensor, "f_number"):
return "Missing container 'f_number'."
mes = sensor.f_number.check_float("val")
if mes is not None:
return "f_number: " + mes
if not hasattr(sensor, "pixel_geometry"):
return "Missing container 'pixel_geometry'."
mes = sensor.pixel_geometry.check_quantity("val", u.pix)
if mes is not None:
return "pixel_geometry: " + mes
if hasattr(sensor, "center_offset") and isinstance(sensor.center_offset, Entry):
mes = sensor.center_offset.check_quantity("val", u.pix)
if mes is not None:
return "center_offset: " + mes
# Check pixel
if not hasattr(sensor, "pixel"):
return "Missing container 'pixel'."
if not hasattr(sensor.pixel, "quantum_efficiency"):
return "Missing container 'quantum_efficiency'."
mes = sensor.pixel.quantum_efficiency.check_quantity("val", u.electron / u.photon)
if mes is not None:
mes = sensor.pixel.quantum_efficiency.check_file("val")
if mes is not None:
return "pixel -> quantum_efficiency: " + mes
if not hasattr(sensor.pixel, "pixel_size"):
return "Missing container 'pixel_size'."
mes = sensor.pixel.pixel_size.check_quantity("val", u.m)
if mes is not None:
return "pixel -> pixel_size: " + mes
if not hasattr(sensor.pixel, "dark_current"):
return "Missing container 'dark_current'."
mes = sensor.pixel.dark_current.check_quantity("val", u.electron / (u.pix * u.s))
if mes is not None:
return "pixel -> dark_current: " + mes
if not hasattr(sensor.pixel, "sigma_read_out"):
return "Missing container 'sigma_read_out'."
mes = sensor.pixel.sigma_read_out.check_quantity("val", u.electron ** 0.5 / u.pix)
if mes is not None:
return "pixel -> sigma_read_out: " + mes
if not hasattr(sensor.pixel, "well_capacity"):
return "Missing container 'well_capacity'."
mes = sensor.pixel.well_capacity.check_quantity("val", u.electron)
if mes is not None:
return "pixel -> well_capacity: " + mes
# Check photometric aperture
if not hasattr(sensor, "photometric_aperture"):
setattr(sensor, "photometric_aperture", Entry(shape=Entry(val="circle"),
contained_energy=Entry(val="FWHM")))
if hasattr(sensor.photometric_aperture, "aperture_size"):
mes = sensor.photometric_aperture.aperture_size.check_quantity("val", u.pix)
if mes is not None:
return "photometric_aperture -> aperture_size: " + mes
else:
if not hasattr(sensor.photometric_aperture, "shape"):
return "Missing container 'shape'."
mes = sensor.photometric_aperture.shape.check_selection("val", ["square", "circle"])
if mes is not None:
return "photometric_aperture -> shape: " + mes
if not hasattr(sensor.photometric_aperture, "contained_energy"):
return "Missing container 'contained_energy'."
mes = sensor.photometric_aperture.contained_energy.check_float("val")
if mes is not None:
if conf.common.psf().lower() == "airy":
mes = sensor.photometric_aperture.contained_energy.check_selection("val",
["peak", "FWHM", "fwhm",
"min"])
if mes is not None:
return "photometric_aperture -> contained_energy: " + mes
else:
mes = sensor.photometric_aperture.contained_energy.check_selection("val", ["FWHM", "fwhm"])
if mes is not None:
return "photometric_aperture -> contained_energy: " + mes