Source code for pypeit.core.pydl

# Licensed under a 3-clause BSD style license - see PYDL_LICENSE.rst
# -*- coding: utf-8 -*-
# Also cite https://doi.org/10.5281/zenodo.1095150 when referencing PYDL
from IPython import embed

import numpy as np

from pypeit import msgs
from pypeit import utils
from pypeit.core import basis
from pypeit.core import fitting

"""This module corresponds to the image directory in idlutils.
"""

[docs]def djs_maskinterp1(yval, mask, xval=None, const=False): """Interpolate over a masked, 1-d array. Parameters ---------- yval : :class:`numpy.ndarray` The input values. mask : :class:`numpy.ndarray` The mask. xval : :class:`numpy.ndarray`, optional If set, use these x values, otherwise use an array. const : :class:`bool`, optional If set to ``True``, bad values around the edges of the array will be set to a constant value. Because of the default behavior of :func:`numpy.interp`, this value actually makes no difference in the output. Returns ------- :class:`numpy.ndarray`: The `yval` array with masked values replaced by interpolated values. """ good = mask == 0 if good.all(): return yval ngood = good.sum() igood = good.nonzero()[0] if ngood == 0: return yval if ngood == 1: return np.zeros(yval.shape, dtype=yval.dtype) + yval[igood[0]] ynew = yval.astype('d') ny = yval.size ibad = (mask != 0).nonzero()[0] if xval is None: ynew[ibad] = np.interp(ibad, igood, ynew[igood]) if const: if igood[0] != 0: ynew[0:igood[0]] = ynew[igood[0]] if igood[ngood-1] != ny-1: ynew[igood[ngood-1]+1:ny] = ynew[igood[ngood-1]] else: ii = xval.argsort() ibad = (mask[ii] != 0).nonzero()[0] igood = (mask[ii] == 0).nonzero()[0] ynew[ii[ibad]] = np.interp(xval[ii[ibad]], xval[ii[igood]], ynew[ii[igood]]) if const: if igood[0] != 0: ynew[ii[0:igood[0]]] = ynew[ii[igood[0]]] if igood[ngood-1] != ny-1: ynew[ii[igood[ngood-1]+1:ny]] = ynew[ii[igood[ngood-1]]] return ynew
[docs]def djs_maskinterp(yval, mask, xval=None, axis=None, const=False): """Interpolate over masked pixels in a vector, image or 3-D array. Parameters ---------- yval : :class:`numpy.ndarray` The input values mask : :class:`numpy.ndarray` The mask xval : :class:`numpy.ndarray`, optional If set, use these x values, otherwise use an array axis : :class:`int`, optional Must be set if yval has more than one dimension. If set to zero, interpolate along the first axis of the array, if set to one, interpolate along the second axis of the array, and so on. const : :class:`bool`, optional This value is passed to a helper function, djs_maskinterp1. Returns ------- :class:`numpy.ndarray` The interpolated array. """ if mask.shape != yval.shape: raise ValueError('mask must have the same shape as yval.') if xval is not None: if xval.shape != yval.shape: raise ValueError('xval must have the same shape as yval.') ndim = yval.ndim if ndim == 1: ynew = djs_maskinterp1(yval, mask, xval=xval, const=const) else: if axis is None: raise ValueError('Must set axis if yval has more than one dimension.') if axis < 0 or axis > ndim-1 or axis - int(axis) != 0: raise ValueError('Invalid axis value.') ynew = np.zeros(yval.shape, dtype=yval.dtype) if ndim == 2: if xval is None: if axis == 0: for i in range(yval.shape[0]): ynew[i, :] = djs_maskinterp1(yval[i, :], mask[i, :], const=const) else: for i in range(yval.shape[1]): ynew[:, i] = djs_maskinterp1(yval[:, i], mask[:, i], const=const) else: if axis == 0: for i in range(yval.shape[0]): ynew[i, :] = djs_maskinterp1(yval[i, :], mask[i, :], xval=xval[i, :], const=const) else: for i in range(yval.shape[1]): ynew[:, i] = djs_maskinterp1(yval[:, i], mask[:, i], xval=xval[:, i], const=const) elif ndim == 3: if xval is None: if axis == 0: for i in range(yval.shape[0]): for j in range(yval.shape[1]): ynew[i, j, :] = djs_maskinterp1(yval[i, j, :], mask[i, j, :], const=const) elif axis == 1: for i in range(yval.shape[0]): for j in range(yval.shape[2]): ynew[i, :, j] = djs_maskinterp1(yval[i, :, j], mask[i, :, j], const=const) else: for i in range(yval.shape[1]): for j in range(yval.shape[2]): ynew[:, i, j] = djs_maskinterp1(yval[:, i, j], mask[:, i, j], const=const) else: if axis == 0: for i in range(yval.shape[0]): for j in range(yval.shape[1]): ynew[i, j, :] = djs_maskinterp1(yval[i, j, :], mask[i, j, :], xval=xval[i, j, :], const=const) elif axis == 1: for i in range(yval.shape[0]): for j in range(yval.shape[2]): ynew[i, :, j] = djs_maskinterp1(yval[i, :, j], mask[i, :, j], xval=xval[i, :, j], const=const) else: for i in range(yval.shape[1]): for j in range(yval.shape[2]): ynew[:, i, j] = djs_maskinterp1(yval[:, i, j], mask[:, i, j], xval=xval[:, i, j], const=const) else: raise ValueError('Unsupported number of dimensions.') return ynew
[docs]def func_fit(x, y, ncoeff, invvar=None, function_name='legendre', ia=None, inputans=None, inputfunc=None): """Fit `x`, `y` positions to a functional form. Parameters ---------- x : array-like X values (independent variable). y : array-like Y values (dependent variable). ncoeff : :class:`int` Number of coefficients to fit. invvar : array-like, optional Weight values; inverse variance. function_name : :class:`str`, optional Function name, default 'legendre'. ia : array-like, optional An array of bool of length `ncoeff` specifying free (``True``) and fixed (``False``) parameters. inputans : array-like, optional An array of values of length `ncoeff` specifying the values of the fixed parameters. inputfunc : array-like, optional Multiply the function fit by these values. Returns ------- :func:`tuple` of array-like Fit coefficients, length `ncoeff`; fitted values. Raises ------ KeyError If an invalid function type is selected. ValueError If input dimensions do not agree. """ if x.shape != y.shape: raise ValueError('Dimensions of X and Y do not agree!') if invvar is None: invvar = np.ones(x.shape, dtype=x.dtype) else: if invvar.shape != x.shape: raise ValueError('Dimensions of X and invvar do not agree!') if ia is None: ia = np.ones((ncoeff,), dtype=bool) if not ia.all(): if inputans is None: inputans = np.zeros((ncoeff,), dtype=x.dtype) # # Select unmasked points # igood = (invvar > 0).nonzero()[0] ngood = len(igood) res = np.zeros((ncoeff,), dtype=x.dtype) yfit = np.zeros(x.shape, dtype=x.dtype) if ngood == 0: pass elif ngood == 1: res[0] = y[igood[0]] yfit += y[igood[0]] else: ncfit = min(ngood, ncoeff) function_map = { 'legendre': basis.flegendre, 'flegendre': basis.flegendre, 'chebyshev': basis.fchebyshev, 'fchebyshev': basis.fchebyshev, 'chebyshev_split': basis.fchebyshev_split, 'fchebyshev_split': basis.fchebyshev_split, 'poly': basis.fpoly, 'fpoly': basis.fpoly } try: legarr = function_map[function_name](x, ncfit).T except KeyError: raise KeyError('Unknown function type: {0}'.format(function_name)) if inputfunc is not None: if inputfunc.shape != x.shape: raise ValueError('Dimensions of X and inputfunc do not agree!') legarr *= np.tile(inputfunc, ncfit).reshape(ncfit, x.shape[0]) yfix = np.zeros(x.shape, dtype=x.dtype) nonfix = ia[0:ncfit].nonzero()[0] nparams = len(nonfix) fixed = (~ia[0:ncfit]).nonzero()[0] if len(fixed) > 0: yfix = np.dot(legarr.T, inputans * (1 - ia)) ysub = y - yfix finalarr = legarr[nonfix, :] else: finalarr = legarr ysub = y # extra2 = finalarr * np.outer(np.ones((nparams,), dtype=x.dtype), # (invvar > 0)) extra2 = finalarr * np.outer(np.ones((nparams,), dtype=x.dtype), invvar) alpha = np.dot(finalarr, extra2.T) # assert alpha.dtype == x.dtype if nparams > 1: # beta = np.dot(ysub * (invvar > 0), finalarr.T) beta = np.dot(ysub * invvar, finalarr.T) assert beta.dtype == x.dtype # uu,ww,vv = np.linalg.svd(alpha, full_matrices=False) res[nonfix] = np.linalg.solve(alpha, beta) else: # res[nonfix] = (ysub * (invvar > 0) * finalarr).sum()/alpha res[nonfix] = (ysub * invvar * finalarr).sum()/alpha if len(fixed) > 0: res[fixed] = inputans[fixed] yfit = np.dot(legarr.T, res[0:ncfit]) return res, yfit
# TODO -- This class needs to become a DataContainer
[docs]class TraceSet(object): """Implements the idea of a trace set. Attributes ---------- func : :class:`str` Name of function type used to fit the trace set. xmin : float-like Minimum x value. xmax : float-like Maximum x value. coeff : array-like Coefficients of the trace set fit. nTrace : :class:`int` Number of traces in the object. ncoeff : :class:`int` Number of coefficients of the trace set fit. xjumplo : float-like Jump value, for BOSS readouts. xjumphi : float-like Jump value, for BOSS readouts. xjumpval : float-like Jump value, for BOSS readouts. lower : `int` or `float`, optional used for robust_polyfit_djs If set, reject points with data < model - lower * sigma. upper : `int` or `float`, optional used for robust_polyfit_djs If set, reject points with data > model + upper * sigma. outmask : array-like When initialized with x,y positions, this contains the rejected points. yfit : array-like When initialized with x,y positions, this contains the fitted y values. pypeitFits : list Holds a list of :class:`~pypeit.core.fitting.PypeItFit` fits """ # ToDO Remove the kwargs and put in all the djs_reject parameters here def __init__(self, *args, **kwargs): """This class can be initialized either with a set of xy positions, or with a trace set HDU from a FITS file. """ from astropy.io.fits.fitsrec import FITS_rec #from .math import djs_reject allowed_functions = ['polynomial', 'legendre', 'chebyshev'] if len(args) == 1 and isinstance(args[0], FITS_rec): # # Initialize with FITS data # self.func = args[0]['FUNC'][0] self.xmin = args[0]['XMIN'][0] self.xmax = args[0]['XMAX'][0] self.coeff = args[0]['COEFF'][0] self.nTrace = self.coeff.shape[0] self.ncoeff = self.coeff.shape[1] if 'XJUMPLO' in args[0].dtype.names: self.xjumplo = args[0]['XJUMPLO'][0] self.xjumphi = args[0]['XJUMPHI'][0] self.xjumpval = args[0]['XJUMPVAL'][0] else: self.xjumplo = None self.xjumphi = None self.xjumpval = None self.lower = None self.upper = None self.outmask = None self.yfit = None elif len(args) == 2: # # Initialize with x, y positions. # xpos = args[0] ypos = args[1] self.nTrace = xpos.shape[0] if 'invvar' in kwargs: invvar = kwargs['invvar'] else: invvar = None #invvar = np.ones(xpos.shape, dtype=xpos.dtype) if 'func' in kwargs: if kwargs['func'] not in allowed_functions: msgs.error('Unrecognized function.') self.func = kwargs['func'] else: self.func = 'legendre' if 'ncoeff' in kwargs: self.ncoeff = int(kwargs['ncoeff']) else: self.ncoeff = 3 if 'xmin' in kwargs: self.xmin = np.float64(kwargs['xmin']) else: self.xmin = xpos.min() if 'xmax' in kwargs: self.xmax = np.float64(kwargs['xmax']) else: self.xmax = xpos.max() if 'maxiter' in kwargs: self.maxiter = int(kwargs['maxiter']) else: self.maxiter = 10 if 'maxdev' in kwargs: self.maxdev = kwargs['maxdev'] else: self.maxdev = None if 'inmask' in kwargs: inmask = kwargs['inmask'] elif invvar is not None: inmask = (invvar > 0.0) else: inmask = np.ones(xpos.shape, dtype=bool) do_jump = False if 'xjumplo' in kwargs: do_jump = True self.xjumplo = np.float64(kwargs['xjumplo']) else: self.xjumplo = None if 'xjumphi' in kwargs: self.xjumphi = np.float64(kwargs['xjumphi']) else: self.xjumphi = None if 'xjumpval' in kwargs: self.xjumpval = np.float64(kwargs['xjumpval']) else: self.xjumpval = None if 'lower' in kwargs: self.lower = np.float64(kwargs['lower']) else: self.lower = None if 'upper' in kwargs: self.upper = np.float64(kwargs['upper']) else: self.upper = None self.coeff = np.zeros((self.nTrace, self.ncoeff+1), dtype=xpos.dtype) self.outmask = np.zeros(xpos.shape, dtype=bool) self.yfit = np.zeros(xpos.shape, dtype=xpos.dtype) self.pypeitFits = [] for iTrace in range(self.nTrace): xvec = self.xnorm(xpos[iTrace, :], do_jump) if invvar is None: thisinvvar = None else: thisinvvar = invvar[iTrace, :] pypeitFit = fitting.robust_fit(xvec, ypos[iTrace, :], self.ncoeff, function=self.func, maxiter = self.maxiter, in_gpm = inmask[iTrace, :], invvar = thisinvvar, lower = self.lower, upper = self.upper, minx = self.xmin, maxx = self.xmax, maxdev=self.maxdev, grow=0,use_mad=False,sticky=False) ycurfit_djs = pypeitFit.eval(xvec) self.pypeitFits.append(pypeitFit) # Load self.yfit[iTrace, :] = ycurfit_djs self.coeff[iTrace, :] = pypeitFit.fitc self.outmask[iTrace, :] = pypeitFit.gpm else: msgs.error('Wrong number of arguments to TraceSet!')
[docs] def xy(self, xpos=None, ignore_jump=False): """Convert from a trace set to an array of x,y positions. Parameters ---------- xpos : array-like, optional If provided, evaluate the trace set at these positions. Otherwise the positions will be constructed from the trace set object iself. ignore_jump : :class:`bool`, optional If ``True``, ignore any jump information in the `tset` object Returns ------- :func:`tuple` of array-like The x, y positions. """ #from .misc import djs_laxisgen do_jump = self.has_jump and (not ignore_jump) if xpos is None: xpos = djs_laxisgen([self.nTrace, self.nx], iaxis=1) + self.xmin ypos = np.zeros(xpos.shape, dtype=xpos.dtype) for iTrace in range(self.nTrace): xvec = self.xnorm(xpos[iTrace, :], do_jump) #legarr = self._func_map[self.func](xvec, self.ncoeff+1) #need to be norder+1 for utils functions #ypos[iTrace, :] = utils.func_val(self.coeff[iTrace, :], xvec, self.func, minx=self.xmin, maxx=self.xmax) ypos[iTrace, :] = self.pypeitFits[iTrace].eval(xvec)#, self.func, minx=self.xmin, maxx=self.xmax) # ypos[iTrace, :] = np.dot(legarr.T, self.coeff[iTrace, :]) return (xpos, ypos)
@property def has_jump(self): """``True`` if jump conditions are set. """ return self.xjumplo is not None @property def xRange(self): """Range of x values. """ return self.xmax - self.xmin @property def nx(self): """Number of x values. """ return int(self.xRange + 1) # JFH This is no longer ndeeded. Below we changed to robust_polyfit_djs convention for writing this. Same value just one less parameter. @property def xmid(self): """Midpoint of x values. """ return 0.5 * (self.xmin + self.xmax)
[docs] def xnorm(self, xinput, jump): """Convert input x coordinates to normalized coordinates suitable for input to special polynomials. Parameters ---------- xinput : array-like Input coordinates. jump : :class:`bool` Set to ``True`` if there is a jump. Returns ------- array-like Normalized coordinates. """ if jump: # Vector specifying what fraction of the jump has passed: jfrac = np.minimum(np.maximum(((xinput - self.xjumplo) / (self.xjumphi - self.xjumplo)), 0.), 1.) # Conversion to "natural" x baseline: xnatural = xinput + jfrac * self.xjumpval else: xnatural = xinput return 2.0 * (xnatural - self.xmin)/self.xRange - 1.0
[docs]def traceset2xy(tset, xpos=None, ignore_jump=False): """Convert from a trace set to an array of x,y positions. Parameters ---------- tset : :class:`TraceSet` A :class:`TraceSet` object. xpos : array-like, optional If provided, evaluate the trace set at these positions. Otherwise the positions will be constructed from the trace set object iself. ignore_jump : bool, optional If ``True``, ignore any jump information in the `tset` object Returns ------- :func:`tuple` of array-like The x, y positions. """ return tset.xy(xpos, ignore_jump)
[docs]def xy2traceset(xpos, ypos, **kwargs): """Convert from x,y positions to a trace set. Parameters ---------- xpos, ypos : array-like X,Y positions corresponding as [nx,Ntrace] arrays. invvar : array-like, optional Inverse variances for fitting. func : :class:`str`, optional Function type for fitting; defaults to 'legendre'. ncoeff : :class:`int`, optional Number of coefficients to fit. Defaults to 3. xmin, xmax : :class:`float`, optional Explicitly set minimum and maximum values, instead of computing them from `xpos`. maxiter : :class:`int`, optional Maximum number of rejection iterations; set to 0 for no rejection; default to 10. inmask : array-like, optional Mask set to 1 for good points and 0 for rejected points; same dimensions as `xpos`, `ypos`. Points rejected by `inmask` are always rejected from the fits (the rejection is "sticky"), and will also be marked as rejected in the outmask attribute. ia, inputans, inputfunc : array-like, optional These arguments will be passed to :func:`func_fit`. xjumplo : :class:`float`, optional x position locating start of an x discontinuity xjumphi : :class:`float`, optional x position locating end of that x discontinuity xjumpval : :class:`float`, optional magnitude of the discontinuity "jump" between those bounds (previous 3 keywords motivated by BOSS 2-phase readout) Returns ------- :class:`TraceSet` A :class:`TraceSet` object. """ return TraceSet(xpos, ypos, **kwargs)
[docs]def djs_reject(data, model, outmask=None, inmask=None, invvar=None, lower=None, upper=None, percentile=False, maxdev=None, maxrej=None, groupdim=None, groupsize=None, groupbadpix=False, grow=0, sticky=False, use_mad=False): """Routine to reject points when doing an iterative fit to data. Parameters ---------- data : :class:`numpy.ndarray` The data model : :class:`numpy.ndarray` The model, must have the same number of dimensions as `data`. outmask : :class:`numpy.ndarray`, optional Output mask, generated by a previous call to `djs_reject`. If sticky=True, then bad points accumulate in this mask between calls. Otherwise, this mask is only used to determine if the rejection iterations are complete (e.g. to set qdone). Although this parameter is technically optional, it will almost always be set. If not supplied, this mask will be initialized to a mask that masks nothing and qdone will always be returned as True. inmask : :class:`numpy.ndarray`, optional Input mask. Bad points are marked with a value that evaluates to ``False``. Must have the same number of dimensions as `data`. Points masked as bad "False" in the inmask will also always evaluate to "False" in the outmask invvar : :class:`numpy.ndarray`, optional Inverse variance of the data, used to reject points based on the values of `upper` and `lower`. lower : :class:`int` or :class:`float`, optional If set, reject points with data < model - lower * sigm, where sigma = 1.0/sqrt(invvar) upper : :class:`int` or :class:`float`, optional If set, reject points with data > model + upper * sigma, where sigma = 1.0/sqrt(invvar) maxdev : :class:`int` or :class:`float`, optional If set, reject points with abs(data-model) > maxdev. It is permitted to set all three of `lower`, `upper` and `maxdev`. maxrej: :class:`int` or :class:`numpy.ndarray`, optional Maximum number of points to reject in this iteration. If `groupsize` or `groupdim` are set to arrays, this should be an array as well. groupdim: class: `int` Dimension along which to group the data; set to 1 to group along the 1st dimension, 2 for the 2nd dimension, etc. If data has shape [100,200], then setting GROUPDIM=2 is equivalent to grouping the data with groupsize=100. In either case, there are 200 groups, specified by ``[*,i]``. NOT WELL TESTED IN PYTHON! groupsize: class: `int` If this and maxrej are set, then reject a maximum of maxrej points per group of groupsize points, where the grouping is performed in the along the dimension of the data vector. (For use in curve fitting, one probably wants to make sure that data is sorted according to the indpeendent variable. For multi-dimensional arrays where one desires this grouping along each dimension, then groupdim should be set. If groupdim is also set, then this specifies sub-groups within that. groupbadpix : :class:`bool`, optional If set to ``True``, consecutive sets of bad pixels are considered groups, overriding the values of `groupsize`. grow : :class:`int`, optional, default = 0 If set to a non-zero integer, N, the N nearest neighbors of rejected pixels will also be rejected. sticky : :class:`bool`, optional If set to True then points rejected in outmask from a previous call to djs_reject are kept rejected. If set to False, if a fit (model) changes between iterations, points can alternate from being rejected to not rejected. use_mad : :class: `bool`, optional, defaul = False It set to ``True``, compute the median of the maximum absolute deviation between the data and use this for the rejection instead of the default which is to compute the standard deviation of the yarray - modelfit. Note that it is not possible to specify use_mad=True and also pass in values invvar, and the code will return an error if this is done. Returns ------- outmask (np.ndarray, boolean): mask where rejected data values are ``False`` qdone (boolean): a value set to "True" if `djs_reject` believes there is no further rejection to be done. This will be set to "False" if the points marked as rejected in the outmask have changed. It will be set to "True" when the same points are rejected in outmask as from a previous call. It will also be set to "False" if model is set to None. Recall that outmask is also an optional input parameter. If it is not set, then qdone will simply return true, so outmask needs to be input from the previous iteration for the routine to do something meaningful. Raises ------ ValueError If dimensions of various inputs do not match. """ #from .misc import djs_laxisnum # # ToDO It would be nice to come up with a way to use MAD but also use the errors in the rejection, i.e. compute the rejection threhsold using the mad. if upper is None and lower is None and maxdev is None: msgs.warn('upper, lower, and maxdev are all set to None. No rejection performed since no rejection criteria were specified.') if (use_mad and (invvar is not None)): raise ValueError('use_mad can only be set to True innvar = None. This code only computes a mad' ' if errors are not input') # Create outmask setting = True for good data. # # ToDo JFH: I think it would actually make more sense for outmask be a required input parameter (named lastmask or something like that). if outmask is None: outmask = np.ones(data.shape, dtype='bool') msgs.warn('outmask was not specified as an input parameter. Cannot asess convergence of rejection -- qdone is automatically True') else: if data.shape != outmask.shape: raise ValueError('Dimensions of data and outmask do not agree.') # # Check other inputs. # if model is None: if inmask is not None: outmask = inmask return (outmask, False) else: if data.shape != model.shape: raise ValueError('Dimensions of data and model do not agree.') if inmask is not None: if data.shape != inmask.shape: raise ValueError('Dimensions of data and inmask do not agree.') if maxrej is not None: if groupdim is not None: if len(maxrej) != len(groupdim): raise ValueError('maxrej and groupdim must have the same number of elements.') else: groupdim = [] if groupsize is not None: if isinstance(maxrej, (int,float)) | isinstance(groupsize, (int,float)): groupsize1=np.asarray([groupsize]) else: if len(maxrej) != len(groupsize): raise ValueError('maxrej and groupsize must have the same number of elements.') groupsize1=groupsize else: groupsize1 = np.asarray([len(data)]) if isinstance(maxrej,(int,float)): maxrej1 = np.asarray([maxrej]) else: maxrej1 = maxrej if invvar is None: if inmask is not None: igood = (inmask & outmask) else: igood = outmask if (np.sum(igood) > 1): if use_mad is True: sigma = 1.4826*np.median(np.abs(data[igood] - model[igood])) else: sigma = np.std(data[igood] - model[igood]) invvar = utils.inverse(sigma**2) else: invvar = 0.0 diff = data - model chi = diff * np.sqrt(invvar) # # The working array is badness, which is set to zero for good points # (or points already rejected), and positive values for bad points. # The values determine just how bad a point is, either corresponding # to the number of sigma above or below the fit, or to the number # of multiples of maxdev away from the fit. # badness = np.zeros(outmask.shape, dtype=data.dtype) if percentile: if inmask is not None: igood = (inmask & outmask) else: igood = outmask if (np.sum(igood)> 1): if lower is not None: lower_chi = np.percentile(chi[igood],lower) else: lower_chi = -np.inf if upper is not None: upper_chi = np.percentile(chi[igood], upper) else: upper_chi = np.inf # # Decide how bad a point is according to lower. # if lower is not None: if percentile: qbad = chi < lower_chi else: qbad = chi < -lower badness += np.fmax(-chi,0.0)*qbad # # Decide how bad a point is according to upper. # if upper is not None: if percentile: qbad = chi > upper_chi else: qbad = chi > upper badness += np.fmax(chi,0.0)*qbad # # Decide how bad a point is according to maxdev. # if maxdev is not None: qbad = np.absolute(diff) > maxdev badness += np.absolute(diff) / maxdev * qbad # # Do not consider rejecting points that are already rejected by inmask. # Do not consider rejecting points that are already rejected by outmask, # if sticky is set. # if inmask is not None: badness *= inmask if sticky: badness *= outmask # # Reject a maximum of maxrej (additional) points in all the data, or # in each group as specified by groupsize, and optionally along each # dimension specified by groupdim. # if maxrej is not None: # # Loop over each dimension of groupdim or loop once if not set. # for iloop in range(max(len(groupdim), 1)): # # Assign an index number in this dimension to each data point. # if len(groupdim) > 0: yndim = len(data.shape) if groupdim[iloop] > yndim: raise ValueError('groupdim is larger than the number of dimensions for ydata.') dimnum = djs_laxisnum(data.shape, iaxis=groupdim[iloop]-1) else: dimnum = np.asarray([0]) # # Loop over each vector specified by groupdim. For example, if # this is a 2-D array with groupdim=1, then loop over each # column of the data. If groupdim=2, then loop over each row. # If groupdim is not set, then use the whole image. # for ivec in range(np.fmax(dimnum.max(),1)): # # At this point it is not possible that dimnum is not set. # if len(groupdim) == 0: indx = np.arange(data.size) else: indx = (dimnum == ivec).nonzero()[0] # # Within this group of points, break it down into groups # of points specified by groupsize, if set. # nin = len(indx) if groupbadpix: goodtemp = badness == 0 groups_lower = (-1*np.diff(np.insert(goodtemp, 0, 1)) == 1).nonzero()[0] groups_upper = (np.diff(np.append(goodtemp, 1)) == 1).nonzero()[0] ngroups = len(groups_lower) else: # The IDL version of this test makes no sense because # groupsize will always be set. # if False: ngroups = 1 groups_lower = [0, ] groups_upper = [nin - 1, ] else: ngroups = nin//groupsize1[iloop] + 1 groups_lower = np.arange(ngroups, dtype='i4')*groupsize1[iloop] foo = (np.arange(ngroups, dtype='i4')+1)*groupsize1[iloop] groups_upper = np.where(foo < nin, foo, nin) - 1 for igroup in range(ngroups): i1 = groups_lower[igroup] i2 = groups_upper[igroup] nii = i2 - i1 + 1 # # Need the test that i1 != -1 below to prevent a crash # condition, but why is it that we ever get groups # without any points? Because this is badly-written, # that's why. # if nii > 0 and i1 != -1: jj = indx[i1:i2+1] # # Test if too many points rejected in this group. # if np.sum(badness[jj] != 0) > maxrej1[iloop]: isort = badness[jj].argsort() # # Make the following points good again. # badness[jj[isort[0:nii-maxrej1[iloop]]]] = 0 i1 += groupsize1[iloop] # # Now modify outmask, rejecting points specified by inmask=0, outmask=0 # if sticky is set, or badness > 0. # # print(badness) newmask = badness == 0.0 # print(newmask) if grow > 0: bpm = np.logical_not(newmask) if bpm.any(): irejects = np.where(bpm)[0] for k in range(1, grow+1): newmask[np.clip(irejects - k, 0,None)] = False newmask[np.clip(irejects + k, None, data.shape[0]-1)] = False if inmask is not None: newmask &= inmask if sticky: newmask &= outmask # # Set qdone if the input outmask is identical to the output outmask. # qdone = bool(np.all(newmask == outmask)) # JFH This needs to be a python (rather than a numpy) boolean to avoid painful problems when comparing # to python True and False booleans outmask = newmask return outmask, qdone
[docs]def djs_laxisnum(dims, iaxis=0): """Returns an integer array where each element of the array is set equal to its index number in the specified axis. Parameters ---------- dims : :class:`list` Dimensions of the array to return. iaxis : :class:`int`, optional Index along this dimension. Returns ------- :class:`numpy.ndarray` An array of indexes with ``dtype=int32``. Raises ------ ValueError If `iaxis` is greater than or equal to the number of dimensions, or if number of dimensions is greater than three. Notes ----- For two or more dimensions, there is no difference between this routine and :func:`~pydl.pydlutils.misc.djs_laxisgen`. Examples -------- >>> from pydl.pydlutils.misc import djs_laxisnum >>> print(djs_laxisnum([4,4])) [[0 0 0 0] [1 1 1 1] [2 2 2 2] [3 3 3 3]] """ ndimen = len(dims) result = np.zeros(dims, dtype='i4') if ndimen == 1: pass elif ndimen == 2: if iaxis == 0: for k in range(dims[0]): result[k, :] = k elif iaxis == 1: for k in range(dims[1]): result[:, k] = k else: raise ValueError("Bad value for iaxis: {0:d}".format(iaxis)) elif ndimen == 3: if iaxis == 0: for k in range(dims[0]): result[k, :, :] = k elif iaxis == 1: for k in range(dims[1]): result[:, k, :] = k elif iaxis == 2: for k in range(dims[2]): result[:, :, k] = k else: raise ValueError("Bad value for iaxis: {0:d}".format(iaxis)) else: raise ValueError("{0:d} dimensions not supported.".format(ndimen)) return result
[docs]def djs_laxisgen(dims, iaxis=0): """Returns an integer array where each element of the array is set equal to its index number along the specified axis. Parameters ---------- dims : :class:`list` Dimensions of the array to return. iaxis : :class:`int`, optional Index along this dimension. Returns ------- :class:`numpy.ndarray` An array of indexes with ``dtype=int32``. Raises ------ ValueError If `iaxis` is greater than or equal to the number of dimensions. Notes ----- For two or more dimensions, there is no difference between this routine and :func:`~pydl.pydlutils.misc.djs_laxisnum`. Examples -------- >>> from pydl.pydlutils.misc import djs_laxisgen >>> print(djs_laxisgen([4,4])) [[0 0 0 0] [1 1 1 1] [2 2 2 2] [3 3 3 3]] """ ndimen = len(dims) if ndimen == 1: return np.arange(dims[0], dtype='i4') return djs_laxisnum(dims, iaxis)
### Following part are imported from pydl spheregroup
[docs]class chunks(object): """chunks class Functions for creating and manipulating spherical chunks are implemented as methods on this class. """ def __init__(self, ra, dec, minSize): """Init creates an object whose attributes are similar those created by the setchunks() function in the spheregroup library. """ # # Save the value of minSize # self.minSize = minSize # # Find maximum and minimum dec (in degrees) # decMin = dec.min() decMax = dec.max() decRange = decMax - decMin # # Find the declination boundaries; make them an integer multiple of # minSize, with extra room (one cell) on the edges. # self.nDec = 3 + int(np.floor(decRange/minSize)) decRange = minSize*float(self.nDec) decMin = decMin - 0.5*(decRange - decMax + decMin) decMax = decMin + decRange if decMin < -90.0 + 3.0*minSize: decMin = -90.0 if decMax > 90.0 - 3.0*minSize: decMax = 90.0 self.decBounds = decMin + ((decMax - decMin) * np.arange(self.nDec + 1, dtype='d'))/float(self.nDec) # # Find ra offset which minimizes the range in ra (this should take care # of the case that ra crosses zero in some parts # if abs(self.decBounds[self.nDec]) > abs(self.decBounds[0]): cosDecMin = np.cos(np.deg2rad(self.decBounds[self.nDec])) else: cosDecMin = np.cos(np.deg2rad(self.decBounds[0])) if cosDecMin <= 0.0: msgs.error("cosDecMin={0:f} not positive in setchunks().".format(cosDecMin)) self.raRange, self.raOffset = self.rarange(ra, minSize/cosDecMin) self.raMin, self.raMax = self.getraminmax(ra, self.raOffset) # # Isn't this redundant? # self.raRange = self.raMax - self.raMin # # For each declination slice, find the number of ra divisions # necessary and set them # self.raBounds = list() self.nRa = list() for i in range(self.nDec): # # Get maximum declination and its cosine # if abs(self.decBounds[i]) > abs(self.decBounds[i+1]): cosDecMin = np.cos(np.deg2rad(self.decBounds[i])) else: cosDecMin = np.cos(np.deg2rad(self.decBounds[i+1])) if cosDecMin <= 0.0: msgs.error("cosDecMin={0:f} not positive in setchunks().".format(cosDecMin)) # # Get raBounds array for this declination array, leave an extra # cell on each end # self.nRa.append(3 + int(np.floor(cosDecMin*self.raRange/minSize))) raRangeTmp = minSize*float(self.nRa[i])/cosDecMin raMinTmp = self.raMin - 0.5*(raRangeTmp-self.raMax+self.raMin) raMaxTmp = raMinTmp + raRangeTmp # # If we cannot avoid the 0/360 point, embrace it # if (raRangeTmp >= 360.0 or raMinTmp <= minSize/cosDecMin or raMaxTmp >= 360.0 - minSize/cosDecMin or abs(self.decBounds[i]) == 90.0): raMinTmp = 0.0 raMaxTmp = 360.0 raRangeTmp = 360.0 if self.decBounds[i] == -90.0 or self.decBounds[i+1] == 90.0: self.nRa[i] = 1 self.raBounds.append(raMinTmp + (raMaxTmp - raMinTmp) * np.arange(self.nRa[i] + 1, dtype='d') / float(self.nRa[i])) # # Create an empty set of lists to hold the output of self.assign() # self.chunkList = [[list() for j in range(self.nRa[i])] for i in range(self.nDec)] # # nChunkMax will be the length of the largest list in chunkList # it is computed by chunks.assign() # self.nChunkMax = 0 return
[docs] def rarange(self, ra, minSize): """Finds the offset which yields the smallest raRange & returns both. Notes ----- .. warning:: This is not (yet) well-defined for the case of only one point. """ NRA = 6 raRangeMin = 361.0 raOffset = 0.0 EPS = 1.0e-5 for j in range(NRA): raMin, raMax = self.getraminmax(ra, 360.0*float(j)/float(NRA)) raRange = raMax-raMin if (2.0*(raRange-raRangeMin)/(raRange+raRangeMin) < -EPS and raMin > minSize and raMax < 360.0 - minSize): raRangeMin = raRange raOffset = 360.0*float(j)/float(NRA) return (raRangeMin, raOffset)
[docs] def getraminmax(self, ra, raOffset): """Utility function used by rarange. """ currRa = np.fmod(ra + raOffset, 360.0) return (currRa.min(), currRa.max())
[docs] def cosDecMin(self, i): """Frequently used utility function. """ if abs(self.decBounds[i]) > abs(self.decBounds[i+1]): return np.cos(np.deg2rad(self.decBounds[i])) else: return np.cos(np.deg2rad(self.decBounds[i+1]))
[docs] def assign(self, ra, dec, marginSize): """Take the objects and the chunks (already defined in the constructor) and assign the objects to the appropriate chunks, with some leeway given by the parameter marginSize. Basically, at the end, each chunk should be associated with a list of the objects that belong to it. """ if marginSize >= self.minSize: msgs.error("marginSize>=minSize ({0:f}={1:f}) in chunks.assign().".format(marginSize, self.minSize)) chunkDone = [[False for j in range(self.nRa[i])] for i in range(self.nDec)] for i in range(ra.size): currRa = np.fmod(ra[i] + self.raOffset, 360.0) try: raChunkMin, raChunkMax, decChunkMin, decChunkMax = self.getbounds(currRa, dec[i], marginSize) except: continue # # Reset chunkDone. This is silly, but is necessary to # reproduce the logic. # for decChunk in range(decChunkMin, decChunkMax+1): for raChunk in range(raChunkMin[decChunk-decChunkMin]-1, raChunkMax[decChunk-decChunkMin]+2): if raChunk < 0: currRaChunk = (raChunk+self.nRa[decChunk]) % self.nRa[decChunk] elif raChunk > self.nRa[decChunk]-1: currRaChunk = (raChunk-self.nRa[decChunk]) % self.nRa[decChunk] else: currRaChunk = raChunk if currRaChunk >= 0 and currRaChunk <= self.nRa[decChunk]-1: chunkDone[decChunk][currRaChunk] = False for decChunk in range(decChunkMin, decChunkMax+1): for raChunk in range(raChunkMin[decChunk-decChunkMin], raChunkMax[decChunk-decChunkMin]+1): if raChunk < 0: currRaChunk = (raChunk+self.nRa[decChunk]) % self.nRa[decChunk] elif raChunk > self.nRa[decChunk]-1: currRaChunk = (raChunk-self.nRa[decChunk]) % self.nRa[decChunk] else: currRaChunk = raChunk if currRaChunk >= 0 and currRaChunk <= self.nRa[decChunk]-1: if not chunkDone[decChunk][currRaChunk]: self.chunkList[decChunk][currRaChunk].append(i) # # Update nChunkMax # if len(self.chunkList[decChunk][currRaChunk]) > self.nChunkMax: self.nChunkMax = len(self.chunkList[decChunk][currRaChunk]) chunkDone[decChunk][currRaChunk] = True return
[docs] def getbounds(self, ra, dec, marginSize): """Find the set of chunks a point (with margin) belongs to. """ # # Find the declination slice without regard to marginSize # decChunkMin = int(np.floor((dec - self.decBounds[0]) * float(self.nDec) / (self.decBounds[self.nDec]-self.decBounds[0]))) decChunkMax = decChunkMin if decChunkMin < 0 or decChunkMin > self.nDec - 1: msgs.error("decChunkMin out of range in chunks.getbounds().") # # Set minimum and maximum bounds of dec # while dec - self.decBounds[decChunkMin] < marginSize and decChunkMin > 0: decChunkMin -= 1 while self.decBounds[decChunkMax+1] - dec < marginSize and decChunkMax < self.nDec - 1: decChunkMax += 1 # # Find ra chunk bounds for each dec chunk # raChunkMin = np.zeros(decChunkMax-decChunkMin+1, dtype='i4') raChunkMax = np.zeros(decChunkMax-decChunkMin+1, dtype='i4') for i in range(decChunkMin, decChunkMax+1): cosDecMin = self.cosDecMin(i) raChunkMin[i-decChunkMin] = int(np.floor((ra - self.raBounds[i][0]) * float(self.nRa[i]) / (self.raBounds[i][self.nRa[i]] - self.raBounds[i][0]))) raChunkMax[i-decChunkMin] = raChunkMin[i-decChunkMin] if raChunkMin[i-decChunkMin] < 0 or raChunkMin[i-decChunkMin] > self.nRa[i]-1: msgs.error("raChunkMin out of range in chunks.getbounds().") # # Set minimum and maximum bounds of ra # raCheck = raChunkMin[i-decChunkMin] keepGoing = True while keepGoing and raCheck > -1: if raCheck >= 0 and raCheck < self.nRa[i]: keepGoing = (ra - self.raBounds[i][raCheck])*cosDecMin < marginSize else: keepGoing = False if keepGoing: raCheck -= 1 raChunkMin[i-decChunkMin] = raCheck raCheck = raChunkMax[i-decChunkMin] keepGoing = True while keepGoing and raCheck < self.nRa[i]: if raCheck >= 0 and raCheck < self.nRa[i]: keepGoing = (self.raBounds[i][raCheck+1]-ra)*cosDecMin < marginSize else: keepGoing = False if keepGoing: raCheck += 1 raChunkMax[i-decChunkMin] = raCheck return (raChunkMin, raChunkMax, decChunkMin, decChunkMax)
[docs] def get(self, ra, dec): """Find the chunk to which a given point belongs. """ # # Find dec chunk # decChunk = int(np.floor((dec - self.decBounds[0]) * float(self.nDec) / (self.decBounds[self.nDec]-self.decBounds[0]))) # # Find ra chunk # if decChunk < self.nDec and decChunk >= 0: raChunk = int(np.floor((ra - self.raBounds[decChunk][0]) * float(self.nRa[decChunk]) / (self.raBounds[decChunk][self.nRa[decChunk]] - self.raBounds[decChunk][0]))) if raChunk < 0 or raChunk > self.nRa[decChunk]-1: msgs.error("raChunk out of range in chunks.get()") else: raChunk = -1 return (raChunk, decChunk)
[docs] def friendsoffriends(self, ra, dec, linkSep): """Friends-of-friends using chunked data. """ nPoints = ra.size inGroup = np.zeros(nPoints, dtype='i4') - 1 # # mapGroups contains an equivalency mapping of groups. mapGroup[i]=j # means i and j are actually the same group. j<=i always, by design. # The largest number of groups you can get # (assuming linkSep < marginSize < minSize) is 9 times the number of # targets # mapGroups = np.zeros(9*nPoints, dtype='i4') - 1 nMapGroups = 0 for i in range(self.nDec): for j in range(self.nRa[i]): if len(self.chunkList[i][j]) > 0: chunkGroup = self.chunkfriendsoffriends(ra, dec, self.chunkList[i][j], linkSep) for k in range(chunkGroup.nGroups): minEarly = 9*nPoints l = chunkGroup.firstGroup[k] while l != -1: if inGroup[self.chunkList[i][j][l]] != -1: checkEarly = inGroup[self.chunkList[i][j][l]] while mapGroups[checkEarly] != checkEarly: checkEarly = mapGroups[checkEarly] minEarly = min(minEarly, checkEarly) else: inGroup[self.chunkList[i][j][l]] = nMapGroups l = chunkGroup.nextGroup[l] if minEarly == 9*nPoints: mapGroups[nMapGroups] = nMapGroups else: mapGroups[nMapGroups] = minEarly l = chunkGroup.firstGroup[k] while l != -1: checkEarly = inGroup[self.chunkList[i][j][l]] while mapGroups[checkEarly] != checkEarly: tmpEarly = mapGroups[checkEarly] mapGroups[checkEarly] = minEarly checkEarly = tmpEarly mapGroups[checkEarly] = minEarly l = chunkGroup.nextGroup[l] nMapGroups += 1 # # Now all groups which are mapped to themselves are the real groups # Make sure the mappings are set up to go all the way down. # nGroups = 0 for i in range(nMapGroups): if mapGroups[i] != -1: if mapGroups[i] == i: mapGroups[i] = nGroups nGroups += 1 else: mapGroups[i] = mapGroups[mapGroups[i]] else: msgs.error("MapGroups[{0:d}]={1:d} in chunks.friendsoffriends().".format(i, mapGroups[i])) for i in range(nPoints): inGroup[i] = mapGroups[inGroup[i]] firstGroup = np.zeros(nPoints, dtype='i4') - 1 nextGroup = np.zeros(nPoints, dtype='i4') - 1 multGroup = np.zeros(nPoints, dtype='i4') for i in range(nPoints-1, -1, -1): nextGroup[i] = firstGroup[inGroup[i]] firstGroup[inGroup[i]] = i for i in range(nGroups): j = firstGroup[i] while j != -1: multGroup[i] += 1 j = nextGroup[j] return (inGroup, multGroup, firstGroup, nextGroup, nGroups)
[docs] def chunkfriendsoffriends(self, ra, dec, chunkList, linkSep): """Does friends-of-friends on the ra, dec that are defined by chunkList. """ # # Convert ra, dec into something that can be digested by the # groups object. # x = np.deg2rad(np.vstack((ra[chunkList], dec[chunkList]))) radLinkSep = np.deg2rad(linkSep) group = groups(x, radLinkSep, 'sphereradec') return group
[docs]class groups(object): """Group a set of objects (a list of coordinates in some space) based on a friends-of-friends algorithm """
[docs] @staticmethod def euclid(x1, x2): """Pythagorean theorem in Euclidean space with arbitrary number of dimensions. """ return np.sqrt(((x1-x2)**2).sum())
[docs] @staticmethod def sphereradec(x1, x2): """Separation of two points on a 2D-sphere, assuming they are in longitude-latitude or right ascension-declination form. Assumes everything is already in radians. """ return gcirc(x1[0], x1[1], x2[0], x2[1], units=0)
def __init__(self, coordinates, distance, separation='euclid'): """Init creates an object and performs the friends-of-friends algorithm. The coordinates can have arbitrary dimensions, with each column representing one of the dimensions. Each row defines an object. If separation is not defined it defaults to Euclidean space. """ # # Find a separation function # if callable(separation): self.separation = separation elif isinstance(separation, str): if separation == 'euclid': self.separation = self.euclid elif separation == 'sphereradec': self.separation = self.sphereradec else: msgs.error("Unknown separation function: {0}.".format(separation)) else: msgs.error("Improper type for separation!") # # Save information about the coordinates. # nGroups = 0 nTargets = coordinates.shape[1] multGroup = np.zeros(nTargets, dtype='i4') firstGroup = np.zeros(nTargets, dtype='i4') - 1 nextGroup = np.zeros(nTargets, dtype='i4') - 1 inGroup = np.arange(nTargets, dtype='i4') # # Find all the other targets associated with each target # for i in range(nTargets): nTmp = 0 minGroup = nGroups for j in range(nTargets): sep = self.separation(coordinates[:, i], coordinates[:, j]) if sep <= distance: multGroup[nTmp] = j minGroup = min(minGroup, inGroup[j]) nTmp += 1 # # Use this minimum for all # for j in range(nTmp): if inGroup[multGroup[j]] < nTargets: k = firstGroup[inGroup[multGroup[j]]] while k != -1: inGroup[k] = minGroup k = nextGroup[k] inGroup[multGroup[j]] = minGroup # # If it is a new group (no earlier groups), increment nGroups # if minGroup == nGroups: nGroups += 1 for j in range(i+1): firstGroup[j] = -1 for j in range(i, -1, -1): nextGroup[j] = firstGroup[inGroup[j]] firstGroup[inGroup[j]] = j # # Renumber to get rid of the numbers which were skipped # renumbered = np.zeros(nTargets, dtype='bool') nTmp = nGroups nGroups = 0 for i in range(nTargets): if not renumbered[i]: j = firstGroup[inGroup[i]] while j != -1: inGroup[j] = nGroups renumbered[j] = True j = nextGroup[j] nGroups += 1 # # Reset the values of firstGroup and inGroup # firstGroup[:] = -1 for i in range(nTargets-1, -1, -1): nextGroup[i] = firstGroup[inGroup[i]] firstGroup[inGroup[i]] = i # # Get the multiplicity # for i in range(nGroups): multGroup[i] = 0 j = firstGroup[i] while j != -1: multGroup[i] += 1 j = nextGroup[j] # # Set attributes # self.nGroups = nGroups self.nTargets = nTargets self.inGroup = inGroup self.multGroup = multGroup self.firstGroup = firstGroup self.nextGroup = nextGroup return
[docs]def spheregroup(ra, dec, linklength, chunksize=None): """Perform friends-of-friends grouping given ra/dec coordinates. Parameters ---------- ra, dec : :class:`numpy.ndarray` Arrays of coordinates to group in decimal degrees. linklength : :class:`float` Linking length for the groups in decimal degrees. chunksize : :class:`float`, optional Break up the sphere into chunks of this size in decimal degrees. Returns ------- :func:`tuple` A tuple containing the group number of each object, the multiplicity of each group, the first member of each group, and the next member of the group for each object. Raises ------ msgs.error If the array of coordinates only contains one point. Notes ----- It is important that `chunksize` >= 4 * `linklength`. This is enforced. .. warning:: Behavior at the poles is not well tested. """ npoints = ra.size if npoints == 1: msgs.error("Cannot group only one point!") # # Define the chunksize # if chunksize is not None: if chunksize < 4.0*linklength: chunksize = 4.0*linklength msgs.warn("chunksize changed to {0:.2f}.".format(chunksize)) else: chunksize = max(4.0*linklength, 0.1) # # Initialize chunks # chunk = chunks(ra, dec, chunksize) chunk.assign(ra, dec, linklength) # # Run friends-of-friends # ingroup, multgroup, firstgroup, nextgroup, ngroups = chunk.friendsoffriends(ra, dec, linklength) # # Renumber the groups in order of appearance # renumbered = np.zeros(npoints, dtype='bool') iclump = 0 for i in range(npoints): if not renumbered[i]: j = firstgroup[ingroup[i]] while j != -1: ingroup[j] = iclump renumbered[j] = True j = nextgroup[j] iclump += 1 # # Reset the index lists # firstgroup[:] = -1 for i in range(npoints-1, -1, -1): nextgroup[i] = firstgroup[ingroup[i]] firstgroup[ingroup[i]] = i # # Reset the multiplicities # multgroup[:] = 0 for i in range(ngroups): j = firstgroup[i] while j != -1: multgroup[i] += 1 j = nextgroup[j] return (ingroup, multgroup, firstgroup, nextgroup)
[docs]def spherematch(ra1, dec1, ra2, dec2, matchlength, chunksize=None, maxmatch=1): """Match points on a sphere. Parameters ---------- ra1, dec1, ra2, dec2 : :class:`numpy.ndarray` The sets of coordinates to match. Assumed to be in decimal degrees matchlength : :class:`float` Two points closer than this separation are matched. Assumed to be in decimal degrees. chunksize : :class:`float`, optional Value to pass to chunk assignment. maxmatch : :class:`int`, optional Allow up to `maxmatch` matches per coordinate. Default 1. If set to zero, All possible matches will be returned. Returns ------- :func:`tuple` A tuple containing the indices into the first set of points, the indices into the second set of points and the match distance in decimal degrees. Notes ----- If you have sets of coordinates that differ in size, call this function with the larger list first. This exploits the inherent asymmetry in the underlying code to reduce memory use. .. warning:: Behavior at the poles is not well tested. """ # # Set default values # if chunksize is None: chunksize = max(4.0*matchlength, 0.1) # # Check input size # if ra1.size == 1: msgs.error("Change the order of the sets of coordinates!") # # Initialize chunks # chunk = chunks(ra1, dec1, chunksize) chunk.assign(ra2, dec2, matchlength) # # Create return arrays # match1 = list() match2 = list() distance12 = list() for i in range(ra1.size): currra = np.fmod(ra1[i]+chunk.raOffset, 360.0) rachunk, decchunk = chunk.get(currra, dec1[i]) jmax = len(chunk.chunkList[decchunk][rachunk]) if jmax > 0: for j in range(jmax): k = chunk.chunkList[decchunk][rachunk][j] sep = gcirc(ra1[i], dec1[i], ra2[k], dec2[k], units=2)/3600.0 if sep < matchlength: match1.append(i) match2.append(k) distance12.append(sep) # # Sort distances # omatch1 = np.array(match1) omatch2 = np.array(match2) odistance12 = np.array(distance12) s = odistance12.argsort() # # Retain only desired matches # if maxmatch > 0: gotten1 = np.zeros(ra1.size, dtype='i4') gotten2 = np.zeros(ra2.size, dtype='i4') nmatch = 0 for i in range(omatch1.size): if (gotten1[omatch1[s[i]]] < maxmatch and gotten2[omatch2[s[i]]] < maxmatch): gotten1[omatch1[s[i]]] += 1 gotten2[omatch2[s[i]]] += 1 nmatch += 1 match1 = np.zeros(nmatch, dtype='i4') match2 = np.zeros(nmatch, dtype='i4') distance12 = np.zeros(nmatch, dtype='d') gotten1[:] = 0 gotten2[:] = 0 nmatch = 0 for i in range(omatch1.size): if (gotten1[omatch1[s[i]]] < maxmatch and gotten2[omatch2[s[i]]] < maxmatch): gotten1[omatch1[s[i]]] += 1 gotten2[omatch2[s[i]]] += 1 match1[nmatch] = omatch1[s[i]] match2[nmatch] = omatch2[s[i]] distance12[nmatch] = odistance12[s[i]] nmatch += 1 else: match1 = omatch1[s] match2 = omatch2[s] distance12 = odistance12[s] return (match1, match2, distance12)
[docs]def gcirc(ra1, dec1, ra2, dec2, units=2): """Computes rigorous great circle arc distances. Parameters ---------- ra1, dec1, ra2, dec2 : :class:`float` or array-like RA and Dec of two points. units : { 0, 1, 2 }, optional * units = 0: everything is already in radians * units = 1: RA in hours, dec in degrees, distance in arcsec. * units = 2: RA, dec in degrees, distance in arcsec (default) Returns ------- :class:`float` or array-like The angular distance. Units of the value returned depend on the input value of `units`. Notes ----- The formula below is the one best suited to handling small angular separations. See: http://en.wikipedia.org/wiki/Great-circle_distance """ from numpy import arcsin, cos, deg2rad, rad2deg, sin, sqrt if units == 0: rarad1 = ra1 dcrad1 = dec1 rarad2 = ra2 dcrad2 = dec2 elif units == 1: rarad1 = deg2rad(15.0*ra1) dcrad1 = deg2rad(dec1) rarad2 = deg2rad(15.0*ra2) dcrad2 = deg2rad(dec2) elif units == 2: rarad1 = deg2rad(ra1) dcrad1 = deg2rad(dec1) rarad2 = deg2rad(ra2) dcrad2 = deg2rad(dec2) else: raise ValueError('units must be 0, 1 or 2!') deldec2 = (dcrad2-dcrad1)/2.0 delra2 = (rarad2-rarad1)/2.0 sindis = sqrt(sin(deldec2)*sin(deldec2) + cos(dcrad1)*cos(dcrad2)*sin(delra2)*sin(delra2)) dis = 2.0*arcsin(sindis) if units == 0: return dis else: return rad2deg(dis)*3600.0
### Above part are imported from pydl spheregroup