def doTest(self, name, func, order=3, numIter=4, specifyBBox=False, doPlot=False): """Apply func(x, y) to each source in self.sourceCat, then fit and check the resulting WCS """ bbox = afwGeom.Box2I() for refObj, src, d in self.matches: origPos = src.get(self.srcCentroidKey) x, y = func(*origPos) distortedPos = afwGeom.Point2D(*func(*origPos)) src.set(self.srcCentroidKey, distortedPos) bbox.include(afwGeom.Point2I(afwGeom.Point2I(distortedPos))) tanSipWcs = self.tanWcs for i in range(numIter): if specifyBBox: sipObject = makeCreateWcsWithSip(self.matches, tanSipWcs, order, bbox) else: sipObject = makeCreateWcsWithSip(self.matches, tanSipWcs, order) tanSipWcs = sipObject.getNewWcs() setMatchDistance(self.matches) fitRes = lsst.pipe.base.Struct( wcs = tanSipWcs, scatterOnSky = sipObject.getScatterOnSky(), ) if doPlot: self.plotWcs(tanSipWcs, name=name) self.checkResults(fitRes, catsUpdated=False) if self.MatchClass == afwTable.ReferenceMatch: # reset source coord and reference centroid based on initial WCS FitTanSipWcsTask.updateRefCentroids(wcs=self.tanWcs, refList = self.refCat) FitTanSipWcsTask.updateSourceCoords(wcs=self.tanWcs, sourceList = self.sourceCat) fitterConfig = FitTanSipWcsTask.ConfigClass() fitterConfig.order = order fitterConfig.numIter = numIter fitter = FitTanSipWcsTask(config=fitterConfig) self.loadData() if specifyBBox: fitRes = fitter.fitWcs( matches = self.matches, initWcs = self.tanWcs, bbox = bbox, refCat = self.refCat, sourceCat = self.sourceCat, ) else: fitRes = fitter.fitWcs( matches = self.matches, initWcs = self.tanWcs, bbox = bbox, refCat = self.refCat, sourceCat = self.sourceCat, ) self.checkResults(fitRes, catsUpdated=True)
def refitWcs(self, exposure, sources, matches): """We have better matches after solving astrometry, so re-solve the WCS @param exposure Exposure of interest @param sources Sources on image (no distortion applied) @param matches Astrometric matches @return the resolved-Wcs object, or None if config.solver.calculateSip is False. """ wcs = exposure.getWcs() sip = None self.log.info("Refitting WCS") # Re-fit the WCS using the current matches if self.config.solver.calculateSip: try: sip = makeCreateWcsWithSip(matches, exposure.getWcs(), self.config.solver.sipOrder) except Exception, e: self.log.warn("Fitting SIP failed: %s" % e) sip = None if sip: wcs = sip.getNewWcs() self.log.info("Astrometric scatter: %f arcsec (%s non-linear terms)" % (sip.getScatterOnSky().asArcseconds(), "with" if wcs.hasDistortion() else "without")) exposure.setWcs(wcs) # Apply WCS to sources for index, source in enumerate(sources): sky = wcs.pixelToSky(source.getX(), source.getY()) source.setCoord(sky)
def _calculateSipTerms(self, origWcs, refCat, sourceCat, matches, bbox): """!Iteratively calculate SIP distortions and regenerate matches based on improved WCS. @param[in] origWcs original WCS object, probably (but not necessarily) a TAN WCS; this is used to set the baseline when determining whether a SIP solution is any better; it will be returned if no better SIP solution can be found. @param[in] refCat reference source catalog @param[in] sourceCat sources in the image to be solved @param[in] matches list of supposedly matched sources, using the "origWcs". @param[in] bbox bounding box of image, which is used when finding reverse SIP coefficients. """ sipOrder = self.config.sipOrder wcs = origWcs lastMatchSize = len(matches) lastMatchStats = self._computeMatchStatsOnSky(wcs=wcs, matchList=matches) for i in range(self.config.maxIter): # fit SIP terms try: sipObject = astromSip.makeCreateWcsWithSip(matches, wcs, sipOrder, bbox) proposedWcs = sipObject.getNewWcs() self.plotSolution(matches, proposedWcs, bbox.getDimensions()) except pexExceptions.Exception as e: self.log.warn('Failed to calculate distortion terms. Error: ', str(e)) break # update the source catalog for source in sourceCat: skyPos = proposedWcs.pixelToSky(source.getCentroid()) source.setCoord(skyPos) # use new WCS to get new matchlist. proposedMatchlist = self._getMatchList(sourceCat, refCat, proposedWcs) proposedMatchSize = len(proposedMatchlist) proposedMatchStats = self._computeMatchStatsOnSky(wcs=proposedWcs, matchList=proposedMatchlist) self.log.debug( "SIP iteration %i: %i objects match, previous = %i;" % (i, proposedMatchSize, lastMatchSize) + " clipped mean scatter = %s arcsec, previous = %s; " % (proposedMatchStats.distMean.asArcseconds(), lastMatchStats.distMean.asArcseconds()) + " max match dist = %s arcsec, previous = %s" % (proposedMatchStats.maxMatchDist.asArcseconds(), lastMatchStats.maxMatchDist.asArcseconds()) ) if lastMatchStats.maxMatchDist <= proposedMatchStats.maxMatchDist: self.log.debug( "Fit WCS: use iter %s because max match distance no better in next iter: " % (i-1,) + " %g < %g arcsec" % (lastMatchStats.maxMatchDist.asArcseconds(), proposedMatchStats.maxMatchDist.asArcseconds())) break wcs = proposedWcs matches = proposedMatchlist lastMatchSize = proposedMatchSize lastMatchStats = proposedMatchStats return wcs, matches
def singleTestInstance(self, filename, distortFunc): cat = self.loadCatalogue(self.filename) img = distort.distortList(cat, distortFunc) bbox = afwGeom.Box2I(afwGeom.Point2I(0, 0), afwGeom.Extent2I(1000, 1000)) res = self.astrom.determineWcs2(img, bbox=bbox) imgWcs = res.getWcs() # Create a wcs with sip cat = cat.cast(SimpleCatalog, False) matchList = self.matchSrcAndCatalogue(cat, img, imgWcs) print("*** num matches =", len(matchList)) return sipObject = sip.makeCreateWcsWithSip(matchList, imgWcs, 3) imgWcs = sipObject.getNewWcs() print('number of matches:', len(matchList), sipObject.getNPoints()) scatter = sipObject.getScatterOnSky().asArcseconds() print("Scatter in arcsec is %g" % (scatter)) self.assertLess(scatter, self.tolArcsec, "Scatter exceeds tolerance in arcsec") if False: scatter = sipObject.getScatterInPixels() self.assertLess( scatter, self.tolPixel, "Scatter exceeds tolerance in pixels: %g" % (scatter, ))
def fitWcs(self, matches, inputWcs, inputBBox): """Fit Wcs to matches The fitting includes iterative sigma-clipping. @param matches: List of matches (first is target, second is input) @param inputWcs: Original input Wcs @param inputBBox: Bounding box of input image @return Wcs """ copyMatches = type(matches)(matches) refCoordKey = copyMatches[0].first.getTable().getCoordKey() inCentroidKey = copyMatches[0].second.getTable().getCentroidSlot( ).getMeasKey() for i in range(self.config.sipIter): sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox) self.log.debug("Registration WCS RMS iteration %d: %f pixels", i, sipFit.getScatterInPixels()) wcs = sipFit.getNewWcs() dr = [ m.first.get(refCoordKey).separation( wcs.pixelToSky( m.second.get(inCentroidKey))).asArcseconds() for m in copyMatches ] dr = numpy.array(dr) rms = math.sqrt((dr * dr).mean()) # RMS from zero rms = max(rms, 1.0e-9) # Don't believe any RMS smaller than this self.log.debug("Registration iteration %d: rms=%f", i, rms) good = numpy.where(dr < self.config.sipRej * rms)[0] numBad = len(copyMatches) - len(good) self.log.debug("Registration iteration %d: rejected %d", i, numBad) if numBad == 0: break copyMatches = type(matches)(copyMatches[i] for i in good) sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox) self.log.info( "Registration WCS: final WCS RMS=%f pixels from %d matches", sipFit.getScatterInPixels(), len(copyMatches)) self.metadata["SIP_RMS"] = sipFit.getScatterInPixels() self.metadata["SIP_GOOD"] = len(copyMatches) self.metadata["SIP_REJECTED"] = len(matches) - len(copyMatches) wcs = sipFit.getNewWcs() return wcs
def fitWcs(initialWcs, title=None): """Do the WCS fitting and display of the results""" sip = makeCreateWcsWithSip(matches, initialWcs, self.config.solver.sipOrder) resultWcs = sip.getNewWcs() if display: showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame, title=title, pause=pause) return resultWcs, sip.getScatterOnSky()
def fitWcs(initialWcs, title=None): """!Do the WCS fitting and display of the results""" sip = makeCreateWcsWithSip(matches, initialWcs, self.config.solver.sipOrder) resultWcs = sip.getNewWcs() if display: showAstrometry(exposure, resultWcs, origMatches, matches, frame=frame, title=title, pause=pause) return resultWcs, sip.getScatterOnSky()
def fitWcs(self, matches, inputWcs, inputBBox): """Fit Wcs to matches The fitting includes iterative sigma-clipping. @param matches: List of matches (first is target, second is input) @param inputWcs: Original input Wcs @param inputBBox: Bounding box of input image @return Wcs """ copyMatches = type(matches)(matches) refCoordKey = copyMatches[0].first.getTable().getCoordKey() inCentroidKey = copyMatches[0].second.getTable().getCentroidKey() for i in range(self.config.sipIter): sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox) self.log.debug("Registration WCS RMS iteration %d: %f pixels", i, sipFit.getScatterInPixels()) wcs = sipFit.getNewWcs() dr = [m.first.get(refCoordKey).separation( wcs.pixelToSky(m.second.get(inCentroidKey))).asArcseconds() for m in copyMatches] dr = numpy.array(dr) rms = math.sqrt((dr*dr).mean()) # RMS from zero rms = max(rms, 1.0e-9) # Don't believe any RMS smaller than this self.log.debug("Registration iteration %d: rms=%f", i, rms) good = numpy.where(dr < self.config.sipRej*rms)[0] numBad = len(copyMatches) - len(good) self.log.debug("Registration iteration %d: rejected %d", i, numBad) if numBad == 0: break copyMatches = type(matches)(copyMatches[i] for i in good) sipFit = makeCreateWcsWithSip(copyMatches, inputWcs, self.config.sipOrder, inputBBox) self.log.info("Registration WCS: final WCS RMS=%f pixels from %d matches" % (sipFit.getScatterInPixels(), len(copyMatches))) self.metadata.set("SIP_RMS", sipFit.getScatterInPixels()) self.metadata.set("SIP_GOOD", len(copyMatches)) self.metadata.set("SIP_REJECTED", len(matches) - len(copyMatches)) wcs = sipFit.getNewWcs() return wcs
def singleTestInstance(self, filename, distortFunc): cat = self.loadCatalogue(self.filename) img = distort.distortList(cat, distortFunc) bbox = afwGeom.Box2I(afwGeom.Point2I(0, 0), afwGeom.Extent2I(1000, 1000)) res = self.astrom.determineWcs2(img, bbox=bbox) imgWcs = res.getWcs() def printWcs(wcs): print("WCS metadata:") md = wcs.getFitsMetadata() for name in md.names(): print("%s: %r" % (name, md.get(name))) printWcs(imgWcs) # Create a wcs with sip cat = cat.cast(SimpleCatalog, False) matchList = self.matchSrcAndCatalogue(cat, img, imgWcs) print("*** num matches =", len(matchList)) return sipObject = sip.makeCreateWcsWithSip(matchList, imgWcs, 3) # print 'Put in TAN Wcs:' # print imgWcs.getFitsMetadata().toString() imgWcs = sipObject.getNewWcs() # print 'Got SIP Wcs:' # print imgWcs.getFitsMetadata().toString() # Write out the SIP header # afwImage.fits_write_imageF('createWcsWithSip.sip', afwImage.ImageF(0,0), # imgWcs.getFitsMetadata()) print('number of matches:', len(matchList), sipObject.getNPoints()) scatter = sipObject.getScatterOnSky().asArcseconds() print("Scatter in arcsec is %g" % (scatter)) self.assertLess(scatter, self.tolArcsec, "Scatter exceeds tolerance in arcsec") if False: scatter = sipObject.getScatterInPixels() self.assertLess( scatter, self.tolPixel, "Scatter exceeds tolerance in pixels: %g" % (scatter, ))
def singleTestInstance(self, filename, distortFunc): cat = self.loadCatalogue(self.filename) img = distort.distortList(cat, distortFunc) bbox = afwGeom.Box2I(afwGeom.Point2I(0, 0), afwGeom.Extent2I(1000, 1000)) res = self.astrom.determineWcs2(img, bbox=bbox) imgWcs = res.getWcs() def printWcs(wcs): print "WCS metadata:" md = wcs.getFitsMetadata() for name in md.names(): print "%s: %r" % (name, md.get(name)) printWcs(imgWcs) #Create a wcs with sip cat = cat.cast(afwTable.SimpleCatalog, False) matchList = self.matchSrcAndCatalogue(cat, img, imgWcs) print "*** num matches =", len(matchList) return sipObject = sip.makeCreateWcsWithSip(matchList, imgWcs, 3) #print 'Put in TAN Wcs:' #print imgWcs.getFitsMetadata().toString() imgWcs = sipObject.getNewWcs() #print 'Got SIP Wcs:' #print imgWcs.getFitsMetadata().toString() # Write out the SIP header #afwImage.fits_write_imageF('createWcsWithSip.sip', afwImage.ImageF(0,0), #imgWcs.getFitsMetadata()) print 'number of matches:', len(matchList), sipObject.getNPoints() scatter = sipObject.getScatterOnSky().asArcseconds() print "Scatter in arcsec is %g" % (scatter) self.assertLess(scatter, self.tolArcsec, "Scatter exceeds tolerance in arcsec") if False: scatter = sipObject.getScatterInPixels() self.assertLess(scatter, self.tolPixel, "Scatter exceeds tolerance in pixels: %g" %(scatter))
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 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 = afwTable.ReferenceMatchVector() bboxd = afwGeom.Box2D(bbox) for x in numpy.linspace(bboxd.getMinX(), bboxd.getMaxX(), nx): for y in numpy.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() assertWcsNearlyEqualOverBBox(mockTest, wcs, fitWcs, bbox, maxDiffSky=skyTolerance, maxDiffPix=pixelTolerance) return fitWcs
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 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 = afwGeom.SpherePoint(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 = afwGeom.SpherePoint(LLl[0], LLl[1], afwGeom.radians) ULc = afwGeom.SpherePoint(ULl[0], ULl[1], afwGeom.radians) LRc = afwGeom.SpherePoint(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() cdMatrix = np.array([cd1_1, cd2_1, cd1_2, cd2_2], dtype=float) cdMatrix.shape = (2, 2) linearWcs = makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix) wcs = sip.makeCreateWcsWithSip(matches, linearWcs, order).getNewWcs() return wcs
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) matchList = afwTable.ReferenceMatchVector() 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 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 approximateWcs(wcs, bbox, order=3, nx=20, ny=20, iterations=3, skyTolerance=0.001*lsst.geom.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. Parameters ---------- wcs : `lsst.afw.geom.SkyWcs` wcs to approximate bbox : `lsst.geom.Box2I` the region over which the WCS will be fit order : `int` order of SIP fit nx : `int` number of grid points along x ny : `int` number of grid points along y iterations : `int` number of times to iterate over fitting skyTolerance : `lsst.geom.Angle` maximum allowed difference in world coordinates between input wcs and approximate wcs (default is 0.001 arcsec) pixelTolerance : `float` maximum allowed difference in pixel coordinates between input wcs and approximate wcs (default is 0.02 pixels) useTanWcs : `bool` send a TAN version of wcs to the fitter? It is documented to require that, but I don't think the fitter actually cares Returns ------- fitWcs : `lsst.afw.geom.SkyWcs` the fit TAN-SIP WCS """ if useTanWcs: crpix = wcs.getPixelOrigin() crval = wcs.getSkyOrigin() cdMatrix = wcs.getCdMatrix(crpix) tanWcs = afwGeom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix) 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 = lsst.geom.Box2D(bbox) for x in np.linspace(bboxd.getMinX(), bboxd.getMaxX(), nx): for y in np.linspace(bboxd.getMinY(), bboxd.getMaxY(), ny): pixelPos = lsst.geom.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, 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 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 doTest(self, name, func, order=3, numIter=4, specifyBBox=False, doPlot=False, doPrint=False): """Apply func(x, y) to each source in self.sourceCat, then fit and check the resulting WCS """ bbox = lsst.geom.Box2I() for refObj, src, d in self.matches: origPos = src.get(self.srcCentroidKey) x, y = func(*origPos) distortedPos = lsst.geom.Point2D(*func(*origPos)) src.set(self.srcCentroidKey, distortedPos) bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(distortedPos))) tanSipWcs = self.tanWcs for i in range(numIter): if specifyBBox: sipObject = makeCreateWcsWithSip(self.matches, tanSipWcs, order, bbox) else: sipObject = makeCreateWcsWithSip(self.matches, tanSipWcs, order) tanSipWcs = sipObject.getNewWcs() setMatchDistance(self.matches) fitRes = lsst.pipe.base.Struct( wcs=tanSipWcs, scatterOnSky=sipObject.getScatterOnSky(), ) if doPrint: print("TAN-SIP metadata fit over bbox=", bbox) metadata = makeTanSipMetadata( crpix=tanSipWcs.getPixelOrigin(), crval=tanSipWcs.getSkyOrigin(), cdMatrix=tanSipWcs.getCdMatrix(), sipA=sipObject.getSipA(), sipB=sipObject.getSipB(), sipAp=sipObject.getSipAp(), sipBp=sipObject.getSipBp(), ) print(metadata.toString()) if doPlot: self.plotWcs(tanSipWcs, name=name) self.checkResults(fitRes, catsUpdated=False) if self.MatchClass == afwTable.ReferenceMatch: # reset source coord and reference centroid based on initial WCS afwTable.updateRefCentroids(wcs=self.tanWcs, refList=self.refCat) afwTable.updateSourceCoords(wcs=self.tanWcs, sourceList=self.sourceCat) fitterConfig = FitTanSipWcsTask.ConfigClass() fitterConfig.order = order fitterConfig.numIter = numIter fitter = FitTanSipWcsTask(config=fitterConfig) self.loadData() if specifyBBox: fitRes = fitter.fitWcs( matches=self.matches, initWcs=self.tanWcs, bbox=bbox, refCat=self.refCat, sourceCat=self.sourceCat, ) else: fitRes = fitter.fitWcs( matches=self.matches, initWcs=self.tanWcs, bbox=bbox, refCat=self.refCat, sourceCat=self.sourceCat, ) self.checkResults(fitRes, catsUpdated=True)