Esempio n. 1
0
    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()
Esempio n. 4
0
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
Esempio n. 5
0
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
Esempio n. 9
0
    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,
        )