def setUp(self): # make a nominal match list where the distances are 0; test can then modify # source centroid, reference coord or distance field for each match, as desired self.wcs = afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(1500, 1500), crval=lsst.geom.SpherePoint(215.5, 53.0, lsst.geom.degrees), cdMatrix=afwGeom.makeCdMatrix(scale=5.1e-5*lsst.geom.degrees)) self.bboxD = lsst.geom.Box2D(lsst.geom.Point2D(10, 100), lsst.geom.Extent2D(1000, 1500)) self.numMatches = 25 sourceSchema = afwTable.SourceTable.makeMinimalSchema() # add centroid (and many other unwanted fields) to sourceSchema SingleFrameMeasurementTask(schema=sourceSchema) self.sourceCentroidKey = afwTable.Point2DKey(sourceSchema["slot_Centroid"]) self.sourceCat = afwTable.SourceCatalog(sourceSchema) refSchema = afwTable.SourceTable.makeMinimalSchema() self.refCoordKey = afwTable.CoordKey(refSchema["coord"]) self.refCat = afwTable.SourceCatalog(refSchema) self.matchList = [] np.random.seed(5) pixPointList = [lsst.geom.Point2D(pos) for pos in np.random.random_sample([self.numMatches, 2])*self.bboxD.getDimensions() + self.bboxD.getMin()] for pixPoint in pixPointList: src = self.sourceCat.addNew() src.set(self.sourceCentroidKey, pixPoint) ref = self.refCat.addNew() ref.set(self.refCoordKey, self.wcs.pixelToSky(pixPoint)) match = afwTable.ReferenceMatch(ref, src, 0) self.matchList.append(match)
def setUp(self): # make a nominal match list where the distances are 0; test can then modify # source centroid, reference coord or distance field for each match, as desired ctrPix = afwGeom.Point2I(1500, 1500) metadata = PropertySet() metadata.set("RADECSYS", "FK5") metadata.set("EQUINOX", 2000.0) metadata.set("CTYPE1", "RA---TAN") metadata.set("CTYPE2", "DEC--TAN") metadata.set("CUNIT1", "deg") metadata.set("CUNIT2", "deg") metadata.set("CRVAL1", 215.5) metadata.set("CRVAL2", 53.0) metadata.set("CRPIX1", ctrPix[0] + 1) metadata.set("CRPIX2", ctrPix[1] + 1) metadata.set("CD1_1", 5.1e-05) metadata.set("CD1_2", 0.0) metadata.set("CD2_2", -5.1e-05) metadata.set("CD2_1", 0.0) self.wcs = afwImage.makeWcs(metadata) self.bboxD = afwGeom.Box2D(afwGeom.Point2D(10, 100), afwGeom.Extent2D(1000, 1500)) self.numMatches = 25 sourceSchema = afwTable.SourceTable.makeMinimalSchema() # add centroid (and many other unwanted fields) to sourceSchema SingleFrameMeasurementTask(schema=sourceSchema) self.sourceCentroidKey = afwTable.Point2DKey( sourceSchema["slot_Centroid"]) self.sourceCat = afwTable.SourceCatalog(sourceSchema) refSchema = afwTable.SourceTable.makeMinimalSchema() self.refCoordKey = afwTable.CoordKey(refSchema["coord"]) self.refCat = afwTable.SourceCatalog(refSchema) self.matchList = [] np.random.seed(5) pixPointList = [ afwGeom.Point2D(pos) for pos in np.random.random_sample([self.numMatches, 2]) * self.bboxD.getDimensions() + self.bboxD.getMin() ] for pixPoint in pixPointList: src = self.sourceCat.addNew() src.set(self.sourceCentroidKey, pixPoint) ref = self.refCat.addNew() ref.set(self.refCoordKey, self.wcs.pixelToSky(pixPoint)) match = afwTable.ReferenceMatch(ref, src, 0) self.matchList.append(match)
def getSipWcsFromCorrespondences(self, origWcs, refCat, sourceCat, bbox): """Produce a SIP solution given a list of known correspondences. Unlike _calculateSipTerms, this does not iterate the solution; it assumes you have given it a good sets of corresponding stars. NOTE that "refCat" and "sourceCat" are assumed to be the same length; entries "refCat[i]" and "sourceCat[i]" are assumed to be correspondences. @param[in] origWcs the WCS to linearize in order to get the TAN part of the TAN-SIP WCS. @param[in] refCat reference source catalog @param[in] sourceCat source catalog @param[in] bbox bounding box of image """ sipOrder = self.config.sipOrder matches = [] for ci, si in zip(refCat, sourceCat): matches.append(afwTable.ReferenceMatch(ci, si, 0.)) sipObject = astromSip.makeCreateWcsWithSip(matches, origWcs, sipOrder, bbox) return sipObject.getNewWcs()
def createWcs(x, y, mapper, order=4, cOffset=1.0): # Here cOffset reflects the differences between FITS coords (LLC = # 1,1) and LSST coords (LLC = 0,0). That is, when creating a Wcs # from scratch, we need to evaluate our WCS at coordinate 0,0 to # create CRVAL, but set CRPIX to 1,1. ra_rad, dec_rad = mapper.xyToRaDec(x, y) # Minimial table for sky coordinates catTable = afwTable.SimpleTable.make( afwTable.SimpleTable.makeMinimalSchema()) # Minimial table + centroids for focal plane coordintes srcSchema = afwTable.SourceTable.makeMinimalSchema() centroidKey = afwTable.Point2DKey.addFields(srcSchema, "centroid", "centroid", "pixel") srcTable = afwTable.SourceTable.make(srcSchema) srcTable.defineCentroid("centroid") matches = [] for i in range(len(x)): src = srcTable.makeRecord() src.set(centroidKey.getX(), x[i]) src.set(centroidKey.getY(), y[i]) cat = catTable.makeRecord() cat.set(catTable.getCoordKey().getRa(), afwGeom.Angle(ra_rad[i], afwGeom.radians)) cat.set(catTable.getCoordKey().getDec(), afwGeom.Angle(dec_rad[i], afwGeom.radians)) mat = afwTable.ReferenceMatch(cat, src, 0.0) matches.append(mat) # Need to make linear Wcs around which to expand solution # CRPIX1 = Column Pixel Coordinate of Ref. Pixel # CRPIX2 = Row Pixel Coordinate of Ref. Pixel crpix = afwGeom.Point2D(x[0] + cOffset, y[0] + cOffset) # CRVAL1 = RA at Reference Pixel # CRVAL2 = DEC at Reference Pixel crval = afwCoord.Coord(afwGeom.Point2D(ra_rad[0], dec_rad[0]), afwGeom.radians) # CD1_1 = RA degrees per column pixel # CD1_2 = RA degrees per row pixel # CD2_1 = DEC degrees per column pixel # CD2_2 = DEC degrees per row pixel LLl = mapper.xyToRaDec(0., 0.) ULl = mapper.xyToRaDec(0., 1.) LRl = mapper.xyToRaDec(1., 0.) LLc = afwCoord.Coord(afwGeom.Point2D(LLl[0], LLl[1]), afwGeom.radians) ULc = afwCoord.Coord(afwGeom.Point2D(ULl[0], ULl[1]), afwGeom.radians) LRc = afwCoord.Coord(afwGeom.Point2D(LRl[0], LRl[1]), afwGeom.radians) cdN_1 = LLc.getTangentPlaneOffset(LRc) cdN_2 = LLc.getTangentPlaneOffset(ULc) cd1_1, cd2_1 = cdN_1[0].asDegrees(), cdN_1[1].asDegrees() cd1_2, cd2_2 = cdN_2[0].asDegrees(), cdN_2[1].asDegrees() linearWcs = afwImage.makeWcs(crval, crpix, cd1_1, cd2_1, cd1_2, cd2_2) wcs = sip.makeCreateWcsWithSip(matches, linearWcs, order).getNewWcs() return wcs
def approximateWcs(wcs, bbox, order=3, nx=20, ny=20, iterations=3, skyTolerance=0.001 * afwGeom.arcseconds, pixelTolerance=0.02, useTanWcs=False): """Approximate an existing WCS as a TAN-SIP WCS The fit is performed by evaluating the WCS at a uniform grid of points within a bounding box. @param[in] wcs wcs to approximate @param[in] bbox the region over which the WCS will be fit @param[in] order order of SIP fit @param[in] nx number of grid points along x @param[in] ny number of grid points along y @param[in] iterations number of times to iterate over fitting @param[in] skyTolerance maximum allowed difference in world coordinates between input wcs and approximate wcs (default is 0.001 arcsec) @param[in] pixelTolerance maximum allowed difference in pixel coordinates between input wcs and approximate wcs (default is 0.02 pixels) @param[in] useTanWcs send a TAN version of wcs to the fitter? It is documented to require that, but I don't think the fitter actually cares @return the fit TAN-SIP WCS """ if useTanWcs: crCoord = wcs.getSkyOrigin() crPix = wcs.getPixelOrigin() cdMat = wcs.getCDMatrix() tanWcs = afwImage.makeWcs(crCoord, crPix, cdMat[0, 0], cdMat[0, 1], cdMat[1, 0], cdMat[1, 1]) else: tanWcs = wcs # create a matchList consisting of a grid of points covering the bbox refSchema = afwTable.SimpleTable.makeMinimalSchema() refCoordKey = afwTable.CoordKey(refSchema["coord"]) refCat = afwTable.SimpleCatalog(refSchema) sourceSchema = afwTable.SourceTable.makeMinimalSchema() SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema sourceCentroidKey = afwTable.Point2DKey(sourceSchema["slot_Centroid"]) sourceCat = afwTable.SourceCatalog(sourceSchema) matchList = [] bboxd = afwGeom.Box2D(bbox) for x in np.linspace(bboxd.getMinX(), bboxd.getMaxX(), nx): for y in np.linspace(bboxd.getMinY(), bboxd.getMaxY(), ny): pixelPos = afwGeom.Point2D(x, y) skyCoord = wcs.pixelToSky(pixelPos) refObj = refCat.addNew() refObj.set(refCoordKey, skyCoord) source = sourceCat.addNew() source.set(sourceCentroidKey, pixelPos) matchList.append(afwTable.ReferenceMatch(refObj, source, 0.0)) # The TAN-SIP fitter is fitting x and y separately, so we have to iterate to make it converge for indx in range(iterations): sipObject = makeCreateWcsWithSip(matchList, tanWcs, order, bbox) tanWcs = sipObject.getNewWcs() fitWcs = sipObject.getNewWcs() mockTest = _MockTestCase() assertWcsAlmostEqualOverBBox(mockTest, wcs, fitWcs, bbox, maxDiffSky=skyTolerance, maxDiffPix=pixelTolerance) return fitWcs
def approximateWcs(wcs, bbox, camera=None, detector=None, obs_metadata=None, order=3, nx=20, ny=20, iterations=3, skyTolerance=0.001 * afwGeom.arcseconds, pixelTolerance=0.02, useTanWcs=False): """Approximate an existing WCS as a TAN-SIP WCS The fit is performed by evaluating the WCS at a uniform grid of points within a bounding box. @param[in] wcs wcs to approximate @param[in] bbox the region over which the WCS will be fit @param[in] camera is an instantiation of afw.cameraGeom.camera @param[in] detector is a detector from camera @param[in] obs_metadata is an ObservationMetaData characterizing the telescope pointing @param[in] order order of SIP fit @param[in] nx number of grid points along x @param[in] ny number of grid points along y @param[in] iterations number of times to iterate over fitting @param[in] skyTolerance maximum allowed difference in world coordinates between input wcs and approximate wcs (default is 0.001 arcsec) @param[in] pixelTolerance maximum allowed difference in pixel coordinates between input wcs and approximate wcs (default is 0.02 pixels) @param[in] useTanWcs send a TAN version of wcs to the fitter? It is documented to require that, but I don't think the fitter actually cares @return the fit TAN-SIP WCS """ if useTanWcs: crCoord = wcs.getSkyOrigin() crPix = wcs.getPixelOrigin() cdMat = wcs.getCDMatrix() tanWcs = afwImage.makeWcs(crCoord, crPix, cdMat[0, 0], cdMat[0, 1], cdMat[1, 0], cdMat[1, 1]) else: tanWcs = wcs # create a matchList consisting of a grid of points covering the bbox refSchema = afwTable.SimpleTable.makeMinimalSchema() refCoordKey = afwTable.CoordKey(refSchema["coord"]) refCat = afwTable.SimpleCatalog(refSchema) sourceSchema = afwTable.SourceTable.makeMinimalSchema() SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema sourceCentroidKey = afwTable.Point2DKey(sourceSchema["slot_Centroid"]) sourceCat = afwTable.SourceCatalog(sourceSchema) # 20 March 2017 # the 'try' block is how it works in swig; # the 'except' block is how it works in pybind11 try: matchList = afwTable.ReferenceMatchVector() except AttributeError: matchList = [] bboxd = afwGeom.Box2D(bbox) for x in np.linspace(bboxd.getMinX(), bboxd.getMaxX(), nx): for y in np.linspace(bboxd.getMinY(), bboxd.getMaxY(), ny): pixelPos = afwGeom.Point2D(x, y) ra, dec = raDecFromPixelCoords(np.array([x]), np.array([y]), [detector.getName()], camera=camera, obs_metadata=obs_metadata, epoch=2000.0, includeDistortion=True) skyCoord = afwCoord.Coord(afwGeom.Point2D(ra[0], dec[0])) refObj = refCat.addNew() refObj.set(refCoordKey, skyCoord) source = sourceCat.addNew() source.set(sourceCentroidKey, pixelPos) matchList.append(afwTable.ReferenceMatch(refObj, source, 0.0)) # The TAN-SIP fitter is fitting x and y separately, so we have to iterate to make it converge for indx in range(iterations): sipObject = makeCreateWcsWithSip(matchList, tanWcs, order, bbox) tanWcs = sipObject.getNewWcs() fitWcs = sipObject.getNewWcs() return fitWcs
def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, match_tolerance, sourceFluxField, verbose): """Implementation of matching sources to position reference objects Unlike matchObjectsToSources, this method does not check if the sources are suitable. Parameters ---------- refCat : `lsst.afw.table.SimpleCatalog` catalog of position reference objects that overlap an exposure sourceCat : `lsst.afw.table.SourceCatalog` catalog of sources found on the exposure wcs : `lsst.afw.geom.SkyWcs` estimated WCS of exposure refFluxField : `str` field of refCat to use for flux numUsableSources : `int` number of usable sources (sources with known centroid that are not near the edge, but may be saturated) minMatchedPairs : `int` minimum number of matches match_tolerance : `lsst.meas.astrom.MatchTolerancePessimistic` a MatchTolerance object containing variables specifying matcher tolerances and state from possible previous runs. sourceFluxField : `str` Name of the flux field in the source catalog. verbose : `bool` Set true to print diagnostic information to std::cout Returns ------- result : Results struct with components: - ``matches`` : a list the matches found (`list` of `lsst.afw.table.ReferenceMatch`). - ``match_tolerance`` : MatchTolerance containing updated values from this fit iteration (`lsst.meas.astrom.MatchTolerancePessimistic`) """ # Load the source and reference catalog as spherical points # in numpy array. We do this rather than relying on internal # lsst C objects for simplicity and because we require # objects contiguous in memory. We need to do these slightly # differently for the reference and source cats as they are # different catalog objects with different fields. src_array = np.empty((len(sourceCat), 4), dtype=np.float64) for src_idx, srcObj in enumerate(sourceCat): coord = wcs.pixelToSky(srcObj.getCentroid()) theta = np.pi / 2 - coord.getLatitude().asRadians() phi = coord.getLongitude().asRadians() flux = srcObj[sourceFluxField] src_array[src_idx, :] = \ self._latlong_flux_to_xyz_mag(theta, phi, flux) if match_tolerance.PPMbObj is None or \ match_tolerance.autoMaxMatchDist is None: # The reference catalog is fixed per AstrometryTask so we only # create the data needed if this is the first step in the match # fit cycle. ref_array = np.empty((len(refCat), 4), dtype=np.float64) for ref_idx, refObj in enumerate(refCat): theta = np.pi / 2 - refObj.getDec().asRadians() phi = refObj.getRa().asRadians() flux = refObj[refFluxField] ref_array[ref_idx, :] = \ self._latlong_flux_to_xyz_mag(theta, phi, flux) # Create our matcher object. match_tolerance.PPMbObj = PessimisticPatternMatcherB( ref_array[:, :3], self.log) self.log.debug("Computing source statistics...") maxMatchDistArcSecSrc = self._get_pair_pattern_statistics( src_array) self.log.debug("Computing reference statistics...") maxMatchDistArcSecRef = self._get_pair_pattern_statistics( ref_array) maxMatchDistArcSec = np.max(( self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds(), np.min((maxMatchDistArcSecSrc, maxMatchDistArcSecRef)))) match_tolerance.autoMaxMatchDist = geom.Angle( maxMatchDistArcSec, geom.arcseconds) # Set configurable defaults when we encounter None type or set # state based on previous run of AstrometryTask._matchAndFitWcs. if match_tolerance.maxShift is None: maxShiftArcseconds = (self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds()) else: # We don't want to clamp down too hard on the allowed shift so # we test that the smallest we ever allow is the pixel scale. maxShiftArcseconds = np.max( (match_tolerance.maxShift.asArcseconds(), self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds())) # If our tolerances are not set from a previous run, estimate a # starting tolerance guess from the statistics of patterns we can # create on both the source and reference catalog. We use the smaller # of the two. if match_tolerance.maxMatchDist is None: match_tolerance.maxMatchDist = match_tolerance.autoMaxMatchDist else: maxMatchDistArcSec = np.max( (self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds(), np.min((match_tolerance.maxMatchDist.asArcseconds(), match_tolerance.autoMaxMatchDist.asArcseconds())))) # Make sure the data we are considering is dense enough to require # the consensus mode of the matcher. If not default to Optimistic # pattern matcher behavior. We enforce pessimistic mode if the # reference cat is sufficiently large, avoiding false positives. numConsensus = self.config.numPatternConsensus if len(refCat) < self.config.numRefRequireConsensus: minObjectsForConsensus = \ self.config.numBrightStars + \ self.config.numPointsForShapeAttempt if len(refCat) < minObjectsForConsensus or \ len(sourceCat) < minObjectsForConsensus: numConsensus = 1 self.log.debug("Current tol maxDist: %.4f arcsec" % maxMatchDistArcSec) self.log.debug("Current shift: %.4f arcsec" % maxShiftArcseconds) match_found = False # Start the iteration over our tolerances. for soften_dist in range(self.config.matcherIterations): if soften_dist == 0 and \ match_tolerance.lastMatchedPattern is not None: # If we are on the first, most stringent tolerance, # and have already found a match, the matcher should behave # like an optimistic pattern matcher. Exiting at the first # match. run_n_consent = 1 else: # If we fail or this is the first match attempt, set the # pattern consensus to the specified config value. run_n_consent = numConsensus # We double the match dist tolerance each round and add 1 to the # to the number of candidate spokes to check. matcher_struct = match_tolerance.PPMbObj.match( source_array=src_array, n_check=self.config.numPointsForShapeAttempt, n_match=self.config.numPointsForShape, n_agree=run_n_consent, max_n_patterns=self.config.numBrightStars, max_shift=maxShiftArcseconds, max_rotation=self.config.maxRotationDeg, max_dist=maxMatchDistArcSec * 2. ** soften_dist, min_matches=minMatchedPairs, pattern_skip_array=np.array( match_tolerance.failedPatternList) ) if soften_dist == 0 and \ len(matcher_struct.match_ids) == 0 and \ match_tolerance.lastMatchedPattern is not None: # If we found a pattern on a previous match-fit iteration and # can't find an optimistic match on our first try with the # tolerances as found in the previous match-fit, # the match we found in the last iteration was likely bad. We # append the bad match's index to the a list of # patterns/matches to skip on subsequent iterations. match_tolerance.failedPatternList.append( match_tolerance.lastMatchedPattern) match_tolerance.lastMatchedPattern = None maxShiftArcseconds = \ self.config.maxOffsetPix * wcs.getPixelScale().asArcseconds() elif len(matcher_struct.match_ids) > 0: # Match found, save a bit a state regarding this pattern # in the match tolerance class object and exit. match_tolerance.maxShift = \ matcher_struct.shift * geom.arcseconds match_tolerance.lastMatchedPattern = \ matcher_struct.pattern_idx match_found = True break # If we didn't find a match, exit early. if not match_found: return pipeBase.Struct( matches=[], match_tolerance=match_tolerance, ) # The matcher returns all the nearest neighbors that agree between # the reference and source catalog. For the current astrometric solver # we need to remove as many false positives as possible before sending # the matches off to the solver. The low value of 100 and high value of # 2 are the low number of sigma and high respectively. The exact values # were found after testing on data of various reference/source # densities and astrometric distortion quality, specifically the # visits: HSC (3358), DECam (406285, 410827), # CFHT (793169, 896070, 980526). distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600 dist_cut_arcsec = np.max( (np.degrees(matcher_struct.max_dist_rad) * 3600, self.config.minMatchDistPixels * wcs.getPixelScale().asArcseconds())) # A match has been found, return our list of matches and # return. matches = [] for match_id_pair, dist_arcsec in zip(matcher_struct.match_ids, distances_arcsec): if dist_arcsec < dist_cut_arcsec: match = afwTable.ReferenceMatch() match.first = refCat[int(match_id_pair[1])] match.second = sourceCat[int(match_id_pair[0])] # We compute the true distance along and sphere. This isn't # used in the WCS fitter however it is used in the unittest # to confirm the matches computed. match.distance = match.first.getCoord().separation( match.second.getCoord()).asArcseconds() matches.append(match) return pipeBase.Struct( matches=matches, match_tolerance=match_tolerance, )
def approximateWcs(wcs, camera_wrapper=None, detector_name=None, obs_metadata=None, order=3, nx=20, ny=20, iterations=3, skyTolerance=0.001 * afwGeom.arcseconds, pixelTolerance=0.02): """Approximate an existing WCS as a TAN-SIP WCS The fit is performed by evaluating the WCS at a uniform grid of points within a bounding box. @param[in] wcs wcs to approximate @param[in] camera_wrapper is an instantiation of GalSimCameraWrapper @param[in] detector_name is the name of the detector @param[in] obs_metadata is an ObservationMetaData characterizing the telescope pointing @param[in] order order of SIP fit @param[in] nx number of grid points along x @param[in] ny number of grid points along y @param[in] iterations number of times to iterate over fitting @param[in] skyTolerance maximum allowed difference in world coordinates between input wcs and approximate wcs (default is 0.001 arcsec) @param[in] pixelTolerance maximum allowed difference in pixel coordinates between input wcs and approximate wcs (default is 0.02 pixels) @return the fit TAN-SIP WCS """ tanWcs = wcs # create a matchList consisting of a grid of points covering the bbox refSchema = afwTable.SimpleTable.makeMinimalSchema() refCoordKey = afwTable.CoordKey(refSchema["coord"]) refCat = afwTable.SimpleCatalog(refSchema) sourceSchema = afwTable.SourceTable.makeMinimalSchema() SingleFrameMeasurementTask(schema=sourceSchema) # expand the schema sourceCentroidKey = afwTable.Point2DKey(sourceSchema["slot_Centroid"]) sourceCat = afwTable.SourceCatalog(sourceSchema) # 20 March 2017 # the 'try' block is how it works in swig; # the 'except' block is how it works in pybind11 try: matchList = afwTable.ReferenceMatchVector() except AttributeError: matchList = [] bbox = camera_wrapper.getBBox(detector_name) bboxd = afwGeom.Box2D(bbox) for x in np.linspace(bboxd.getMinX(), bboxd.getMaxX(), nx): for y in np.linspace(bboxd.getMinY(), bboxd.getMaxY(), ny): pixelPos = afwGeom.Point2D(x, y) ra, dec = camera_wrapper.raDecFromPixelCoords( np.array([x]), np.array([y]), detector_name, obs_metadata=obs_metadata, epoch=2000.0, includeDistortion=True) skyCoord = afwGeom.SpherePoint(ra[0], dec[0], LsstGeom.degrees) refObj = refCat.addNew() refObj.set(refCoordKey, skyCoord) source = sourceCat.addNew() source.set(sourceCentroidKey, pixelPos) matchList.append(afwTable.ReferenceMatch(refObj, source, 0.0)) # The TAN-SIP fitter is fitting x and y separately, so we have to iterate to make it converge for indx in range(iterations): sipObject = makeCreateWcsWithSip(matchList, tanWcs, order, bbox) tanWcs = sipObject.getNewWcs() fitWcs = sipObject.getNewWcs() return fitWcs
def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, match_tolerance, sourceFluxField, verbose): """!Implementation of matching sources to position reference stars Unlike matchObjectsToSources, this method does not check if the sources are suitable. @param[in] refCat catalog of position reference stars that overlap an exposure @param[in] sourceCat catalog of sources found on the exposure @param[in] wcs estimated WCS of exposure @param[in] refFluxField field of refCat to use for flux @param[in] numUsableSources number of usable sources (sources with known centroid that are not near the edge, but may be saturated) @param[in] minMatchedPairs minimum number of matches @param[in] match_tolerance a MatchTolerance object containing variables specifying matcher tolerances and state from possible previous runs. @param[in] sourceInfo SourceInfo for the sourceCat @param[in] verbose true to print diagnostic information to std::cout @return a list of matches, an instance of lsst.afw.table.ReferenceMatch, a MatchTolerance object """ # Load the source and reference catalog as spherical points # in numpy array. We do this rather than relying on internal # lsst C objects for simplicity and because we require # objects contiguous in memory. We need to do these slightly # differently for the reference and source cats as they are # different catalog objects with different fields. ref_array = np.empty((len(refCat), 4), dtype=np.float64) for ref_idx, refObj in enumerate(refCat): theta = np.pi / 2 - refObj.getDec().asRadians() phi = refObj.getRa().asRadians() flux = refObj[refFluxField] ref_array[ref_idx, :] = \ self._latlong_flux_to_xyz_mag(theta, phi, flux) src_array = np.empty((len(sourceCat), 4), dtype=np.float64) for src_idx, srcObj in enumerate(sourceCat): coord = wcs.pixelToSky(srcObj.getCentroid()) theta = np.pi / 2 - coord.getLatitude().asRadians() phi = coord.getLongitude().asRadians() flux = srcObj.getPsfFlux() src_array[src_idx, :] = \ self._latlong_flux_to_xyz_mag(theta, phi, flux) # Set configurable defaults when we encounter None type or set # state based on previous run of AstrometryTask._matchAndFitWcs. if match_tolerance.maxShift is None: maxShiftArcseconds = (self.config.maxOffsetPix * wcs.pixelScale().asArcseconds()) else: # We don't want to clamp down too hard on the allowed shift so # we test that the smallest we ever allow is the pixel scale. maxShiftArcseconds = np.max( (match_tolerance.maxShift.asArcseconds(), wcs.pixelScale().asArcseconds())) # If our tolerances are not set from a previous run, estiamte a # starting tolerance guess from statistics of patterns we can create # on both the source and reference catalog. We use the smaller of the # two. if match_tolerance.maxMatchDist is None: self.log.debug("Computing source statistics...") maxMatchDistArcSecSrc = self._get_pair_pattern_statistics( src_array) self.log.debug("Computing reference statistics...") maxMatchDistArcSecRef = self._get_pair_pattern_statistics( ref_array) maxMatchDistArcSec = np.min( (maxMatchDistArcSecSrc, maxMatchDistArcSecRef)) match_tolerance.autoMaxDist = afwgeom.Angle( maxMatchDistArcSec, afwgeom.arcseconds) else: maxMatchDistArcSec = np.max( (wcs.pixelScale().asArcseconds(), np.min((match_tolerance.maxMatchDist.asArcseconds(), match_tolerance.autoMaxDist.asArcseconds())))) # Make sure the data we are considering is dense enough to require # the consensus mode of the matcher. If not default to Optimistic # pattern matcher behavior. numConsensus = self.config.numPatternConsensus minObjectsForConsensus = \ self.config.numBrightStars + self.config.numPointsForShapeAttempt if ref_array.shape[0] < minObjectsForConsensus or \ src_array.shape[0] < minObjectsForConsensus: numConsensus = 1 self.log.debug("Current tol maxDist: %.4f arcsec" % maxMatchDistArcSec) self.log.debug("Current shift: %.4f arcsec" % maxShiftArcseconds) # Create our matcher object. pyPPMb = PessimisticPatternMatcherB(ref_array[:, :3], self.log) # Start the ineration over our tolerances. for try_idx in range(self.config.matcherIterations): if try_idx == 0 and \ match_tolerance.lastMatchedPattern is not None: # If we are on the first, most stringent tolerance, # and have already found a match, the matcher should behave # like an optimistic pattern matcher. Exiting at the first # match. matcher_struct = pyPPMb.match( source_array=src_array, n_check=self.config.numPointsForShapeAttempt, n_match=self.config.numPointsForShape, n_agree=1, max_n_patterns=self.config.numBrightStars, max_shift=maxShiftArcseconds, max_rotation=self.config.maxRotationDeg, max_dist=maxMatchDistArcSec, min_matches=minMatchedPairs, pattern_skip_array=np.array( match_tolerance.failedPatternList)) else: # Once we fail and start softening tolerences we switch to # pessimistic or consenus mode where 3 patterns must agree # on a rotation before exiting. We double the match dist # tolerance each round and add 1 to the pattern complexiy and # two to the number of candidate spokes to check. matcher_struct = pyPPMb.match( source_array=src_array, n_check=self.config.numPointsForShapeAttempt + 2 * try_idx, n_match=self.config.numPointsForShape + try_idx, n_agree=numConsensus, max_n_patterns=self.config.numBrightStars, max_shift=(self.config.maxOffsetPix * wcs.pixelScale().asArcseconds()), max_rotation=self.config.maxRotationDeg, max_dist=maxMatchDistArcSec * 2.**try_idx, min_matches=minMatchedPairs, pattern_skip_array=np.array( match_tolerance.failedPatternList)) if try_idx == 0 and \ len(matcher_struct.match_ids) == 0 and \ match_tolerance.lastMatchedPattern is not None: # If we found a pattern on a previous match-fit iteration and # can't find an optimistic match on our first try with the # tolerances as found in the previous match-fit, # the match we found in the last iteration was likely bad. We # append the bad match's index to the a list of # patterns/matches to skip on subsequent iterations. match_tolerance.failedPatternList.append( match_tolerance.lastMatchedPattern) match_tolerance.lastMatchedPattern = None elif len(matcher_struct.match_ids) > 0: # Match found, save a bit a state regarding this pattern # in the struct and exit. match_tolerance.maxShift = afwgeom.Angle( matcher_struct.shift, afwgeom.arcseconds) match_tolerance.lastMatchedPattern = \ matcher_struct.pattern_idx break # The matcher returns all the nearest neighbors that agree between # the reference and source catalog. For the currrent astrometry solver # we need to remove as many false positives as possible before sending # the matches off to the astronomery solver. distances_arcsec = np.degrees(matcher_struct.distances_rad) * 3600 # Rather than have a hard cut on the max distance we allow for # objects we preform a iterative sigma clipping. We input a # starting point for the clipping based on the largest of the # distances we have. The hope is this distance will not be so # large as to have the sigma_clip return a large unphysical value # and not so small that we don't allow for some distortion in the # WCS between source and reference. clipped_dist_arcsec = self._sigma_clip( values=distances_arcsec, n_sigma=2, input_cut=np.max((10 * wcs.pixelScale().asArcseconds(), maxMatchDistArcSec, maxShiftArcseconds))) # We pick the largest of the sigma clipping, the unsofted # maxMatchDistArcSec or 2 pixels. This pevents # the AstrometryTask._matchAndFitWCS from overfitting to a small # number of objects and also allows the WCS fitter to bring in more # matches as the WCS fit improves. dist_cut_arcsec = np.max((2 * wcs.pixelScale().asArcseconds(), clipped_dist_arcsec, maxMatchDistArcSec)) # A match has been found, return our list of matches and # return. matches = [] for match_id_pair, dist_arcsec in zip(matcher_struct.match_ids, distances_arcsec): if dist_arcsec < dist_cut_arcsec: match = afwTable.ReferenceMatch() match.first = refCat[match_id_pair[1]] match.second = sourceCat[match_id_pair[0]] # We compute the true distance along and sphere instead # and store it in units of arcseconds. The previous # distances we used were aproximate. match.distance = match.first.getCoord().angularSeparation( match.second.getCoord()).asArcseconds() matches.append(match) return pipeBase.Struct( matches=matches, match_tolerance=match_tolerance, )