import copy
import numpy as np
from typing import List
import autoarray as aa
from autoarray.dataset.imaging.simulator import SimulatorImaging
from autocti.charge_injection.imaging.imaging import ImagingCI
from autocti.charge_injection.layout import Layout2DCI
from autocti.clocker.two_d import Clocker2D
from autocti.extract.settings import SettingsExtract
from autocti.model.model_util import CTI2D
from typing import Optional
[docs]class SimulatorImagingCI(SimulatorImaging):
[docs] def __init__(
self,
pixel_scales: aa.type.PixelScales,
norm: float,
max_norm: float = np.inf,
column_sigma: Optional[float] = None,
row_slope: Optional[float] = 0.0,
non_uniform_norm_limit=None,
read_noise: Optional[float] = None,
charge_noise: Optional[float] = None,
noise_if_add_noise_false: float = 0.1,
noise_seed: int = -1,
ci_seed: int = -1,
):
"""A class representing a Imaging observation, using the shape of the image, the pixel scale,
psf, exposure time, etc.
Parameters
----------
exposure_time_map
The exposure time of an observation using this data_type.
"""
super().__init__(
exposure_time=1.0,
noise_if_add_noise_false=noise_if_add_noise_false,
noise_seed=noise_seed,
)
self.pixel_scales = pixel_scales
self.norm = norm
self.max_norm = max_norm
self.column_sigma = column_sigma
self.row_slope = row_slope
self.non_uniform_norm_limit = non_uniform_norm_limit
self.read_noise = read_noise
self.charge_noise = charge_noise
self.ci_seed = ci_seed
@property
def _ci_seed(self) -> int:
if self.ci_seed == -1:
return np.random.randint(0, int(1e9))
return self.ci_seed
def median_list_from(self, total_columns: int) -> List[float]:
np.random.seed(self._ci_seed)
injection_norm_list = []
for column_number in range(total_columns):
injection_norm = 0
while injection_norm <= 0 or injection_norm >= self.max_norm:
injection_norm = np.random.normal(self.norm, self.column_sigma)
injection_norm_list.append(injection_norm)
return injection_norm_list
def injection_norm_list_with_limit_from(self, total_columns: int) -> List[float]:
injection_norm_list = self.median_list_from(
total_columns=self.non_uniform_norm_limit
)
injection_norm_limited_list = []
for i in range(total_columns):
injection_norm = np.random.choice(injection_norm_list)
injection_norm_limited_list.append(injection_norm)
return injection_norm_limited_list
def pre_cti_data_uniform_from(self, layout: Layout2DCI) -> aa.Array2D:
"""
Use this charge injection layout_ci to generate a pre-cti charge injection image. This is performed by \
going to its charge injection regions and adding the charge injection normalization value.
Parameters
----------
shape_native
The image_shape of the pre_cti_datas to be created.
"""
return layout.pre_cti_data_uniform_from(
norm=self.norm, pixel_scales=self.pixel_scales
)
def pre_cti_data_non_uniform_from(self, layout: Layout2DCI) -> aa.Array2D:
"""
Use this charge injection layout to generate a pre-cti charge injection image. This is performed by going
to its charge injection regions and adding an input normalization value to each column, which are
passed in as a list.
For one column of a non-uniform charge injection pre-cti image, this function assumes that each non-uniform
charge injection region has the same overall normalization value. This assumes the spikes / troughs in the
injection current that cause non-uniformity occur in an identical fashion for each charge injection region.
Non-uniformity across the rows of a charge injection layout_ci is due to a drop-off in voltage in the current.
Therefore, it appears smooth and be modeled as an analytic function, which this code assumes is a
power-law with slope `row_slope`.
Parameters
----------
shape_native
The image_shape of the pre_cti_datas to be created.
row_slope
The power-law slope of non-uniformity in the row charge injection profile.
ci_seed
Input ci_seed for the random number generator to give reproducible results. A new ci_seed is always used for each \
pre_cti_datas, ensuring each non-uniform ci_region has the same column non-uniformity layout_ci.
"""
for region in layout.region_list:
if self.non_uniform_norm_limit is None:
injection_norm_list = self.median_list_from(
total_columns=region.total_columns
)
else:
injection_norm_list = self.injection_norm_list_with_limit_from(
total_columns=region.total_columns
)
return layout.pre_cti_data_non_uniform_from(
injection_norm_list=injection_norm_list,
pixel_scales=self.pixel_scales,
row_slope=self.row_slope,
)
def via_layout_from(
self,
layout: Layout2DCI,
clocker: Optional[Clocker2D],
cti: Optional[CTI2D],
cosmic_ray_map: Optional[aa.Array2D] = None,
) -> ImagingCI:
"""Simulate a charge injection image, including effects like noises.
Parameters
----------
pre_cti_data
cosmic_ray_map
The dimensions of the output simulated charge injection image.
frame_geometry
The quadrant geometry of the simulated image, defining where the parallel / serial overscans are and \
therefore the direction of clocking and rotations before input into the cti algorithm.
layout
The charge injection layout_ci (regions, normalization, etc.) of the charge injection image.
cti_params
The CTI model parameters (trap density, trap release_timescales etc.).
clocker
The settings that control the cti clocking algorithm (e.g. ccd well_depth express option).
read_noise
The FWHM of the Gaussian read-noises added to the image.
noise_seed
Seed for the read-noises added to the image.
"""
if self.column_sigma is not None:
pre_cti_data = self.pre_cti_data_non_uniform_from(layout=layout)
else:
pre_cti_data = self.pre_cti_data_uniform_from(layout=layout)
return self.via_pre_cti_data_from(
pre_cti_data=pre_cti_data.native,
layout=layout,
clocker=clocker,
cti=cti,
cosmic_ray_map=cosmic_ray_map,
)
def via_pre_cti_data_from(
self,
pre_cti_data: aa.Array2D,
layout: Layout2DCI,
clocker: Optional[Clocker2D],
cti: Optional[CTI2D],
cosmic_ray_map: Optional[aa.Array2D] = None,
) -> ImagingCI:
pre_cti_data = pre_cti_data.native
if cosmic_ray_map is not None:
pre_cti_data += cosmic_ray_map.native
if self.charge_noise is not None:
pre_cti_data = layout.extract.parallel_fpr.add_gaussian_noise_to(
array=pre_cti_data,
noise_sigma=self.charge_noise,
noise_seed=self.noise_seed,
settings=SettingsExtract(
pixels_from_end=layout.extract.parallel_fpr.total_rows_min
),
)
if cti is not None:
post_cti_data = clocker.add_cti(data=pre_cti_data, cti=cti)
else:
post_cti_data = copy.copy(pre_cti_data)
if cosmic_ray_map is not None:
pre_cti_data -= cosmic_ray_map.native
return self.via_post_cti_data_from(
post_cti_data=post_cti_data,
pre_cti_data=pre_cti_data,
layout=layout,
cosmic_ray_map=cosmic_ray_map,
)
def via_post_cti_data_from(
self,
post_cti_data: aa.Array2D,
pre_cti_data: aa.Array2D,
layout: Layout2DCI,
cosmic_ray_map: Optional[aa.Array2D] = None,
) -> ImagingCI:
if self.read_noise is not None:
ci_image = aa.preprocess.data_with_gaussian_noise_added(
data=post_cti_data, sigma=self.read_noise, seed=self.noise_seed
)
ci_image = aa.Array2D.no_mask(
values=ci_image, pixel_scales=self.pixel_scales
)
ci_noise_map = (
self.read_noise
* aa.Array2D.ones(
shape_native=layout.shape_2d, pixel_scales=self.pixel_scales
).native
)
else:
ci_image = post_cti_data
ci_noise_map = aa.Array2D.full(
fill_value=self.noise_if_add_noise_false,
shape_native=layout.shape_2d,
pixel_scales=self.pixel_scales,
).native
return ImagingCI(
data=aa.Array2D.no_mask(
values=ci_image.native, pixel_scales=self.pixel_scales
),
noise_map=aa.Array2D.no_mask(
values=ci_noise_map, pixel_scales=self.pixel_scales
),
pre_cti_data=aa.Array2D.no_mask(
values=pre_cti_data.native, pixel_scales=self.pixel_scales
),
cosmic_ray_map=cosmic_ray_map,
layout=layout,
)