Source code for aiapy.psf.psf

"""
Calculate the point spread function (PSF) for the AIA telescopes.
"""

from numpy import asarray

import astropy.units as u

from sunpy.util.decorators import deprecated

from aiapy.psf import jit, lax, np
from aiapy.psf.utils import filter_mesh_parameters
from aiapy.utils.decorators import validate_channel

__all__ = ["calculate_psf", "psf"]


[docs] @u.quantity_input @validate_channel("channel", valid_channels=[94, 131, 171, 193, 211, 304, 335] * u.angstrom) def calculate_psf(channel: u.angstrom, *, use_preflightcore=False, diffraction_orders=None): r""" Calculate the composite PSF for a given channel, including diffraction and core effects. .. note:: This function has been adapted from `aia_calc_psf.pro <https://sohoftp.nascom.nasa.gov/solarsoft/sdo/aia/idl/psf/PRO/aia_calc_psf.pro>`_. .. note:: If the jax package is installed it will be used to accelerate the computation. jax can use CPUs or GPUs. See their `documentation for instructions <https://docs.jax.dev/en/latest/installation.html>`__. The point spread function (PSF) can be modeled as a 2D Gaussian function of the radial distance :math:`r` from the center, .. math:: I(r, \theta) = I_0 \exp\left(\frac{-r^2}{2\sigma^2}\right) where, - :math:`I_0` : the intensity of a diffraction spike - :math:`r` : the radial distance from the center - :math:`\theta = m\lambda/d` - :math:`m` : diffraction order - :math:`\lambda` : the wavelength of light - :math:`\sigma` : width of Gaussian The intensity of a particular diffraction spike, :math:`I_0`, is given by, .. math:: I_0 = \mathrm{sinc}^2\left(\frac{\theta w}{\lambda}\right) where, - :math:`w` : the width of the mesh wires - :math:`d` : spacing between two consecutive mesh wires The PSF for a given filter can then be calculated as, .. math:: \mathrm{PSF} = \sum_{m=-\infty}^{+\infty}I_m(r,\theta) where, in practice, one can approximate the summation by simply summing over a sufficiently large number of diffraction orders. In this case, we sum from :math:`m=--100` to :math:`m=100`. Finally, the composite PSF of the entrance and focal plane filters is given by, .. math:: \mathrm{PSF}_c = \left|\mathcal{F}\left\{ \mathcal{F}\{\mathrm{PSF}_f\} \mathcal{F}\{\mathrm{PSF}_e\} \right\}\right| where :math:`\mathcal{F}` denotes the Fourier transform, :math:`\mathrm{PSF}_f` is the PSF of the focal plane filter, and :math:`\mathrm{PSF}_e` is the PSF of the entrance filter. For a more detailed explanation of the PSF and the above calculation, see [1]_. Parameters ---------- channel : `~astropy.units.Quantity` Wavelength of channel use_preflightcore : `bool`, optional If True, use the pre-flight values of the mesh width diffraction_orders : array-like, optional The diffraction orders to sum over. If None, the full range from -100 to +100 in steps of 1 will be used. Returns ------- `~numpy.ndarray` The composite PSF of the entrance and focal plane filters. See Also -------- filter_mesh_parameters deconvolve References ---------- .. [1] `Grigis, P., Su, Y., Weber M., et al., 2012, AIA PSF Characterization and Deconvolution <https://sohoftp.nascom.nasa.gov/solarsoft/sdo/aia/idl/psf/DOC/psfreport.pdf>`__ """ mesh_info = filter_mesh_parameters(use_preflightcore=use_preflightcore)[channel] diffraction_orders = np.arange(-100, 101, 1) if diffraction_orders is None else np.asarray(diffraction_orders) psf_entrance = _psf(mesh_info, mesh_info["angle_arm"], diffraction_orders, focal_plane=False) psf_focal_plane = _psf(mesh_info, u.Quantity([45.0, -45.0], "deg"), diffraction_orders, focal_plane=True) # Composite PSF of entrance and focal plane PSFs psf = np.abs(np.fft.fft2(np.fft.fft2(psf_entrance) * np.fft.fft2(psf_focal_plane))) # Center PSF at pixel (0,0) psf = np.fft.fftshift(psf) # Normalize by total number of pixels and always return a numpy array return asarray(psf / (psf.shape[0] * psf.shape[1]))
@jit def _calculate_mesh_spikes(x, y, dxs, dys, orders, width, mesh_ratio, cx0, cy0): psf0 = np.zeros((y.shape[0], x.shape[0]), dtype=x.dtype) def order_body(i, acc): m = orders[i] i0 = (np.sinc(m / mesh_ratio) ** 2) * (m != 0) def angle_body(j, inner): dx, dy = dxs[j], dys[j] gx = np.exp(-width * (x - (cx0 + dx * m)) ** 2) gy = np.exp(-width * (y - (cy0 + dy * m)) ** 2) return inner + i0 * (gy[:, None] * gx[None, :]) return lax.fori_loop(0, dxs.shape[0], angle_body, acc) return lax.fori_loop(0, orders.shape[0], order_body, psf0) def _psf(meshinfo, angles, diffraction_orders, *, focal_plane=False): ny, nx = 4096, 4096 width = meshinfo["width"].to_value("pixel") spacing = (meshinfo["spacing_fp"] if focal_plane else meshinfo["spacing_e"]).to_value("pixel") mesh_ratio = (meshinfo["mesh_pitch"] / meshinfo["mesh_width"]).decompose().value # Fractional area not covered by the mesh # Sourced from AIA Instrument Paper # Table 3 Multilayer and filter properties. # The metal filters are supported on a 82% transmitting nickel mesh area_not_mesh = 0.82 # 1-D coordinates and image center x = np.arange(nx) + 0.5 y = np.arange(ny) + 0.5 cx0 = 0.5 * nx + 0.5 cy0 = 0.5 * ny + 0.5 # Per-angle pixel offsets ang = np.asarray(angles.to_value(u.rad)) dxs = spacing * np.cos(ang) dys = spacing * np.sin(ang) orders = np.asarray(diffraction_orders) psf_spikes = _calculate_mesh_spikes(x, y, dxs, dys, orders, width, mesh_ratio, cx0, cy0) # Gaussian core psf_core = np.exp(-width * (x - cx0) ** 2)[:, None] * np.exp(-width * (y - cy0) ** 2)[None, :] psf_spikes = psf_spikes / psf_spikes.sum() psf_core = psf_core / psf_core.sum() return (1.0 - area_not_mesh) * psf_spikes + area_not_mesh * psf_core
[docs] @deprecated("0.11.0", alternative="calculate_psf") @u.quantity_input def psf(channel: u.angstrom, *, use_preflightcore=False, diffraction_orders=None): """ This function is deprecated. Use `aiapy.psf.calculate_psf` instead. """ return calculate_psf( channel, use_preflightcore=use_preflightcore, diffraction_orders=diffraction_orders, )