Source code for exosim.tools.darkCurrentMap

import astropy.units as u
import numpy as np
from scipy.interpolate import interp1d

import exosim.models.signal as signal
import exosim.output as output
from exosim.tasks.task import Task
from exosim.utils import RunConfig, check_units


[docs]class DarkCurrentMap(Task): """ Produces the channel dark current map Returns -------- :class:`~exosim.models.signal.Signal` channel dark current map Raises ------ TypeError: if the output is not a :class:`~exosim.models.signal.Signal` class Notes ----- This is a default class with standardized inputs and outputs. The user can load this class and overwrite the "model" method to implement a custom Task to replace this. """ def __init__(self): """ Parameters __________ parameters: dict dictionary containing the parameters. This is usually parsed from :class:`~exosim.tasks.load.loadOptions.LoadOptions` time: :class:`~astropy.units.Quantity` time grid output: :class:`~exosim.output.output.Output` (optional) output file """ self.add_task_param("parameters", "parameters dict") self.add_task_param("time", "time grid") self.add_task_param("output", "output file", None)
[docs] def execute(self): self.info("creating dark current map") parameters = self.get_task_param("parameters") tt = self.get_task_param("time") dc_map = self.model(parameters, tt) # checking output if not isinstance(dc_map, signal.Signal): self.error("wrong output format") raise TypeError("wrong output format") self.debug("dark current map: {}".format(dc_map.data)) output_file = self.get_task_param("output") if output_file: if issubclass(output_file.__class__, output.Output): dc_map.write(output_file, "dc_map") self.set_output(dc_map)
[docs] def model(self, parameters, time): """ Parameters ---------- parameters: dict dictionary contained the sources parameters. This is usually parsed from :class:`~exosim.tasks.load.loadOptions.LoadOptions` time: :class:`~astropy.units.Quantity` time grid. Returns -------- :class:`~exosim.models.signal.Signal` dark current efficiency map """ # variance map if "dc_sigma" not in parameters["detector"].keys(): self.error("dc_sigma not provided in detector configuration") raise KeyError("dc_sigma not provided in detector configuration") if "dc_mean" in parameters["detector"].keys(): pass elif "dc_median" in parameters["detector"].keys(): check_units(parameters["detector"]["dc_median"], "ct/s") self.compute_dc_mean(parameters["detector"]) check_units(parameters["detector"]["dc_mean"], "ct/s") check_units(parameters["detector"]["dc_sigma"], "ct/s") variance_map = RunConfig.random_generator.normal( parameters["detector"]["dc_mean"].value, parameters["detector"]["dc_sigma"].value, ( 1, parameters["detector"]["spatial_pix"], parameters["detector"]["spectral_pix"], ), ) idx = np.where(variance_map <= 0) variance_map[idx] = 1e-16 t = [0] * u.hr # temporal variation if "dc_aging_factor" in parameters["detector"].keys(): t0 = parameters["detector"]["dc_aging_time_scale"] t0 = check_units(t0, u.hr, force=True) # creates the aging effect dc_aging = RunConfig.random_generator.normal( 1, parameters["detector"]["dc_aging_factor"], ( 1, parameters["detector"]["spatial_pix"], parameters["detector"]["spectral_pix"], ), ) # remove values greater than 1, because QE should become smaller dc_aging[dc_aging > 1] = 2 - dc_aging[dc_aging > 1] # create the map evolution over time variance_map = np.vstack((variance_map, variance_map * dc_aging)) t = np.hstack((t, t0)) variance_map = variance_map.reshape( (t.size, variance_map.shape[1] * variance_map.shape[2]) ) f = interp1d(t, variance_map, fill_value="extrapolate", axis=0) variance_map = f(time) variance_map = variance_map.reshape( ( time.size, parameters["detector"]["spatial_pix"], parameters["detector"]["spectral_pix"], ) ) t = time # resized map considering an oversampling factor resized_map = variance_map.repeat( parameters["detector"]["oversampling"], axis=1 ).repeat(parameters["detector"]["oversampling"], axis=2) dc_map = signal.Dimensionless( data=resized_map, spectral=np.arange(0, parameters["detector"]["spectral_pix"]) * u.pix, time=t, ) dc_map.temporal_rebin(time) return dc_map
[docs] def compute_dc_mean(self, detector): """ Computes the mean of the dark current (dc_mean) from the log-normal distributon. Notes ----- The probability density function for the log-normal distributon is: .. math:: pdf(x) = \frac{1}{\\sigma x \\sqrt{2\\pi}} \\exp\\left(-\frac{(\\log(x) - \\mu)^2}{2 \\sigma^2}\right) The mean of the pdf can be computed as: .. math:: mean = \\exp(\\mu + \frac{s^2}{2}) where s is the pdf standard deviation, computed by taking the sqrt of the variance, defined as: .. math:: var = \\left(\\exp(\\sigma^2) - 1\right) \\exp(2 \\mu + \\sigma^2) Parameters ---------- detector: dict Dictionary for the detector. This is usually parsed from :class:`~exosim.tasks.load.loadOptions.LoadOptions` Returns ------- None Updates the detector dictionary with the value of dc_mean """ mu = np.log(detector["dc_median"].value) root = lambda s: detector["dc_sigma"].value ** 2 - ( np.exp(s**2) - 1 ) * np.exp(2 * mu + s**2) from scipy.optimize import newton sigma = newton(func=root, x0=0.5) atol = 1e-6 if not np.isclose(root(sigma), 0.0, atol=atol): self.error( "dc_sigma root not close to 0 (tol={:.1e})".format(atol) ) raise ValueError( "dc_sigma root not close to 0 (tol={:.1e})".format(atol) ) dc_mean = np.exp(mu + sigma**2 / 2) detector.update({"dc_mean": dc_mean * u.ct / u.s})