Source code for exosim.tools.dark_current_map

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.load_options.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(f"dark current map: {dc_map.data}") output_file = self.get_task_param("output") if output_file and 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.load_options.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"]: self.error("dc_sigma not provided in detector configuration") raise KeyError("dc_sigma not provided in detector configuration") if "dc_mean" in parameters["detector"]: pass elif "dc_median" in parameters["detector"]: 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"]: 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): r""" Computes the mean of the dark current (dc_mean) from the log-normal distribution. Notes ----- The probability density function for the log-normal distribution 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\left(\mu + \frac{s^2}{2}\right) 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.load_options.LoadOptions` Returns ------- None Updates the detector dictionary with the value of dc_mean """ mu = np.log(detector["dc_median"].value) def root(s): return 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(f"dc_sigma root not close to 0 (tol={atol:.1e})") raise ValueError(f"dc_sigma root not close to 0 (tol={atol:.1e})") dc_mean = np.exp(mu + sigma**2 / 2) detector.update({"dc_mean": dc_mean * u.ct / u.s})