Source code for exosim.tasks.instrument.createOversampledIntrapixelResponseFunction
import astropy.units as u
import numpy as np
from exosim.tasks.instrument.createIntrapixelResponseFunction import (
CreateIntrapixelResponseFunction,
)
[docs]class CreateOversampledIntrapixelResponseFunction(
CreateIntrapixelResponseFunction
):
[docs] def model(self, parameters):
"""
This class produces an oversampled version of the intrapixel response function.
This kernel is zero-padded to the size of the PSF.
This version is compatible with :func:`exosim.utils.convolution.fast_convolution` convolution function.
Estimate the detector pixel response function with the prescription of
Barron et al., PASP, 119, 466-475 (2007).
Parameters
----------
oversampling: int
number of samples in each resolving element. The
final shape of the response function would be shape*osf
delta_pix: :class:`astropy.units.Quantity`
Phisical size of the detector pixel in microns
diffusion_length: :class:`astropy.units.Quantity`
diffusion length in microns
intra_pix_distance: :class:`astropy.units.Quantity`
distance between two adjacent detector pixels
in microns
Returns
-------
kernel : 2D array
the kernel image
kernel_delta : :class:`astropy.units.Quantity`
the kernel sampling interval in microns
"""
if "oversampling" in parameters["detector"].keys():
osf = (
8 * parameters["detector"]["oversampling"]
) # activate for old convolution
else:
osf = 7 # activate for old convolution
# TODO find a better zeroing method. It should be enought if the zeros are twice the size of the kernel.
psf_shape = parameters["psf_shape"]
delta = parameters["detector"]["delta_pix"]
if "diffusion_length" in parameters["detector"].keys():
lx = parameters["detector"]["diffusion_length"]
else:
lx = 0.0 * u.um
if "intra_pix_distance" in parameters["detector"].keys():
ipd = parameters["detector"]["intra_pix_distance"]
else:
ipd = 0 * u.um
if type(osf) != int:
osf = int(osf)
lx += 1e-20 * u.um # to avoid problems if user pass lx=0
lx = lx.to(delta.unit)
kernel = np.zeros((psf_shape[0] * osf, psf_shape[1] * osf))
self.debug("kernel size: {}".format(kernel.shape))
# prepare the kernel stamp grid
kernel_delta = delta / osf
yc, xc = np.array(kernel.shape) // 2
yy = (np.arange(-1 * osf, 1 * osf)) * kernel_delta
xx = (np.arange(-1 * osf, 1 * osf)) * kernel_delta
# inverse mask to select every other pixel but the central one
i_mask_xx = np.where(np.abs(xx) > 0.5 * (delta - ipd))
i_mask_yy = np.where(np.abs(yy) > 0.5 * (delta - ipd))
xx, yy = np.meshgrid(xx, yy)
# compute kernel stamp
kernel_stamp = np.arctan(
np.tanh((0.5 * (0.5 * delta - xx) / lx).value)
) - np.arctan(np.tanh((0.5 * (-0.5 * delta - xx) / lx).value))
kernel_stamp *= np.arctan(
np.tanh((0.5 * (0.5 * delta - yy) / lx).value)
) - np.arctan(np.tanh((0.5 * (-0.5 * delta - yy) / lx).value))
# set the unused area of kernel stamp to zero
kernel_stamp[i_mask_yy, ...] = 0.0
kernel_stamp[..., i_mask_xx] = 0.0
# Normalise the kernel such that the pixel has QE=1
kernel_stamp *= osf * osf / kernel_stamp.sum()
# put back the kernel stamp
x_off = kernel.shape[1] // 2 - kernel_stamp.shape[1] // 2
y_off = kernel.shape[0] // 2 - kernel_stamp.shape[0] // 2
kernel[
y_off : y_off + kernel_stamp.shape[0],
x_off : x_off + kernel_stamp.shape[1],
] = kernel_stamp
# roll to use for the fast fft convolve
kernel = np.roll(kernel, -xc, axis=1)
kernel = np.roll(kernel, -yc, axis=0)
return kernel, kernel_delta
# Author note: previous version was computing the pixel irf shape on the full kernel and then setting to 0 all other pixels.
# That method resulted in a very slow computation of arctan and huge memory requiring for high osf.
# This "kernel stamp" solution return the same kernel (with relative differences under 1e-8 - tested on 2022/05/19)
# but very lower computing time.
# TEST: using osf 12 psf =(64,64): previous method took 4.2s, new method takes 6ms with)
# This method removes the memory problem: not much of RAM used for this computation
# tested on jupyter notebook on 2022/05/19. L.V.M.