def test_slices_edges(): """ Test overlap_slices when extracting along edges. """ slc_lg, slc_sm = overlap_slices((10, 10), (3, 3), (1, 1), mode='strict') assert slc_lg[0].start == slc_lg[1].start == 0 assert slc_lg[0].stop == slc_lg[1].stop == 3 assert slc_sm[0].start == slc_sm[1].start == 0 assert slc_sm[0].stop == slc_sm[1].stop == 3 slc_lg, slc_sm = overlap_slices((10, 10), (3, 3), (8, 8), mode='strict') assert slc_lg[0].start == slc_lg[1].start == 7 assert slc_lg[0].stop == slc_lg[1].stop == 10 assert slc_sm[0].start == slc_sm[1].start == 0 assert slc_sm[0].stop == slc_sm[1].stop == 3 # test (0, 0) shape slc_lg, slc_sm = overlap_slices((10, 10), (0, 0), (0, 0)) assert slc_lg[0].start == slc_lg[0].stop == 0 assert slc_lg[1].start == slc_lg[1].stop == 0 assert slc_sm[0].start == slc_sm[0].stop == 0 assert slc_sm[1].start == slc_sm[1].stop == 0 slc_lg, slc_sm = overlap_slices((10, 10), (0, 0), (5, 5)) assert slc_lg[0].start == slc_lg[0].stop == 5 assert slc_lg[1].start == slc_lg[1].stop == 5 assert slc_sm[0].start == slc_sm[0].stop == 0 assert slc_sm[1].start == slc_sm[1].stop == 0
def test_slices_no_overlap(position): """ A ValueError should be raised if position contains a non-finite value. """ with pytest.raises(ValueError): overlap_slices((7, 7), (3, 3), position)
def test_slices_partial_overlap(): '''Compute a slice for partially overlapping arrays.''' temp = overlap_slices((5, ), (3, ), (0, )) assert temp == ((slice(0, 2, None), ), (slice(1, 3, None), )) temp = overlap_slices((5, ), (3, ), (0, ), mode='partial') assert temp == ((slice(0, 2, None), ), (slice(1, 3, None), )) for pos in [0, 4]: with pytest.raises(PartialOverlapError) as e: temp = overlap_slices((5, ), (3, ), (pos, ), mode='strict') assert 'Arrays overlap only partially.' in str(e.value)
def test_slices_partial_overlap(): '''Compute a slice for partially overlapping arrays.''' temp = overlap_slices((5,), (3,), (0,)) assert temp == ((slice(0, 2, None),), (slice(1, 3, None),)) temp = overlap_slices((5,), (3,), (0,), mode='partial') assert temp == ((slice(0, 2, None),), (slice(1, 3, None),)) for pos in [0, 4]: with pytest.raises(PartialOverlapError) as e: temp = overlap_slices((5,), (3,), (pos,), mode='strict') assert 'Arrays overlap only partially.' in str(e.value)
def __init__(self, nddata, position, shape): if isinstance(position, SkyCoord): if nddata.wcs is None: raise ValueError('nddata must contain WCS if the input ' 'position is a SkyCoord') x, y = skycoord_to_pixel(position, nddata.wcs, mode='all') position = (y, x) data = np.asanyarray(nddata.data) print(data.shape, shape, position) slices_large, slices_small = overlap_slices(data.shape, shape, position) self.slices_large = slices_large self.slices_small = slices_small data = nddata.data[slices_large] mask = None uncertainty = None if nddata.mask is not None: mask = nddata.mask[slices_large] if nddata.uncertainty is not None: uncertainty = nddata.uncertainty[slices_large] self.nddata = NDData(data, mask=mask, uncertainty=uncertainty)
def cutout_slices(self, geom, mode="partial"): """Compute cutout slices. Parameters ---------- geom : `WcsGeom` Parent geometry mode : {"trim", "partial", "strict"} Cutout slices mode. Returns ------- slices : dict Dictionary containing "parent-slices" and "cutout-slices". """ position = geom.to_image().coord_to_pix(self.center_skydir) slices = overlap_slices( large_array_shape=geom.data_shape[-2:], small_array_shape=self.data_shape[-2:], position=position[::-1], mode=mode ) return { "parent-slices": slices[0], "cutout-slices": slices[1], }
def insert_fake_source(image, source, x0, y0, flux, slicesout=False): '''Insert source with some scaling into image at position x0, y0 ``image`` will be modified in place. Pass in a copy if the original should be preserved. ''' slice_large, slice_small = overlap_slices(image.shape[:2], source.shape, (y0, x0), 'trim') image[slice_large] = image[slice_large] + flux / source.sum( ) * source[slice_small] if slicesout: return image, slice_large, slice_small else: return image
def insert_in_shape(self, array, shape, fill_value=True, dtype=np.float): ''' Insert the cut down mask into the given shape. ''' full_size = np.ones(shape, dtype=dtype) * fill_value large_slices, small_slices = \ overlap_slices(shape, array.shape, self._center_coords, mode='partial') full_size[large_slices] = array[small_slices] return full_size
def __init__(self, data, position, shape, wcs=None): if isinstance(position, SkyCoord): if wcs is None: raise ValueError('wcs must be input if position is a ' 'SkyCoord') x, y = skycoord_to_pixel(position, wcs, mode='all') position = (y, x) data = np.asanyarray(data) slices_large, slices_small = overlap_slices(data.shape, shape, position) self.slices_large = slices_large self.slices_small = slices_small self.data = data[slices_large] self.requested_position = position self.requested_shape = shape
def nstar(self, image, star_groups): """ Fit, as appropriate, a compound or single model to the given ``star_groups``. Groups are fitted sequentially from the smallest to the biggest. In each iteration, ``image`` is subtracted by the previous fitted group. Parameters ---------- image : numpy.ndarray Background-subtracted image. star_groups : `~astropy.table.Table` This table must contain the following columns: ``id``, ``group_id``, ``x_0``, ``y_0``, ``flux_0``. ``x_0`` and ``y_0`` are initial estimates of the centroids and ``flux_0`` is an initial estimate of the flux. Additionally, columns named as ``<param_name>_0`` are required if any other parameter in the psf model is free (i.e., the ``fixed`` attribute of that parameter is ``False``). Returns ------- result_tab : `~astropy.table.Table` Astropy table that contains photometry results. image : numpy.ndarray Residual image. """ result_tab = Table() for param_tab_name in self._pars_to_output.keys(): result_tab.add_column(Column(name=param_tab_name)) unc_tab = Table() for param, isfixed in self.psf_model.fixed.items(): if not isfixed: unc_tab.add_column(Column(name=param + "_unc")) y, x = np.indices(image.shape) star_groups = star_groups.group_by('group_id') for n in range(len(star_groups.groups)): group_psf = get_grouped_psf_model(self.psf_model, star_groups.groups[n], self._pars_to_set) usepixel = np.zeros_like(image, dtype=bool) for row in star_groups.groups[n]: usepixel[overlap_slices(large_array_shape=image.shape, small_array_shape=self.fitshape, position=(row['y_0'], row['x_0']), mode='trim')[0]] = True fit_model = self.fitter(group_psf, x[usepixel], y[usepixel], image[usepixel]) param_table = self._model_params2table(fit_model, len(star_groups.groups[n])) result_tab = vstack([result_tab, param_table]) param_cov = self.fitter.fit_info.get('param_cov', None) if param_cov is not None: unc_tab = vstack([unc_tab, self._get_uncertainties( len(star_groups.groups[n]))]) # do not subtract if the fitting did not go well try: image = subtract_psf(image, self.psf_model, param_table, subshape=self.fitshape) except NoOverlapError: pass if param_cov is not None: result_tab = hstack([result_tab, unc_tab]) return result_tab, image
def _fit_star(self, epsf, star, fitter, fitter_kwargs, fitter_has_fit_info, fit_boxsize): """ Fit an ePSF model to a single star. The input ``epsf`` will usually be modified by the fitting routine in this function. Make a copy before calling this function if the original is needed. """ if fit_boxsize is not None: try: xcenter, ycenter = star.cutout_center large_slc, small_slc = overlap_slices(star.shape, fit_boxsize, (ycenter, xcenter), mode='strict') except (PartialOverlapError, NoOverlapError): warnings.warn( 'The star at ({0}, {1}) cannot be fit because ' 'its fitting region extends beyond the star ' 'cutout image.'.format(star.center[0], star.center[1]), AstropyUserWarning) star = copy.deepcopy(star) star._fit_error_status = 1 return star data = star.data[large_slc] weights = star.weights[large_slc] # define the origin of the fitting region x0 = large_slc[1].start y0 = large_slc[0].start else: # use the entire cutout image data = star.data weights = star.weights # define the origin of the fitting region x0 = 0 y0 = 0 x_oversamp = star.pixel_scale[0] / epsf.pixel_scale[0] y_oversamp = star.pixel_scale[1] / epsf.pixel_scale[1] scaled_data = data / (x_oversamp * y_oversamp) # define positions in the ePSF oversampled grid yy, xx = np.indices(data.shape, dtype=np.float) xx = (xx - (star.cutout_center[0] - x0)) * x_oversamp yy = (yy - (star.cutout_center[1] - y0)) * y_oversamp # define the initial guesses for fitted flux and shifts epsf.flux = star.flux epsf.x_0 = 0.0 epsf.y_0 = 0.0 # The oversampling factor is used in the FittableImageModel # evaluate method (which is use when fitting). We do not want # to use oversampling here because it has been set by the ratio # of the ePSF and EPSFStar pixel scales. This allows for # oversampling factors that differ between stars and also for # the factor to be different along the x and y axes. epsf._oversampling = 1. try: fitted_epsf = fitter(model=epsf, x=xx, y=yy, z=scaled_data, weights=weights, **fitter_kwargs) except TypeError: # fitter doesn't support weights fitted_epsf = fitter(model=epsf, x=xx, y=yy, z=scaled_data, **fitter_kwargs) fit_error_status = 0 if fitter_has_fit_info: fit_info = copy.copy(fitter.fit_info) if 'ierr' in fit_info and fit_info['ierr'] not in [1, 2, 3, 4]: fit_error_status = 2 # fit solution was not found else: fit_info = None # compute the star's fitted position x_center = (star.cutout_center[0] + (fitted_epsf.x_0.value / x_oversamp)) y_center = (star.cutout_center[1] + (fitted_epsf.y_0.value / y_oversamp)) star = copy.deepcopy(star) star.cutout_center = (x_center, y_center) # set the star's flux to the ePSF-fitted flux star.flux = fitted_epsf.flux.value star._fit_info = fit_info star._fit_error_status = fit_error_status return star
def _recenter_epsf(self, epsf_data, epsf, centroid_func=centroid_com, box_size=5, maxiters=20, center_accuracy=1.0e-4): """ Calculate the center of the ePSF data and shift the data so the ePSF center is at the center of the ePSF data array. Parameters ---------- epsf_data : 2D `~numpy.ndarray` A 2D array containing the ePSF image. epsf : `EPSFModel` object The ePSF model. centroid_func : callable, optional A callable object (e.g. function or class) that is used to calculate the centroid of a 2D array. The callable must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally an ``error`` keyword. The callable object must return a tuple of two 1D `~numpy.ndarray`\\s, representing the x and y centroids. The default is `~photutils.centroids.centroid_com`. recentering_boxsize : float or tuple of two floats, optional The size (in pixels) of the box used to calculate the centroid of the ePSF during each build iteration. If a single integer number is provided, then a square box will be used. If two values are provided, then they should be in ``(ny, nx)`` order. The default is 5. maxiters : int, optional The maximum number of recentering iterations to perform. The default is 20. center_accuracy : float, optional The desired accuracy for the centers of stars. The building iterations will stop if the center of the ePSF changes by less than ``center_accuracy`` pixels between iterations. The default is 1.0e-4. Returns ------- result : 2D `~numpy.ndarray` The recentered ePSF data. """ # Define an EPSFModel for the input data. This EPSFModel will be # used to evaluate the model on a shifted pixel grid to place the # centroid at the array center. epsf = EPSFModel(data=epsf_data, origin=epsf.origin, normalize=False, oversampling=epsf.oversampling) epsf.fill_value = 0.0 xcenter, ycenter = epsf.origin dx_total = 0 dy_total = 0 y, x = np.indices(epsf_data.shape, dtype=np.float) iter_num = 0 center_accuracy_sq = center_accuracy ** 2 center_dist_sq = center_accuracy_sq + 1.e6 center_dist_sq_prev = center_dist_sq + 1 while (iter_num < maxiters and center_dist_sq >= center_accuracy_sq): iter_num += 1 # extract a cutout from the ePSF slices_large, slices_small = overlap_slices(epsf_data.shape, box_size, (ycenter, xcenter)) epsf_cutout = epsf_data[slices_large] mask = ~np.isfinite(epsf_cutout) # find a new center position xcenter_new, ycenter_new = centroid_func(epsf_cutout, mask=mask) xcenter_new += slices_large[1].start ycenter_new += slices_large[0].start # calculate the shift dx = xcenter - xcenter_new dy = ycenter - ycenter_new center_dist_sq = dx**2 + dy**2 if center_dist_sq >= center_dist_sq_prev: # don't shift break center_dist_sq_prev = center_dist_sq # Resample the ePSF data to a shifted grid to place the # centroid in the center of the central pixel. The shift is # always performed on the input epsf_data. dx_total += dx # accumulated shifts for the input epsf_data dy_total += dy epsf_data = epsf.evaluate(x=x, y=y, flux=1.0, x_0=xcenter + dx_total, y_0=ycenter + dy_total, use_oversampling=False) return epsf_data
def _extract_stars(data, catalog, size=(11, 11), use_xy=True): """ Extract cutout images from a single image centered on stars defined in the single input catalog. Parameters ---------- data : `~astropy.nddata.NDData` A `~astropy.nddata.NDData` object containing the 2D image from which to extract the stars. If the input ``catalog`` contains only the sky coordinates (i.e., not the pixel coordinates) of the stars then the `~astropy.nddata.NDData` object must have a valid ``wcs`` attribute. catalogs : `~astropy.table.Table` A single catalog of sources to be extracted from the input ``data``. The center of each source can be defined either in pixel coordinates (in ``x`` and ``y`` columns) or sky coordinates (in a ``skycoord`` column containing a `~astropy.coordinates.SkyCoord` object). If both are specified, then the value of the ``use_xy`` keyword determines which coordinates will be used. size : int or array_like (int), optional The extraction box size along each axis. If ``size`` is a scalar then a square box of size ``size`` will be used. If ``size`` has two elements, they should be in ``(ny, nx)`` order. The size must be greater than or equal to 3 pixel for both axes. Size must be odd in both axes; if either is even, it is padded by one to force oddness. use_xy : bool, optional Whether to use the ``x`` and ``y`` pixel positions when both pixel and sky coordinates are present in the input catalog table. If `False` then sky coordinates are used instead of pixel coordinates (e.g., for linked stars). The default is `True`. Returns ------- stars : list of `EPSFStar` objects A list of `EPSFStar` instances containing the extracted stars. """ # Force size to odd numbers such that there is always a central pixel with # even spacing either side of the pixel. if np.isscalar(size): size = size + 1 if size % 2 == 0 else size else: size = tuple(_size + 1 if _size % 2 == 0 else _size for _size in size) colnames = catalog.colnames if ('x' not in colnames or 'y' not in colnames) or not use_xy: try: xcenters, ycenters = data.wcs.world_to_pixel(catalog['skycoord']) except AttributeError: # for Astropy < 3.1 WCS support xcenters, ycenters = skycoord_to_pixel(catalog['skycoord'], data.wcs, origin=0, mode='all') else: xcenters = catalog['x'].data.astype(float) ycenters = catalog['y'].data.astype(float) if 'id' in colnames: ids = catalog['id'] else: ids = np.arange(len(catalog), dtype=int) + 1 if data.uncertainty is None: weights = np.ones_like(data.data) else: if data.uncertainty.uncertainty_type == 'weights': weights = np.asanyarray(data.uncertainty.array, dtype=float) else: warnings.warn( 'The data uncertainty attribute has an unsupported ' 'type. Only uncertainty_type="weights" can be ' 'used to set weights. Weights will be set to 1.', AstropyUserWarning) weights = np.ones_like(data.data) if data.mask is not None: weights[data.mask] = 0. stars = [] for xcenter, ycenter, obj_id in zip(xcenters, ycenters, ids): try: large_slc, _ = overlap_slices(data.data.shape, size, (ycenter, xcenter), mode='strict') data_cutout = data.data[large_slc] weights_cutout = weights[large_slc] except (PartialOverlapError, NoOverlapError): stars.append(None) continue origin = (large_slc[1].start, large_slc[0].start) cutout_center = (xcenter - origin[0], ycenter - origin[1]) star = EPSFStar(data_cutout, weights_cutout, cutout_center=cutout_center, origin=origin, wcs_large=data.wcs, id_label=obj_id) stars.append(star) return stars
def _fit_star(self, epsf, star, fitter, fitter_kwargs, fitter_has_fit_info, fit_boxsize): """ Fit an ePSF model to a single star. The input ``epsf`` will usually be modified by the fitting routine in this function. Make a copy before calling this function if the original is needed. """ if fit_boxsize is not None: try: xcenter, ycenter = star.cutout_center large_slc, _ = overlap_slices(star.shape, fit_boxsize, (ycenter, xcenter), mode='strict') except (PartialOverlapError, NoOverlapError): warnings.warn( 'The star at ({0}, {1}) cannot be fit because ' 'its fitting region extends beyond the star ' 'cutout image.'.format(star.center[0], star.center[1]), AstropyUserWarning) star = copy.deepcopy(star) star._fit_error_status = 1 return star data = star.data[large_slc] weights = star.weights[large_slc] # define the origin of the fitting region x0 = large_slc[1].start y0 = large_slc[0].start else: # use the entire cutout image data = star.data weights = star.weights # define the origin of the fitting region x0 = 0 y0 = 0 # Define positions in the undersampled grid. The fitter will # evaluate on the defined interpolation grid, currently in the # range [0, len(undersampled grid)]. yy, xx = np.indices(data.shape, dtype=np.float) xx = xx + x0 - star.cutout_center[0] yy = yy + y0 - star.cutout_center[1] # define the initial guesses for fitted flux and shifts epsf.flux = star.flux epsf.x_0 = 0.0 epsf.y_0 = 0.0 try: fitted_epsf = fitter(model=epsf, x=xx, y=yy, z=data, weights=weights, **fitter_kwargs) except TypeError: # fitter doesn't support weights fitted_epsf = fitter(model=epsf, x=xx, y=yy, z=data, **fitter_kwargs) fit_error_status = 0 if fitter_has_fit_info: fit_info = copy.copy(fitter.fit_info) if 'ierr' in fit_info and fit_info['ierr'] not in [1, 2, 3, 4]: fit_error_status = 2 # fit solution was not found else: fit_info = None # compute the star's fitted position x_center = star.cutout_center[0] + fitted_epsf.x_0.value y_center = star.cutout_center[1] + fitted_epsf.y_0.value star = copy.deepcopy(star) star.cutout_center = (x_center, y_center) # set the star's flux to the ePSF-fitted flux star.flux = fitted_epsf.flux.value star._fit_info = fit_info star._fit_error_status = fit_error_status return star
def cutout_footprint(data, position, box_size=3, footprint=None, mask=None, error=None): """ Cut out a region from data (and optional mask and error) centered at specified (x, y) position. The size of the region is specified via the ``box_size`` or ``footprint`` keywords. The output mask for the cutout region represents the combination of the input mask and footprint mask. Parameters ---------- data : array_like The 2D array of the image. position : 2 tuple The ``(x, y)`` pixel coordinate of the center of the region. box_size : scalar or tuple, optional The size of the region to cutout from ``data``. If ``box_size`` is a scalar then a square box of size ``box_size`` will be used. If ``box_size`` has two elements, they should be in ``(ny, nx)`` order. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. footprint : `~numpy.ndarray` of bools, optional A boolean array where `True` values describe the local footprint region. ``box_size=(n, m)`` is equivalent to ``footprint=np.ones((n, m))``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. mask : array_like, bool, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. Returns ------- region_data : `~numpy.ndarray` The ``data`` cutout. region_mask : `~numpy.ndarray` The ``mask`` cutout. region_error : `~numpy.ndarray` The ``error`` cutout. slices : tuple of slices Slices in each dimension of the ``data`` array used to define the cutout region. """ if len(position) != 2: raise ValueError('position must have a length of 2') if footprint is None: if box_size is None: raise ValueError('box_size or footprint must be defined.') if not isinstance(box_size, collections.Iterable): shape = (box_size, box_size) else: if len(box_size) != 2: raise ValueError('box_size must have a length of 2') shape = box_size footprint = np.ones(shape, dtype=bool) else: footprint = np.asanyarray(footprint, dtype=bool) slices_large, slices_small = overlap_slices(data.shape, footprint.shape, position[::-1]) region_data = data[slices_large] if error is not None: region_error = error[slices_large] else: region_error = None if mask is not None: region_mask = mask[slices_large] else: region_mask = np.zeros_like(region_data, dtype=bool) footprint_mask = ~footprint footprint_mask = footprint_mask[slices_small] # trim if necessary region_mask = np.logical_or(region_mask, footprint_mask) return region_data, region_mask, region_error, slices_large
def centroid_quadratic(data, xpeak=None, ypeak=None, fit_boxsize=5, search_boxsize=None, mask=None): """ Calculate the centroid of an n-dimensional array by fitting a 2D quadratic polynomial. A second degree 2D polynomial is fit within a small region of the data defined by ``fit_boxsize`` to calculate the centroid position. The initial center of the fitting box can specified using the ``xpeak`` and ``ypeak`` keywords. If both ``xpeak`` and ``ypeak`` are `None`, then the box will be centered at the position of the maximum value in the input ``data``. If ``xmax`` and ``ymax`` are specified, the ``search_boxsize`` optional keyword can be used to further refine the initial center of the fitting box by searching for the position of the maximum pixel within a box of size ``search_boxsize``. Parameters ---------- data : numpy.ndarray Image data. xpeak, ypeak : float or `None`, optional The initial guess of the position of the centroid. When both ``xpeak`` and ``ypeak`` are `None` then the position of the maximum value in the input ``data`` will be used as the initial guess. fit_boxsize : int or tuple of int, optional The size (in pixels) of the box used to define the fitting region. If ``fit_boxsize`` has two elements, they should be in ``(ny, nx)`` order. If ``fit_boxsize`` is a scalar then a square box of size ``fit_boxsize`` will be used. search_boxsize : int or tuple of int, optional The size (in pixels) of the box used to search for the maximum pixel value if ``xpeak`` and ``ypeak`` are both `None`. If ``fit_boxsize`` has two elements, they should be in ``(ny, nx)`` order. If ``fit_boxsize`` is a scalar then a square box of size ``fit_boxsize`` will be used. This parameter is ignored when ``xmax`` and ``ymax`` are both `None`. In that case, the entire array is search for the maximum value. mask : bool `~numpy.ndarray`, optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from calculations. Returns ------- centroid : `~numpy.ndarray` The ``x, y`` coordinates of the centroid. """ if ((xpeak is None and ypeak is not None) or (xpeak is not None and ypeak is None)): raise ValueError('xpeak and ypeak must both be input or "None"') data = np.asanyarray(data, dtype=float) ny, nx = data.shape badmask = ~np.isfinite(data) if np.any(badmask): warnings.warn('Input data contains non-finite values (e.g., NaN or ' 'inf) that were automatically masked.', AstropyUserWarning) data[badmask] = np.nan if mask is not None: if data.shape != mask.shape: raise ValueError('data and mask must have the same shape.') data[mask] = np.nan fit_boxsize = _process_boxsize(fit_boxsize, data.shape) if np.product(fit_boxsize) < 6: raise ValueError('fit_boxsize is too small. 6 values are required ' 'to fit a 2D quadratic polynomial.') if xpeak is None: # and ypeak too yidx, xidx = np.unravel_index(np.nanargmax(data), data.shape) else: xidx = _py2intround(xpeak) yidx = _py2intround(ypeak) if search_boxsize is not None: search_boxsize = _process_boxsize(search_boxsize, data.shape) slc_data, slc_cutout = overlap_slices(data.shape, search_boxsize, (yidx, xidx), mode='trim') cutout = data[slc_data] yidx, xidx = np.unravel_index(np.nanargmax(cutout), cutout.shape) xidx += slc_data[1].start yidx += slc_data[0].start # if peak is at the edge of the data, return the position of the maximum if xidx == 0 or xidx == nx - 1 or yidx == 0 or yidx == ny - 1: warnings.warn('maximum value is at the edge of the data and its ' 'position was returned; no quadratic fit was ' 'performed', AstropyUserWarning) return np.array((xidx, yidx), dtype=float) # extract the fitting region slc_data, slc_cutout = overlap_slices(data.shape, fit_boxsize, (yidx, xidx), mode='trim') xidx0, xidx1 = (slc_data[1].start, slc_data[1].stop) yidx0, yidx1 = (slc_data[0].start, slc_data[0].stop) # shift the fitting box if it was clipped by the data edge if (xidx1 - xidx0) < fit_boxsize[1]: if xidx0 == 0: xidx1 = min(nx, xidx0 + fit_boxsize[1]) if xidx1 == nx: xidx0 = max(0, xidx1 - fit_boxsize[1]) if (yidx1 - yidx0) < fit_boxsize[0]: if yidx0 == 0: yidx1 = min(ny, yidx0 + fit_boxsize[0]) if yidx1 == ny: yidx0 = max(0, yidx1 - fit_boxsize[0]) cutout = data[yidx0:yidx1, xidx0:xidx1].ravel() if np.count_nonzero(~np.isnan(cutout)) < 6: warnings.warn('at least 6 unmasked data points are required to ' 'perform a 2D quadratic fit', AstropyUserWarning) return np.array((np.nan, np.nan)) # fit a 2D quadratic polynomial to the fitting region xi = np.arange(xidx0, xidx1) yi = np.arange(yidx0, yidx1) x, y = np.meshgrid(xi, yi) x = x.ravel() y = y.ravel() coeff_matrix = np.vstack((np.ones_like(x), x, y, x * y, x * x, y * y)).T try: c = np.linalg.lstsq(coeff_matrix, cutout, rcond=None)[0] except np.linalg.LinAlgError: warnings.warn('quadratic fit failed', AstropyUserWarning) return np.array((np.nan, np.nan)) # analytically find the maximum of the polynomial _, c10, c01, c11, c20, c02 = c det = 4 * c20 * c02 - c11**2 if det <= 0 or ((c20 > 0.0 and c02 >= 0.0) or (c20 >= 0.0 and c02 > 0.0)): warnings.warn('quadratic fit does not have a maximum', AstropyUserWarning) return np.array((np.nan, np.nan)) xm = (c01 * c11 - 2.0 * c02 * c10) / det ym = (c10 * c11 - 2.0 * c20 * c01) / det if xm > 0.0 and xm < (nx - 1.0) and ym > 0.0 and ym < (ny - 1.0): xycen = np.array((xm, ym), dtype=float) else: warnings.warn('quadratic polynomial maximum value falls outside ' 'of the image', AstropyUserWarning) return np.array((np.nan, np.nan)) return xycen
def listpixels(data, position, shape, subarray_indices=False, wcs=None): """ Return a `~astropy.table.Table` listing the ``(row, col)`` (``(y, x)``) positions and ``data`` values for a subarray. Given a position of the center of the subarray, with respect to the large array, the array indices and values are returned. This function takes care of the correct behavior at the boundaries, where the small array is appropriately trimmed. Parameters ---------- data : array-like The input data. position : tuple (int) or `~astropy.coordinates.SkyCoord` The position of the subarray center with respect to the data array. The position can be specified either as an integer ``(row, col)`` (``(y, x)``) tuple or a `~astropy.coordinates.SkyCoord`, in which case ``wcs`` is a required input. shape : tuple (int) The integer shape (``(ny, nx)``) of the subarray. subarray_indices : bool, optional If `True` then the returned positions are relative to the small subarray. If `False` (default) then the returned positions are relative to the ``data`` array. wcs : `~astropy.wcs.WCS`, optional The WCS transformation to use if ``position`` is a `~astropy.coordinates.SkyCoord`. Returns ------- table : `~astropy.table.Table` A table containing the ``x`` and ``y`` positions and data values. Notes ----- This function is decorated with `~astropy.nddata.support_nddata` and thus supports `~astropy.nddata.NDData` objects as input. See Also -------- :func:`astropy.nddata.utils.overlap_slices` Examples -------- >>> import numpy as np >>> from imutils import listpixels >>> data = np.arange(625).reshape(25, 25) >>> tbl = listpixels(data, (10, 12), (3, 3)) >>> print(len(tbl)) 3 >>> tbl.pprint(max_lines=-1) x y value --- --- ----- 11 9 236 12 9 237 13 9 238 11 10 261 12 10 262 13 10 263 11 11 286 12 11 287 13 11 288 """ if isinstance(position, SkyCoord): if wcs is None: raise ValueError('wcs must be input if positions is a SkyCoord') x, y = skycoord_to_pixel(position, wcs, mode='all') position = (y, x) data = np.asanyarray(data) slices_large, slices_small = overlap_slices(data.shape, shape, position) slices = slices_large yy, xx = np.mgrid[slices] values = data[yy, xx] if subarray_indices: slices = slices_small yy, xx = np.mgrid[slices] tbl = Table() tbl['x'] = xx.ravel() tbl['y'] = yy.ravel() tbl['value'] = values.ravel() return tbl
def nstar(self, image, star_groups): """ Fit, as appropriate, a compound or single model to the given ``star_groups``. Groups are fitted sequentially from the smallest to the biggest. In each iteration, ``image`` is subtracted by the previous fitted group. Parameters ---------- image : numpy.ndarray Background-subtracted image. star_groups : `~astropy.table.Table` This table must contain the following columns: ``id``, ``group_id``, ``x_0``, ``y_0``, ``flux_0``. ``x_0`` and ``y_0`` are initial estimates of the centroids and ``flux_0`` is an initial estimate of the flux. Additionally, columns named as ``<param_name>_0`` are required if any other parameter in the psf model is free (i.e., the ``fixed`` attribute of that parameter is ``False``). Returns ------- result_tab : `~astropy.table.Table` Astropy table that contains photometry results. image : numpy.ndarray Residual image. """ result_tab = Table() for param_tab_name in self._pars_to_output.keys(): result_tab.add_column(Column(name=param_tab_name)) unc_tab = Table() for param, isfixed in self.psf_model.fixed.items(): if not isfixed: unc_tab.add_column(Column(name=param + "_unc")) y, x = np.indices(image.shape) star_groups = star_groups.group_by('group_id') for n in range(len(star_groups.groups)): group_psf = get_grouped_psf_model(self.psf_model, star_groups.groups[n], self._pars_to_set) usepixel = np.zeros_like(image, dtype=np.bool) for row in star_groups.groups[n]: usepixel[overlap_slices(large_array_shape=image.shape, small_array_shape=self.fitshape, position=(row['y_0'], row['x_0']), mode='trim')[0]] = True fit_model = self.fitter(group_psf, x[usepixel], y[usepixel], image[usepixel]) param_table = self._model_params2table(fit_model, len(star_groups.groups[n])) result_tab = vstack([result_tab, param_table]) if 'param_cov' in self.fitter.fit_info.keys(): unc_tab = vstack([unc_tab, self._get_uncertainties( len(star_groups.groups[n]))]) try: from astropy.nddata.utils import NoOverlapError except ImportError: raise ImportError("astropy 1.1 or greater is required in " "order to use this class.") # do not subtract if the fitting did not go well try: image = subtract_psf(image, self.psf_model, param_table, subshape=self.fitshape) except NoOverlapError: pass if 'param_cov' in self.fitter.fit_info.keys(): result_tab = hstack([result_tab, unc_tab]) return result_tab, image
def test_slices_overlap_wrong_mode(): '''Call overlap_slices with non-existing mode.''' with pytest.raises(ValueError) as e: overlap_slices((5,), (3,), (0,), mode='full') assert "Mode can be only" in str(e.value)
def centroid_sources(data, xpos, ypos, box_size=11, footprint=None, error=None, mask=None, centroid_func=centroid_com): """ Calculate the centroid of sources at the defined positions. A cutout image centered on each input position will be used to calculate the centroid position. The cutout image is defined either using the ``box_size`` or ``footprint`` keyword. The ``footprint`` keyword can be used to create a non-rectangular cutout image. Parameters ---------- data : array_like The 2D array of the image. xpos, ypos : float or array-like of float The initial ``x`` and ``y`` pixel position(s) of the center position. A cutout image centered on this position be used to calculate the centroid. box_size : int or array-like of int, optional The size of the cutout image along each axis. If ``box_size`` is a number, then a square cutout of ``box_size`` will be created. If ``box_size`` has two elements, they should be in ``(ny, nx)`` order. footprint : `~numpy.ndarray` of bools, optional A 2D boolean array where `True` values describe the local footprint region to cutout. ``footprint`` can be used to create a non-rectangular cutout image, in which case the input ``xpos`` and ``ypos`` represent the center of the minimal bounding box for the input ``footprint``. ``box_size=(n, m)`` is equivalent to ``footprint=np.ones((n, m))``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. mask : array_like, bool, optional A 2D boolean array with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. ``error`` must have the same shape as ``data``. ``error`` will be used only if supported by the input ``centroid_func``. centroid_func : callable, optional A callable object (e.g. function or class) that is used to calculate the centroid of a 2D array. The ``centroid_func`` must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally an ``error`` keyword. The callable object must return a tuple of two 1D `~numpy.ndarray`\s, representing the x and y centroids. The default is `~photutils.centroids.centroid_com`. Returns ------- xcentroid, ycentroid : `~numpy.ndarray` The ``x`` and ``y`` pixel position(s) of the centroids. """ xpos = np.atleast_1d(xpos) ypos = np.atleast_1d(ypos) if xpos.ndim != 1: raise ValueError('xpos must be a 1D array.') if ypos.ndim != 1: raise ValueError('ypos must be a 1D array.') if footprint is None: if box_size is None: raise ValueError('box_size or footprint must be defined.') else: box_size = np.atleast_1d(box_size) if len(box_size) == 1: box_size = np.repeat(box_size, 2) if len(box_size) != 2: raise ValueError('box_size must have 1 or 2 elements.') footprint = np.ones(box_size, dtype=bool) else: footprint = np.asanyarray(footprint, dtype=bool) if footprint.ndim != 2: raise ValueError('footprint must be a 2D array.') use_error = False spec = inspect.getfullargspec(centroid_func) if 'mask' not in spec.args: raise ValueError('The input "centroid_func" must have a "mask" ' 'keyword.') if 'error' in spec.args: use_error = True xcentroids = [] ycentroids = [] for xp, yp in zip(xpos, ypos): slices_large, slices_small = overlap_slices(data.shape, footprint.shape, (yp, xp)) data_cutout = data[slices_large] mask_cutout = None if mask is not None: mask_cutout = mask[slices_large] footprint_mask = ~footprint # trim footprint mask if partial overlap on the data footprint_mask = footprint_mask[slices_small] if mask_cutout is None: mask_cutout = footprint_mask else: # combine the input mask and footprint mask mask_cutout = np.logical_or(mask_cutout, footprint_mask) if error is not None and use_error: error_cutout = error[slices_large] xcen, ycen = centroid_func(data_cutout, mask=mask_cutout, error=error_cutout) else: xcen, ycen = centroid_func(data_cutout, mask=mask_cutout) xcentroids.append(xcen + slices_large[1].start) ycentroids.append(ycen + slices_large[0].start) return np.array(xcentroids), np.array(ycentroids)
def test_slices_no_overlap(pos): '''If there is no overlap between arrays, an error should be raised.''' with pytest.raises(NoOverlapError): overlap_slices((5, 5), (2, 2), pos)
def test_slices_pos_different_dim(): '''Position must have same dim as arrays.''' with pytest.raises(ValueError) as e: overlap_slices((4, 5), (1, 2), (0, 0, 3)) assert "the same number of dimensions" in str(e.value)
def nstar(self, image, star_groups): """ Fit, as appropriate, a compound or single model to the given ``star_groups``. Groups are fitted sequentially from the smallest to the biggest. In each iteration, ``image`` is subtracted by the previous fitted group. Parameters ---------- image : numpy.ndarray Background-subtracted image. star_groups : `~astropy.table.Table` This table must contain the following columns: ``id``, ``group_id``, ``x_0``, ``y_0``, ``flux_0``. ``x_0`` and ``y_0`` are initial estimates of the centroids and ``flux_0`` is an initial estimate of the flux. Returns ------- result_tab : `~astropy.table.Table` Astropy table that contains photometry results. image : numpy.ndarray Residual image. """ result_tab = Table([[], [], [], [], []], names=('id', 'group_id', 'x_fit', 'y_fit', 'flux_fit'), dtype=('i4', 'i4', 'f8', 'f8', 'f8')) star_groups = star_groups.group_by('group_id') y, x = np.indices(image.shape) for n in range(len(star_groups.groups)): group_psf = get_grouped_psf_model(self.psf_model, star_groups.groups[n]) usepixel = np.zeros_like(image, dtype=np.bool) for row in star_groups.groups[n]: usepixel[overlap_slices(large_array_shape=image.shape, small_array_shape=self.fitshape, position=(row['y_0'], row['x_0']), mode='trim')[0]] = True fit_model = self.fitter(group_psf, x[usepixel], y[usepixel], image[usepixel]) param_table = self._model_params2table(fit_model, star_groups.groups[n]) result_tab = vstack([result_tab, param_table]) try: from astropy.nddata.utils import NoOverlapError except ImportError: raise ImportError("astropy 1.1 or greater is required in " "order to use this class.") # do not subtract if the fitting did not go well try: image = subtract_psf(image, self.psf_model, param_table, subshape=self.fitshape) except NoOverlapError: pass return result_tab, image
def _fit_star(self, epsf, star, fitter, fitter_kwargs, fitter_has_fit_info, fit_boxsize): """ Fit an ePSF model to a single star. The input ``epsf`` will usually be modified by the fitting routine in this function. Make a copy before calling this function if the original is needed. """ if fit_boxsize is not None: try: xcenter, ycenter = star.cutout_center large_slc, small_slc = overlap_slices(star.shape, fit_boxsize, (ycenter, xcenter), mode='strict') except (PartialOverlapError, NoOverlapError): warnings.warn( 'The star at ({0}, {1}) cannot be fit because ' 'its fitting region extends beyond the star ' 'cutout image.'.format(star.center[0], star.center[1]), AstropyUserWarning) star = copy.deepcopy(star) star._fit_error_status = 1 return star data = star.data[large_slc] weights = star.weights[large_slc] # define the origin of the fitting region x0 = large_slc[1].start y0 = large_slc[0].start else: # use the entire cutout image data = star.data weights = star.weights # define the origin of the fitting region x0 = 0 y0 = 0 scaled_data = data / np.prod(epsf._oversampling) # define positions in the ePSF oversampled grid yy, xx = np.indices(data.shape, dtype=np.float) xx = (xx - (star.cutout_center[0] - x0)) * epsf._oversampling[0] yy = (yy - (star.cutout_center[1] - y0)) * epsf._oversampling[1] # define the initial guesses for fitted flux and shifts epsf.flux = star.flux epsf.x_0 = 0.0 epsf.y_0 = 0.0 # create copy to avoid overwriting original oversampling factor _epsf = epsf.copy() _epsf._oversampling = np.array([1., 1.]) try: fitted_epsf = fitter(model=_epsf, x=xx, y=yy, z=scaled_data, weights=weights, **fitter_kwargs) except TypeError: # fitter doesn't support weights fitted_epsf = fitter(model=_epsf, x=xx, y=yy, z=scaled_data, **fitter_kwargs) fit_error_status = 0 if fitter_has_fit_info: fit_info = copy.copy(fitter.fit_info) if 'ierr' in fit_info and fit_info['ierr'] not in [1, 2, 3, 4]: fit_error_status = 2 # fit solution was not found else: fit_info = None # compute the star's fitted position x_center = (star.cutout_center[0] + (fitted_epsf.x_0.value / epsf._oversampling[0])) y_center = (star.cutout_center[1] + (fitted_epsf.y_0.value / epsf._oversampling[1])) star = copy.deepcopy(star) star.cutout_center = (x_center, y_center) # set the star's flux to the ePSF-fitted flux star.flux = fitted_epsf.flux.value star._fit_info = fit_info star._fit_error_status = fit_error_status return star
def phot_sherpa(reduced_images621, reduced_images845, indexname, row, psf_621, psf_845, slice_size=11, debug=False, maxdxdy=1, **kwargs): '''Perform 2 band photometry This function performs two band photometry on a single source. Photometry is done by fitting a PSF model simultaneously to data in both bands. Only a single object is fit (so this does not take into account blended sources), but this function is still providing a benefit over simple aperture photometry: - Because the functional form of the PSF is fit, it does work in the presence of some masked pixels (e.g. if the center of the source is saturated). - This function couples the source position in both bands. Source positions are allowed to vary within a small range around the input source position. This allows for small errors in the WCS of the two images or for differences that arise because we extract the Cepheid-centered images to full-pixels, so that sub-pixel differences in the position can arise. ''' i = indexname(row['TARGNAME']) # Set up data # # Note how x and y are reversed to match order of array slice_large, slice_small = overlap_slices( reduced_images621[:, :, 0].shape, (slice_size, slice_size), (row['ycentroid'], row['xcentroid']), 'trim') x0axis, x1axis = np.mgrid[slice_large] im621 = reduced_images621[:, :, i][slice_large] im845 = reduced_images845[:, :, i][slice_large] data621 = sherpa.data.Data2D('img621', x0axis.ravel(), x1axis.ravel(), im621.ravel(), shape=(slice_size, slice_size)) data621.mask = ~im621.mask.ravel() data845 = sherpa.data.Data2D('img845', x0axis.ravel(), x1axis.ravel(), im845.ravel(), shape=(slice_size, slice_size)) data845.mask = ~im845.mask.ravel() # Set up model # colnames = ['ycentroid', 'xcentroid'] for psf in [psf_621, psf_845]: for j, par in enumerate([psf.xpos, psf.ypos]): # Set min/max to large numbers to make sure assignment works par.min = -1e10 par.max = 1e10 # set value par.val = row[colnames[j]] # Then restrict min/max to the range we want par.max = par.val + maxdxdy par.min = par.val - maxdxdy psf_621.ampl = data621.y.max() psf_845.ampl = data845.y.max() # Prepare and perform combined fit # # Code for combined fitting - but that turned out not to be necessary # databoth = sherpa.data.DataSimulFit('bothdata', (data621, data845)) # modelboth = sherpa.models.SimulFitModel('bothmodel', (psf_621, psf_845)) # fitboth = sherpa.fit.Fit(databoth, modelboth, **kwargs) # fitres = fitboth.fit() fit621 = sherpa.fit.Fit(data621, psf_621, **kwargs) fit845 = sherpa.fit.Fit(data845, psf_845, **kwargs) fit621.fit() fit845.fit() # Format output # # Get full images so that we can construct residual images of full size im621 = reduced_images621[:, :, i] x0axis, x1axis = np.mgrid[0:im621.shape[0], 0:im621.shape[1]] im845 = reduced_images845[:, :, i] resim621 = im621 - psf_621(x0axis.ravel(), x1axis.ravel()).reshape( im621.shape) resim845 = im845 - psf_845(x0axis.ravel(), x1axis.ravel()).reshape( im845.shape) outtab = Table({ 'TARGNAME': [row['TARGNAME']], 'y_621': [psf_621.xpos.val], 'x_621': [psf_621.ypos.val], 'y_845': [psf_845.xpos.val], 'x_845': [psf_845.ypos.val], 'mag_621': [betamodel2mag(psf_621, 'F621M')], 'mag_845': [betamodel2mag(psf_845, 'F845M')], 'x_0': [row['xcentroid']], 'y_0': [row['ycentroid']], 'residual_image_621': resim621.reshape(1, resim621.shape[0], resim621.shape[1]), 'residual_image_845': resim845.reshape(1, resim845.shape[0], resim845.shape[1]), }) if debug: return data621, data845, fit621, fit845, outtab else: return outtab
def test_slices_overlap_wrong_mode(): '''Call overlap_slices with non-existing mode.''' with pytest.raises(ValueError) as e: overlap_slices((5, ), (3, ), (0, ), mode='full') assert "Mode can be only" in str(e.value)
def fit_sources(image1d, psfbase, shape, normperim, medianim, mastermask, threshold=12, **kwargs): '''find and fit sources in the image perform PSF subtraction and then find and fit sources see comments in code for details Parameters ---------- image1d : ndarray flattened, normalized image psfbase : ndarray 2d array of psf templates (PSF library) threshold : float Detection threshold. Higher numbers find only the stronger sources. Experiment to find the right value. kwargs : dict or names arguments arguments for daofind (fwmh, min and max roundness, etc.) Returns ------- fluxes_gaussian : astropy.table.Table imag : PSF subtracted image scaled_im : PSF subtracted image in daofind scaling ''' psf_coeff = psf_from_projection(image1d, psfbase) im = image1d - np.dot(psfbase, psf_coeff) bkg_sigma = 1.48 * mad(im) # Do source detection on 2d, scaled image scaled_im = remove_normmask(im.reshape((-1, 1)), np.ones(1), np.ones_like(medianim), mastermask).reshape(shape) imag = remove_normmask(im.reshape((-1, 1)), normperim, medianim, mastermask).reshape(shape) sources = photutils.daofind(scaled_im, threshold=threshold * bkg_sigma, **kwargs) if len(sources) == 0: return None, imag, scaled_im else: # insert extra step here to find the brightest source, subtract it and # redo the PSF fit or add a PSF model to psfbase to improve the PSF fit # I think 1 level of that is enough, no infinite recursion. # Idea 1: mask out a region around the source, so that this does not # influence the PSF fit. newmask = deepcopy(mastermask).reshape(shape) for source in sources: sl, temp = overlap_slices(shape, [9,9], [source['xcentroid'], source['ycentroid']]) newmask[sl[0], sl[1]] = True newmask = newmask.flatten() psf_coeff = psf_from_projection(image1d[~(newmask[~mastermask])], psfbase[~(newmask[~mastermask]), :]) im = image1d - np.dot(psfbase, psf_coeff) scaled_im = remove_normmask(im.reshape((-1, 1)), np.ones(1), np.ones_like(medianim), mastermask).reshape(shape) imag = remove_normmask(im.reshape((-1, 1)), normperim, medianim, mastermask).reshape(shape) # cosmics in the image lead to high points, which means that the # average area will be overcorrected imag = imag - np.ma.median(imag) # do photometry on image in real space psf_gaussian = photutils.psf.GaussianPSF(1.8) # width measured by hand # default in photutils is to freeze this stuff, but I disagree # psf_gaussian.fixed['sigma'] = False # psf_gaussian.fixed['x_0'] = False # psf_gaussian.fixed['y_0'] = False fluxes_gaussian = photutils.psf.psf_photometry(imag, sources['xcentroid', 'ycentroid'], psf_gaussian) '''Estimate flux of Gaussian PSF from A and sigma. Should be part of photutils in a more clever (analytic) implementation. As long as it's missing there, but in this crutch here. ''' x, y = np.mgrid[-3:3, -4:4] amp2flux = np.sum(psf_gaussian.evaluate(x, y, 1, 1, 0, 1.8)) # 1.8 hard-coded above fluxes_gaussian.add_column(MaskedColumn(name='flux_fit', data=amp2flux * fluxes_gaussian['amplitude_fit'])) return fluxes_gaussian, imag, scaled_im
def centroid_sources(data, xpos, ypos, box_size=11, footprint=None, error=None, mask=None, centroid_func=centroid_com): """ Calculate the centroid of sources at the defined positions. A cutout image centered on each input position will be used to calculate the centroid position. The cutout image is defined either using the ``box_size`` or ``footprint`` keyword. The ``footprint`` keyword can be used to create a non-rectangular cutout image. Parameters ---------- data : array_like The 2D array of the image. xpos, ypos : float or array-like of float The initial ``x`` and ``y`` pixel position(s) of the center position. A cutout image centered on this position be used to calculate the centroid. box_size : int or array-like of int, optional The size of the cutout image along each axis. If ``box_size`` is a number, then a square cutout of ``box_size`` will be created. If ``box_size`` has two elements, they should be in ``(ny, nx)`` order. footprint : `~numpy.ndarray` of bools, optional A 2D boolean array where `True` values describe the local footprint region to cutout. ``footprint`` can be used to create a non-rectangular cutout image, in which case the input ``xpos`` and ``ypos`` represent the center of the minimal bounding box for the input ``footprint``. ``box_size=(n, m)`` is equivalent to ``footprint=np.ones((n, m))``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. mask : array_like, bool, optional A 2D boolean array with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. ``error`` must have the same shape as ``data``. ``error`` will be used only if supported by the input ``centroid_func``. centroid_func : callable, optional A callable object (e.g. function or class) that is used to calculate the centroid of a 2D array. The ``centroid_func`` must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally an ``error`` keyword. The callable object must return a tuple of two 1D `~numpy.ndarray`\\s, representing the x and y centroids. The default is `~photutils.centroids.centroid_com`. Returns ------- xcentroid, ycentroid : `~numpy.ndarray` The ``x`` and ``y`` pixel position(s) of the centroids. """ xpos = np.atleast_1d(xpos) ypos = np.atleast_1d(ypos) if xpos.ndim != 1: raise ValueError('xpos must be a 1D array.') if ypos.ndim != 1: raise ValueError('ypos must be a 1D array.') if footprint is None: if box_size is None: raise ValueError('box_size or footprint must be defined.') box_size = np.atleast_1d(box_size) if len(box_size) == 1: box_size = np.repeat(box_size, 2) if len(box_size) != 2: raise ValueError('box_size must have 1 or 2 elements.') footprint = np.ones(box_size, dtype=bool) else: footprint = np.asanyarray(footprint, dtype=bool) if footprint.ndim != 2: raise ValueError('footprint must be a 2D array.') use_error = False spec = inspect.getfullargspec(centroid_func) if 'mask' not in spec.args: raise ValueError('The input "centroid_func" must have a "mask" ' 'keyword.') if 'error' in spec.args: use_error = True xcentroids = [] ycentroids = [] for xp, yp in zip(xpos, ypos): slices_large, slices_small = overlap_slices(data.shape, footprint.shape, (yp, xp)) data_cutout = data[slices_large] mask_cutout = None if mask is not None: mask_cutout = mask[slices_large] footprint_mask = ~footprint # trim footprint mask if partial overlap on the data footprint_mask = footprint_mask[slices_small] if mask_cutout is None: mask_cutout = footprint_mask else: # combine the input mask and footprint mask mask_cutout = np.logical_or(mask_cutout, footprint_mask) if error is not None and use_error: error_cutout = error[slices_large] xcen, ycen = centroid_func(data_cutout, mask=mask_cutout, error=error_cutout) else: xcen, ycen = centroid_func(data_cutout, mask=mask_cutout) xcentroids.append(xcen + slices_large[1].start) ycentroids.append(ycen + slices_large[0].start) return np.array(xcentroids), np.array(ycentroids)
def _recenter_epsf(self, epsf, centroid_func=centroid_epsf, box_size=(5, 5), maxiters=20, center_accuracy=1.0e-4): """ Calculate the center of the ePSF data and shift the data so the ePSF center is at the center of the ePSF data array. Parameters ---------- epsf : `EPSFModel` object The ePSF model. centroid_func : callable, optional A callable object (e.g. function or class) that is used to calculate the centroid of a 2D array. The callable must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally an ``error`` keyword. The callable object must return a tuple of two 1D `~numpy.ndarray` variables, representing the x and y centroids. box_size : float or tuple of two floats, optional The size (in pixels) of the box used to calculate the centroid of the ePSF during each build iteration. If a single integer number is provided, then a square box will be used. If two values are provided, then they should be in ``(ny, nx)`` order. maxiters : int, optional The maximum number of recentering iterations to perform. center_accuracy : float, optional The desired accuracy for the centers of stars. The building iterations will stop if the center of the ePSF changes by less than ``center_accuracy`` pixels between iterations. Returns ------- result : 2D `~numpy.ndarray` The recentered ePSF data. """ epsf_data = epsf._data epsf = EPSFModel(data=epsf._data, origin=epsf.origin, oversampling=epsf.oversampling, norm_radius=epsf._norm_radius, shift_val=epsf._shift_val, normalize=False) xcenter, ycenter = epsf.origin y, x = np.indices(epsf._data.shape, dtype=np.float) x /= epsf.oversampling[0] y /= epsf.oversampling[1] dx_total, dy_total = 0, 0 iter_num = 0 center_accuracy_sq = center_accuracy**2 center_dist_sq = center_accuracy_sq + 1.e6 center_dist_sq_prev = center_dist_sq + 1 while (iter_num < maxiters and center_dist_sq >= center_accuracy_sq): iter_num += 1 # Anderson & King (2000) recentering function depends # on specific pixels, and thus does not need a cutout if self.recentering_func == centroid_epsf: epsf_cutout = epsf_data else: slices_large, _ = overlap_slices( epsf_data.shape, box_size, (ycenter * self.oversampling[1], xcenter * self.oversampling[0])) epsf_cutout = epsf_data[slices_large] mask = ~np.isfinite(epsf_cutout) try: # find a new center position xcenter_new, ycenter_new = centroid_func( epsf_cutout, mask=mask, oversampling=epsf.oversampling, shift_val=epsf._shift_val) except TypeError: # centroid_func doesn't accept oversampling and/or shift_val # keywords - try oversampling alone try: xcenter_new, ycenter_new = centroid_func( epsf_cutout, mask=mask, oversampling=epsf.oversampling) except TypeError: # centroid_func doesn't accept oversampling and # shift_val xcenter_new, ycenter_new = centroid_func(epsf_cutout, mask=mask) if self.recentering_func != centroid_epsf: xcenter_new += slices_large[1].start / self.oversampling[0] ycenter_new += slices_large[0].start / self.oversampling[1] # Calculate the shift; dx = i - x_star so if dx was positively # incremented then x_star was negatively incremented for a given i. # We will therefore actually subsequently subtract dx from xcenter # (or x_star). dx = xcenter_new - xcenter dy = ycenter_new - ycenter center_dist_sq = dx**2 + dy**2 if center_dist_sq >= center_dist_sq_prev: # don't shift break center_dist_sq_prev = center_dist_sq dx_total += dx dy_total += dy epsf_data = epsf.evaluate(x=x, y=y, flux=1.0, x_0=xcenter - dx_total, y_0=ycenter - dy_total) return epsf_data
def _extract_stars(data, catalog, size=(11, 11), use_xy=True): """ Extract cutout images from a single image centered on stars defined in the single input catalog. Parameters ---------- data : `~astropy.nddata.NDData` A `~astropy.nddata.NDData` object containing the 2D image from which to extract the stars. If the input ``catalog`` contains only the sky coordinates (i.e. not the pixel coordinates) of the stars then the `~astropy.nddata.NDData` object must have a valid ``wcs`` attribute. catalogs : `~astropy.table.Table` A single catalog of sources to be extracted from the input ``data``. The center of each source can be defined either in pixel coordinates (in ``x`` and ``y`` columns) or sky coordinates (in a ``skycoord`` column containing a `~astropy.coordinates.SkyCoord` object). If both are specified, then the value of the ``use_xy`` keyword determines which coordinates will be used. size : int or array_like (int), optional The extraction box size along each axis. If ``size`` is a scalar then a square box of size ``size`` will be used. If ``size`` has two elements, they should be in ``(ny, nx)`` order. The size must be greater than or equal to 3 pixel for both axes. use_xy : bool, optional Whether to use the ``x`` and ``y`` pixel positions when both pixel and sky coordinates are present in the input catalog table. If `False` then sky coordinates are used instead of pixel coordinates (e.g. for linked stars). The default is `True`. Returns ------- stars : list of `EPSFStar` objects A list of `EPSFStar` instances containing the extracted stars. """ colnames = catalog.colnames if ('x' not in colnames or 'y' not in colnames) or not use_xy: xcenters, ycenters = skycoord_to_pixel(catalog['skycoord'], data.wcs, origin=0, mode='all') else: xcenters = catalog['x'].data.astype(np.float) ycenters = catalog['y'].data.astype(np.float) if 'id' in colnames: ids = catalog['id'] else: ids = np.arange(len(catalog), dtype=np.int) + 1 if data.uncertainty is None: weights = np.ones_like(data.data) else: if data.uncertainty.uncertainty_type == 'weights': weights = np.asanyarray(data.uncertainty.array, dtype=np.float) else: warnings.warn('The data uncertainty attribute has an unsupported ' 'type. Only uncertainty_type="weights" can be ' 'used to set weights. Weights will be set to 1.', AstropyUserWarning) weights = np.ones_like(data.data) if data.mask is not None: weights[data.mask] = 0. stars = [] for xcenter, ycenter, obj_id in zip(xcenters, ycenters, ids): try: large_slc, small_slc = overlap_slices(data.data.shape, size, (ycenter, xcenter), mode='strict') data_cutout = data.data[large_slc] weights_cutout = weights[large_slc] except (PartialOverlapError, NoOverlapError): stars.append(None) continue origin = (large_slc[1].start, large_slc[0].start) cutout_center = (xcenter - origin[0], ycenter - origin[1]) star = EPSFStar(data_cutout, weights_cutout, cutout_center=cutout_center, origin=origin, wcs_large=data.wcs, id_label=obj_id) stars.append(star) return stars
def _fit_star(self, epsf, star, fitter, fitter_kwargs, fitter_has_fit_info, fit_boxsize): """ Fit an ePSF model to a single star. The input ``epsf`` will usually be modified by the fitting routine in this function. Make a copy before calling this function if the original is needed. """ if fit_boxsize is not None: try: xcenter, ycenter = star.cutout_center large_slc, small_slc = overlap_slices(star.shape, fit_boxsize, (ycenter, xcenter), mode='strict') except (PartialOverlapError, NoOverlapError): warnings.warn('The star at ({0}, {1}) cannot be fit because ' 'its fitting region extends beyond the star ' 'cutout image.'.format(star.center[0], star.center[1]), AstropyUserWarning) star = copy.deepcopy(star) star._fit_error_status = 1 return star data = star.data[large_slc] weights = star.weights[large_slc] # define the origin of the fitting region x0 = large_slc[1].start y0 = large_slc[0].start else: # use the entire cutout image data = star.data weights = star.weights # define the origin of the fitting region x0 = 0 y0 = 0 scaled_data = data / np.prod(epsf._oversampling) # define positions in the ePSF oversampled grid yy, xx = np.indices(data.shape, dtype=np.float) xx = (xx - (star.cutout_center[0] - x0)) * epsf._oversampling[0] yy = (yy - (star.cutout_center[1] - y0)) * epsf._oversampling[1] # define the initial guesses for fitted flux and shifts epsf.flux = star.flux epsf.x_0 = 0.0 epsf.y_0 = 0.0 # create copy to avoid overwriting original oversampling factor _epsf = epsf.copy() _epsf._oversampling = np.array([1., 1.]) try: fitted_epsf = fitter(model=_epsf, x=xx, y=yy, z=scaled_data, weights=weights, **fitter_kwargs) except TypeError: # fitter doesn't support weights fitted_epsf = fitter(model=_epsf, x=xx, y=yy, z=scaled_data, **fitter_kwargs) fit_error_status = 0 if fitter_has_fit_info: fit_info = copy.copy(fitter.fit_info) if 'ierr' in fit_info and fit_info['ierr'] not in [1, 2, 3, 4]: fit_error_status = 2 # fit solution was not found else: fit_info = None # compute the star's fitted position x_center = (star.cutout_center[0] + (fitted_epsf.x_0.value / epsf._oversampling[0])) y_center = (star.cutout_center[1] + (fitted_epsf.y_0.value / epsf._oversampling[1])) star = copy.deepcopy(star) star.cutout_center = (x_center, y_center) # set the star's flux to the ePSF-fitted flux star.flux = fitted_epsf.flux.value star._fit_info = fit_info star._fit_error_status = fit_error_status return star
def listpixels(data, position, shape, subarray_indices=False, wcs=None): """ Return a `~astropy.table.Table` listing the ``(y, x)`` positions and ``data`` values for a subarray. Given a position of the center of the subarray, with respect to the large array, the array indices and values are returned. This function takes care of the correct behavior at the boundaries, where the small array is appropriately trimmed. Parameters ---------- data : array-like The input data. position : tuple (int) or `~astropy.coordinates.SkyCoord` The position of the subarray center with respect to the data array. The position can be specified either as an integer ``(y, x)`` tuple of pixel coordinates or a `~astropy.coordinates.SkyCoord`, in which case ``wcs`` is a required input. shape : tuple (int) The integer shape (``(ny, nx)``) of the subarray. subarray_indices : bool, optional If `True` then the returned positions are relative to the small subarray. If `False` (default) then the returned positions are relative to the ``data`` array. wcs : `~astropy.wcs.WCS`, optional The WCS transformation to use if ``position`` is a `~astropy.coordinates.SkyCoord`. Returns ------- table : `~astropy.table.Table` A table containing the ``x`` and ``y`` positions and data values. Notes ----- This function is decorated with `~astropy.nddata.support_nddata` and thus supports `~astropy.nddata.NDData` objects as input. Examples -------- >>> import numpy as np >>> from astroimtools import listpixels >>> np.random.seed(12345) >>> data = np.random.random((25, 25)) >>> tbl = listpixels(data, (8, 11), (3, 3)) >>> for col in tbl.colnames: ... tbl[col].info.format = '%.8g' # for consistent table output >>> tbl.pprint(max_lines=-1) x y value --- --- ----------- 10 7 0.75857204 11 7 0.069529666 12 7 0.70547344 10 8 0.8406625 11 8 0.46931469 12 8 0.56264343 10 9 0.034131584 11 9 0.23049655 12 9 0.22835371 """ if isinstance(position, SkyCoord): if wcs is None: raise ValueError('wcs must be input if positions is a SkyCoord') x, y = skycoord_to_pixel(position, wcs, mode='all') position = (y, x) data = np.asanyarray(data) slices_large, slices_small = overlap_slices(data.shape, shape, position) slices = slices_large yy, xx = np.mgrid[slices] values = data[yy, xx] if subarray_indices: slices = slices_small yy, xx = np.mgrid[slices] tbl = Table() tbl['x'] = xx.ravel() tbl['y'] = yy.ravel() tbl['value'] = values.ravel() return tbl
def _recenter_epsf(self, epsf_data, epsf, centroid_func=centroid_com, box_size=5, maxiters=20, center_accuracy=1.0e-4): """ Calculate the center of the ePSF data and shift the data so the ePSF center is at the center of the ePSF data array. Parameters ---------- epsf_data : 2D `~numpy.ndarray` A 2D array containing the ePSF image. epsf : `EPSFModel` object The ePSF model. centroid_func : callable, optional A callable object (e.g. function or class) that is used to calculate the centroid of a 2D array. The callable must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally an ``error`` keyword. The callable object must return a tuple of two 1D `~numpy.ndarray`\\s, representing the x and y centroids. The default is `~photutils.centroids.centroid_com`. recentering_boxsize : float or tuple of two floats, optional The size (in pixels) of the box used to calculate the centroid of the ePSF during each build iteration. If a single integer number is provided, then a square box will be used. If two values are provided, then they should be in ``(ny, nx)`` order. The default is 5. maxiters : int, optional The maximum number of recentering iterations to perform. The default is 20. center_accuracy : float, optional The desired accuracy for the centers of stars. The building iterations will stop if the center of the ePSF changes by less than ``center_accuracy`` pixels between iterations. The default is 1.0e-4. Returns ------- result : 2D `~numpy.ndarray` The recentered ePSF data. """ # Define an EPSFModel for the input data. This EPSFModel will be # used to evaluate the model on a shifted pixel grid to place the # centroid at the array center. epsf = EPSFModel(data=epsf_data, origin=epsf.origin, normalize=False, oversampling=epsf.oversampling, pixel_scale=epsf.pixel_scale) epsf.fill_value = 0.0 xcenter, ycenter = epsf.origin dx_total = 0 dy_total = 0 y, x = np.indices(epsf_data.shape, dtype=np.float) iter_num = 0 center_accuracy_sq = center_accuracy**2 center_dist_sq = center_accuracy_sq + 1.e6 center_dist_sq_prev = center_dist_sq + 1 while (iter_num < maxiters and center_dist_sq >= center_accuracy_sq): iter_num += 1 # extract a cutout from the ePSF slices_large, slices_small = overlap_slices( epsf_data.shape, box_size, (ycenter, xcenter)) epsf_cutout = epsf_data[slices_large] mask = ~np.isfinite(epsf_cutout) # find a new center position xcenter_new, ycenter_new = centroid_func(epsf_cutout, mask=mask) xcenter_new += slices_large[1].start ycenter_new += slices_large[0].start # calculate the shift dx = xcenter - xcenter_new dy = ycenter - ycenter_new center_dist_sq = dx**2 + dy**2 if center_dist_sq >= center_dist_sq_prev: # don't shift break center_dist_sq_prev = center_dist_sq # Resample the ePSF data to a shifted grid to place the # centroid in the center of the central pixel. The shift is # always performed on the input epsf_data. dx_total += dx # accumulated shifts for the input epsf_data dy_total += dy epsf_data = epsf.evaluate(x=x, y=y, flux=1.0, x_0=xcenter + dx_total, y_0=ycenter + dy_total, use_oversampling=False) return epsf_data
def centroid_sources(data, xpos, ypos, box_size=11, footprint=None, mask=None, centroid_func=centroid_com, **kwargs): """ Calculate the centroid of sources at the defined positions. A cutout image centered on each input position will be used to calculate the centroid position. The cutout image is defined either using the ``box_size`` or ``footprint`` keyword. The ``footprint`` keyword can be used to create a non-rectangular cutout image. Parameters ---------- data : array_like The 2D array of the image. xpos, ypos : float or array-like of float The initial ``x`` and ``y`` pixel position(s) of the center position. A cutout image centered on this position be used to calculate the centroid. box_size : int or array-like of int, optional The size of the cutout image along each axis. If ``box_size`` is a number, then a square cutout of ``box_size`` will be created. If ``box_size`` has two elements, they should be in ``(ny, nx)`` order. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. footprint : `~numpy.ndarray` of bools, optional A 2D boolean array where `True` values describe the local footprint region to cutout. ``footprint`` can be used to create a non-rectangular cutout image, in which case the input ``xpos`` and ``ypos`` represent the center of the minimal bounding box for the input ``footprint``. ``box_size=(n, m)`` is equivalent to ``footprint=np.ones((n, m))``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. mask : array_like, bool, optional A 2D boolean array with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. ``error`` must have the same shape as ``data``. ``error`` will be used only if supported by the input ``centroid_func``. centroid_func : callable, optional A callable object (e.g., function or class) that is used to calculate the centroid of a 2D array. The ``centroid_func`` must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally an ``error`` keyword. The callable object must return a tuple of two 1D `~numpy.ndarray`, representing the x and y centroids. The default is `~photutils.centroids.centroid_com`. **kwargs : `dict` Any additional keyword arguments accepted by the ``centroid_func``. Returns ------- xcentroid, ycentroid : `~numpy.ndarray` The ``x`` and ``y`` pixel position(s) of the centroids. NaNs will be returned where the centroid failed. This is usually due a ``box_size`` that is too small when using a fitting-based centroid function (e.g., `centroid_1dg`, `centroid_2dg`, or `centroid_quadratic`. """ xpos = np.atleast_1d(xpos) ypos = np.atleast_1d(ypos) if xpos.ndim != 1: raise ValueError('xpos must be a 1D array.') if ypos.ndim != 1: raise ValueError('ypos must be a 1D array.') if (np.any(np.min(xpos) < 0) or np.any(np.min(ypos) < 0) or np.any(np.max(xpos) > data.shape[1] - 1) or np.any(np.max(ypos) > data.shape[0] - 1)): raise ValueError('xpos, ypos values contains point(s) outside of ' 'input data') if footprint is None: if box_size is None: raise ValueError('box_size or footprint must be defined.') box_size = np.atleast_1d(box_size) if len(box_size) == 1: box_size = np.repeat(box_size, 2) if len(box_size) != 2: raise ValueError('box_size must have 1 or 2 elements.') footprint = np.ones(box_size, dtype=bool) else: footprint = np.asanyarray(footprint, dtype=bool) if footprint.ndim != 2: raise ValueError('footprint must be a 2D array.') spec = inspect.getfullargspec(centroid_func) if 'mask' not in spec.args: raise ValueError('The input "centroid_func" must have a "mask" ' 'keyword.') # drop any **kwargs not supported by the centroid_func centroid_kwargs = {} for key, val in kwargs.items(): if key in spec.args: centroid_kwargs[key] = val xcentroids = [] ycentroids = [] for xp, yp in zip(xpos, ypos): slices_large, slices_small = overlap_slices(data.shape, footprint.shape, (yp, xp)) data_cutout = data[slices_large] footprint_mask = np.logical_not(footprint) # trim footprint mask if it has only partial overlap on the data footprint_mask = footprint_mask[slices_small] if mask is not None: # combine the input mask cutout and footprint mask mask_cutout = np.logical_or(mask[slices_large], footprint_mask) else: mask_cutout = footprint_mask centroid_kwargs.update({'mask': mask_cutout}) error = centroid_kwargs.get('error', None) if error is not None: centroid_kwargs['error'] = error[slices_large] # remove xpeak and ypeak from the dict and add back only if both # are specified and not None xpeak = centroid_kwargs.pop('xpeak', None) ypeak = centroid_kwargs.pop('ypeak', None) if xpeak is not None and ypeak is not None: centroid_kwargs['xpeak'] = xpeak - slices_large[1].start centroid_kwargs['ypeak'] = ypeak - slices_large[0].start try: xcen, ycen = centroid_func(data_cutout, **centroid_kwargs) except (ValueError, TypeError): xcen, ycen = np.nan, np.nan xcentroids.append(xcen + slices_large[1].start) ycentroids.append(ycen + slices_large[0].start) return np.array(xcentroids), np.array(ycentroids)
def cutout_footprint(data, position, box_size=3, footprint=None, mask=None, error=None): """ Cut out a region from data (and optional mask and error) centered at specified (x, y) position. The size of the region is specified via the ``box_size`` or ``footprint`` keywords. The output mask for the cutout region represents the combination of the input mask and footprint mask. Parameters ---------- data : array_like The 2D array of the image. position : 2 tuple The ``(x, y)`` pixel coordinate of the center of the region. box_size : scalar or tuple, optional The size of the region to cutout from ``data``. If ``box_size`` is a scalar, then the region shape will be ``(box_size, box_size)``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. footprint : `~numpy.ndarray` of bools, optional A boolean array where `True` values describe the local footprint region. ``box_size=(n, m)`` is equivalent to ``footprint=np.ones((n, m))``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. mask : array_like, bool, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. Returns ------- region_data : `~numpy.ndarray` The ``data`` cutout. region_mask : `~numpy.ndarray` The ``mask`` cutout. region_error : `~numpy.ndarray` The ``error`` cutout. slices : tuple of slices Slices in each dimension of the ``data`` array used to define the cutout region. """ if len(position) != 2: raise ValueError('position must have a length of 2') if footprint is None: if box_size is None: raise ValueError('box_size or footprint must be defined.') if not isinstance(box_size, collections.Iterable): shape = (box_size, box_size) else: if len(box_size) != 2: raise ValueError('box_size must have a length of 2') shape = box_size footprint = np.ones(shape, dtype=bool) else: footprint = np.asanyarray(footprint, dtype=bool) slices_large, slices_small = overlap_slices(data.shape, footprint.shape, position[::-1]) region_data = data[slices_large] if error is not None: region_error = error[slices_large] else: region_error = None if mask is not None: region_mask = mask[slices_large] else: region_mask = np.zeros_like(region_data, dtype=bool) footprint_mask = ~footprint footprint_mask = footprint_mask[slices_small] # trim if necessary region_mask = np.logical_or(region_mask, footprint_mask) return region_data, region_mask, region_error, slices_large
def test_slices_different_dim(): '''Overlap from arrays with different number of dim is undefined.''' with pytest.raises(ValueError) as e: overlap_slices((4, 5, 6), (1, 2), (0, 0)) assert "the same number of dimensions" in str(e.value)