[docs]classGridCoords:r""" The GridCoords object uses desired image dimensions (via the ``cell_size`` and ``npix`` arguments) to define a corresponding Fourier plane grid. Parameters ---------- cell_size : float width of a single square pixel in [arcsec] npix : int number of pixels in the width of the image The Fourier grid is defined over the domain :math:`[-u,+u]`, :math:`[-v,+v]`, even though for real images, technically we could use an RFFT grid from :math:`[0,+u]` to :math:`[-v,+v]`. The reason we opt for a full FFT grid in this instance is implementation simplicity. Images (and their corresponding Fourier transform quantities) are represented as two-dimensional arrays packed as ``[y, x]`` and ``[v, u]``. This means that an image with dimensions ``(npix, npix)`` will also have a corresponding FFT Fourier grid with shape ``(npix, npix)``. The native :class:`~mpol.gridding.GridCoords` representation assumes the Fourier grid (and thus image) are laid out as one might normally expect an image (i.e., no ``np.fft.fftshift`` has been applied). After the object is initialized, instance variables can be accessed, for example >>> myCoords = GridCoords(cell_size=0.005, npix=512) >>> myCoords.img_ext :ivar dl: image-plane cell spacing in RA direction (assumed to be positive) [radians] :ivar dm: image-plane cell spacing in DEC direction [radians] :ivar img_ext: The length-4 list of (left, right, bottom, top) expected by routines like ``matplotlib.pyplot.imshow`` in the ``extent`` parameter assuming ``origin='lower'``. Units of [arcsec] :ivar packed_x_centers_2D: 2D array of l increasing, with fftshifted applied [arcseconds]. Useful for directly evaluating some function to create a packed cube. :ivar packed_y_centers_2D: 2D array of m increasing, with fftshifted applied [arcseconds]. Useful for directly evaluating some function to create a packed cube. :ivar sky_x_centers_2D: 2D array of l arranged for evaluating a sky image [arcseconds]. l coordinate increases to the left (as on sky). :ivar sky_y_centers_2D: 2D array of m arranged for evaluating a sky image [arcseconds]. :ivar du: Fourier-plane cell spacing in East-West direction [:math:`\lambda`] :ivar dv: Fourier-plane cell spacing in North-South direction [:math:`\lambda`] :ivar u_centers: 1D array of cell centers in East-West direction [:math:`\lambda`]. :ivar v_centers: 1D array of cell centers in North-West direction [:math:`\lambda`]. :ivar u_edges: 1D array of cell edges in East-West direction [:math:`\lambda`]. :ivar v_edges: 1D array of cell edges in North-South direction [:math:`\lambda`]. :ivar u_bin_min: minimum u edge [:math:`\lambda`] :ivar u_bin_max: maximum u edge [:math:`\lambda`] :ivar v_bin_min: minimum v edge [:math:`\lambda`] :ivar v_bin_max: maximum v edge [:math:`\lambda`] :ivar max_grid: maximum spatial frequency enclosed by Fourier grid [:math:`\lambda`] :ivar vis_ext: length-4 list of (left, right, bottom, top) expected by routines like ``matplotlib.pyplot.imshow`` in the ``extent`` parameter assuming ``origin='lower'``. Units of [:math:`\lambda`] :ivar vis_ext_Mlam: like vis_ext, but in units of [:math:`\mathrm{M}\lambda`]. """def__init__(self,cell_size:float,npix:int):ifnpix<=0ornot(npix%2==0):raiseValueError("Image must have a positive and even number of pixels.")ifcell_size<=0:raiseValueError("cell_size must be a positive real number.")# Imply to users that GridCoords instance is read-only and new instance# is the approach if changing valuesself._cell_size=cell_sizeself._npix=npix# Image relatedself._image_pixel_width=cell_size*const.arcsec# [radians]self._image_centers=self._image_pixel_width*(np.arange(npix)-npix//2)# [radians]# Spatial frequency related# These properties are identical for both u & v and defined here# edges and centers return fftspaced arrays (not packed, though)# All units in [λ]self._uv_pixel_width=1/(npix*self._image_pixel_width)self.uv_edges=self._uv_pixel_width*(np.arange(npix+1)-npix//2-0.5)self._uv_centers=self._uv_pixel_width*(np.arange(npix)-npix//2)self._min_uv=float(self.uv_edges.min())self._max_uv=float(self.uv_edges.max())def__repr__(self):returnf"GridCoords(cell_size={self.cell_size:.2e}, npix={self.npix})"# the output spatial frequencies of the FFT routine@propertydefcell_size(self)->float:returnself._cell_size@propertydefnpix(self)->int:returnself._npix@propertydefdl(self)->float:returnself._image_pixel_width# [radians]@propertydefdm(self)->float:returnself._image_pixel_width# [radians]@propertydefl_centers(self)->npt.NDArray[np.floating[Any]]:returnself._image_centers@propertydefm_centers(self)->npt.NDArray[np.floating[Any]]:returnself._image_centers@propertydefnpix_u(self)->int:returnself.npix@propertydefnpix_v(self)->int:returnself.npix@propertydefdu(self)->float:returnself._uv_pixel_width@propertydefdv(self)->float:returnself._uv_pixel_width@propertydefu_edges(self)->npt.NDArray[np.floating[Any]]:returnself.uv_edges@propertydefv_edges(self)->npt.NDArray[np.floating[Any]]:returnself.uv_edges@propertydefu_centers(self)->npt.NDArray[np.floating[Any]]:returnself._uv_centers@propertydefv_centers(self)->npt.NDArray[np.floating[Any]]:returnself._uv_centers@propertydefu_bin_min(self)->float:returnself._min_uv@propertydefv_bin_min(self)->float:returnself._min_uv@propertydefu_bin_max(self)->float:returnself._max_uv@propertydefv_bin_max(self)->float:returnself._max_uv@propertydefmax_uv_grid_value(self)->float:returnself._max_uv@propertydefimg_ext(self)->list[float]:# calculate the image extent# say we had 10 pixels representing centers -5, -4, -3, ...# it should go from -5.5 to +4.5lmax=self.cell_size*(self.npix//2-0.5)lmin=-self.cell_size*(self.npix//2+0.5)return[lmax,lmin,lmin,lmax]# arcsecs@propertydefvis_ext(self)->list[float]:return[self.u_bin_min,self.u_bin_max,self.v_bin_min,self.v_bin_max,]# [λ]@propertydefvis_ext_Mlam(self)->list[float]:return[1e-6*edgeforedgeinself.vis_ext]@cached_propertydefground_u_centers_2D(self)->npt.NDArray[np.floating[Any]]:# only useful for plotting a sky_vis# uu increasing, no fftshift# tile replicates the 1D u_centers array to a 2D array the size of the full# UV gridreturnnp.tile(self.u_centers,(self.npix_u,1))@cached_propertydefground_v_centers_2D(self)->npt.NDArray[np.floating[Any]]:# only useful for plotting a sky_vis# vv increasing, no fftshiftreturnnp.tile(self.v_centers,(self.npix_v,1)).T@cached_propertydefpacked_u_centers_2D(self)->npt.NDArray[np.floating[Any]]:# for evaluating a packed vis# uu increasing, fftshiftedreturnnp_fft.fftshift(self.ground_u_centers_2D)@cached_propertydefpacked_v_centers_2D(self)->npt.NDArray[np.floating[Any]]:# for evaluating a packed vis# vv increasing + fftshiftedreturnnp_fft.fftshift(self.ground_v_centers_2D)@cached_propertydefground_q_centers_2D(self)->npt.NDArray[np.floating[Any]]:returnnp.sqrt(self.ground_u_centers_2D**2+self.ground_v_centers_2D**2)# [kλ]@cached_propertydefsky_phi_centers_2D(self)->npt.NDArray[np.floating[Any]]:# https://en.wikipedia.org/wiki/Atan2returnnp.arctan2(# type: ignoreself.ground_v_centers_2D,self.ground_u_centers_2D)# (pi, pi]@cached_propertydefpacked_q_centers_2D(self)->npt.NDArray[np.floating[Any]]:# for evaluating a packed vis in polar coordinates# q increasing, fftshiftedreturnnp_fft.fftshift(self.ground_q_centers_2D)@cached_propertydefpacked_phi_centers_2D(self)->npt.NDArray[np.floating[Any]]:# for evaluating a packed vis in polar coordinates# phi increasing, fftshiftedreturnnp_fft.fftshift(self.sky_phi_centers_2D)@cached_propertydefq_max(self)->float:# outer edge [lambda]returnfloat(np.abs(self.packed_q_centers_2D).max()+np.sqrt(2)*self.du)@cached_propertydefx_centers_2D(self)->npt.NDArray[np.floating[Any]]:returnnp.tile(self.l_centers/const.arcsec,(self.npix,1))# [arcsec]@cached_propertydefy_centers_2D(self)->npt.NDArray[np.floating[Any]]:returnnp.tile(self.m_centers/const.arcsec,(self.npix,1)).T# [arcsec]@cached_propertydefpacked_x_centers_2D(self)->npt.NDArray[np.floating[Any]]:returnnp.fft.fftshift(self.x_centers_2D)# [arcsec]@cached_propertydefpacked_y_centers_2D(self)->npt.NDArray[np.floating[Any]]:returnnp.fft.fftshift(self.y_centers_2D)# [arcsec]@propertydefsky_x_centers_2D(self)->npt.NDArray[np.floating[Any]]:# for evaluating a sky image# ll mirrored, increasing, no fftshiftreturnnp.fliplr(self.x_centers_2D)# [arcsec]@propertydefsky_y_centers_2D(self)->npt.NDArray[np.floating[Any]]:# for evaluating a sky image# mm increasing, no fftshiftreturnself.y_centers_2D# [arcsec]
[docs]defcheck_data_fit(self,uu:torch.Tensor|npt.NDArray[np.floating[Any]],vv:torch.Tensor|npt.NDArray[np.floating[Any]],)->bool:r""" Test whether loose data visibilities fit within the Fourier grid defined by cell_size and npix. Parameters ---------- uu : :class:`torch.Tensor` u spatial frequency coordinates. Units of [:math:`\lambda`] vv : :class:`torch.Tensor` v spatial frequency coordinates. Units of [:math:`\lambda`] Returns ------- bool ``True`` if all visibilities fit within the Fourier grid defined by ``[u_bin_min, u_bin_max, v_bin_min, v_bin_max]``. Otherwise a :class:`mpol.exceptions.CellSizeError` is raised on the first violated boundary. """# we need this routine to work with both numpy.ndarray or torch.Tensor# because it is called for DirtyImager setup (numpy only)# so we'll cast to tensor as a precautionuu=torch.as_tensor(uu)vv=torch.as_tensor(vv)# max freq in datasetmax_uu_vv=np.abs(np.concatenate([uu,vv])).max()max_uu_vv=torch.max(torch.abs(torch.concatenate([uu,vv]))).item()# max freq needed to support datasetmax_cell_size=get_maximum_cell_size(max_uu_vv)iftorch.max(torch.abs(uu))>self.max_uv_grid_value:raiseCellSizeError("Dataset contains uu spatial frequency measurements larger ""than those in the proposed model image. "f"Decrease cell_size below {max_cell_size} arcsec.")iftorch.max(torch.abs(vv))>self.max_uv_grid_value:raiseCellSizeError("Dataset contains vv spatial frequency measurements larger ""than those in the proposed model image. "f"Decrease cell_size below {max_cell_size} arcsec.")returnTrue
def__eq__(self,other:Any)->bool:ifnotisinstance(other,GridCoords):# don't attempt to compare against different typesreturnNotImplemented# GridCoords objects are considered equal if they have the same cell_size and# npix, since all other attributes are derived from these two core properties.returnbool(self.cell_size==other.cell_sizeandself.npix==other.npix)