def makePixelToTanPixel(bbox, orientation, focalPlaneToField, pixelSizeMm):
    """!Make a Transform whose forward direction converts PIXELS to TAN_PIXELS for one detector

    PIXELS and TAN_PIXELS are defined in @ref afwCameraGeomCoordSys in doc/cameraGeom.dox

    @param[in] bbox  detector bounding box (an lsst.afw.geom.Box2I)
    @param[in] orientation  orientation of detector in focal plane (an lsst.afw.cameraGeom.Orientation)
    @param[in] focalPlaneToField  an lsst.afw.geom.Transform that converts from focal plane (mm)
        to field angle coordinates (radians) in the forward direction
    @param[in] pixelSizeMm  size of the pixel in mm in X and Y (an lsst.afw.geom.Extent2D)
    @return a TransformPoint2ToPoint2 whose forward direction converts PIXELS to TAN_PIXELS
    """
    pixelToFocalPlane = orientation.makePixelFpTransform(pixelSizeMm)
    pixelToField = pixelToFocalPlane.then(focalPlaneToField)
    # fieldToTanPix is affine and matches fieldToPix at field center
    # Note: focal plane to field angle is typically a radial transform,
    # and linearizing the inverse transform of that may fail,
    # so linearize the forward direction instead. (pixelToField is pixelToFocalPlane,
    # an affine transform, followed by focalPlaneToField,
    # so the same consideration applies to pixelToField)
    pixAtFieldCtr = pixelToField.applyInverse(afwGeom.Point2D(0, 0))
    tanPixToFieldAffine = afwGeom.linearizeTransform(pixelToField,
                                                     pixAtFieldCtr)
    fieldToTanPix = afwGeom.makeTransform(tanPixToFieldAffine.invert())

    return pixelToField.then(fieldToTanPix)
Exemple #2
0
def plantSources(x0, y0, nx, ny, sky, nObj, wid, detector, useRandom=False):

    pixToTanPix = detector.getTransform(cameraGeom.PIXELS,
                                        cameraGeom.TAN_PIXELS)

    img0 = afwImage.ImageF(afwGeom.ExtentI(nx, ny))
    img = afwImage.ImageF(afwGeom.ExtentI(nx, ny))

    ixx0, iyy0, ixy0 = wid * wid, wid * wid, 0.0

    edgeBuffer = 40.0 * wid

    flux = 1.0e4
    nkx, nky = int(10 * wid) + 1, int(10 * wid) + 1
    xhwid, yhwid = nkx // 2, nky // 2

    nRow = int(math.sqrt(nObj))
    xstep = (nx - 1 - 0.0 * edgeBuffer) // (nRow + 1)
    ystep = (ny - 1 - 0.0 * edgeBuffer) // (nRow + 1)

    if useRandom:
        nObj = nRow * nRow

    goodAdded0 = []
    goodAdded = []

    for i in range(nObj):

        # get our position
        if useRandom:
            xcen0, ycen0 = np.random.uniform(nx), np.random.uniform(ny)
        else:
            xcen0, ycen0 = xstep * (
                (i % nRow) + 1), ystep * (int(i / nRow) + 1)
        ixcen0, iycen0 = int(xcen0), int(ycen0)

        # distort position and shape
        pTan = afwGeom.Point2D(xcen0, ycen0)
        p = pixToTanPix.applyInverse(pTan)
        linTransform = afwGeom.linearizeTransform(pixToTanPix,
                                                  p).invert().getLinear()
        m = afwGeom.Quadrupole(ixx0, iyy0, ixy0)
        m.transform(linTransform)

        xcen, ycen = xcen0, ycen0  # p.getX(), p.getY()
        if (xcen < 1.0 * edgeBuffer or (nx - xcen) < 1.0 * edgeBuffer
                or ycen < 1.0 * edgeBuffer or (ny - ycen) < 1.0 * edgeBuffer):
            continue
        ixcen, iycen = int(xcen), int(ycen)
        ixx, iyy, ixy = m.getIxx(), m.getIyy(), m.getIxy()

        # plant the object
        tmp = 0.25 * (ixx - iyy)**2 + ixy**2
        a2 = 0.5 * (ixx + iyy) + np.sqrt(tmp)
        b2 = 0.5 * (ixx + iyy) - np.sqrt(tmp)

        theta = 0.5 * np.arctan2(2.0 * ixy, ixx - iyy)
        a = np.sqrt(a2)
        b = np.sqrt(b2)

        c, s = math.cos(theta), math.sin(theta)
        good0, good = True, True
        for y in range(nky):
            iy = iycen + y - yhwid
            iy0 = iycen0 + y - yhwid

            for x in range(nkx):
                ix = ixcen + x - xhwid
                ix0 = ixcen0 + x - xhwid

                if ix >= 0 and ix < nx and iy >= 0 and iy < ny:
                    dx, dy = ix - xcen, iy - ycen
                    u = c * dx + s * dy
                    v = -s * dx + c * dy
                    I0 = flux / (2 * math.pi * a * b)
                    val = I0 * math.exp(-0.5 * ((u / a)**2 + (v / b)**2))
                    if val < 0:
                        val = 0
                    prevVal = img.get(ix, iy)
                    img.set(ix, iy, val + prevVal)
                else:
                    good = False

                if ix0 >= 0 and ix0 < nx and iy0 >= 0 and iy0 < ny:
                    dx, dy = ix - xcen, iy - ycen
                    I0 = flux / (2 * math.pi * wid * wid)
                    val = I0 * math.exp(-0.5 * ((dx / wid)**2 + (dy / wid)**2))
                    if val < 0:
                        val = 0
                    prevVal = img0.get(ix0, iy0)
                    img0.set(ix0, iy0, val + prevVal)
                else:
                    good0 = False

        if good0:
            goodAdded0.append([xcen, ycen])
        if good:
            goodAdded.append([xcen, ycen])

    # add sky and noise
    img += sky
    img0 += sky
    noise = afwImage.ImageF(afwGeom.ExtentI(nx, ny))
    noise0 = afwImage.ImageF(afwGeom.ExtentI(nx, ny))
    for i in range(nx):
        for j in range(ny):
            noise.set(i, j, np.random.poisson(img.get(i, j)))
            noise0.set(i, j, np.random.poisson(img0.get(i, j)))

    edgeWidth = int(0.5 * edgeBuffer)
    mask = afwImage.Mask(afwGeom.ExtentI(nx, ny))
    left = afwGeom.Box2I(afwGeom.Point2I(0, 0), afwGeom.ExtentI(edgeWidth, ny))
    right = afwGeom.Box2I(afwGeom.Point2I(nx - edgeWidth, 0),
                          afwGeom.ExtentI(edgeWidth, ny))
    top = afwGeom.Box2I(afwGeom.Point2I(0, ny - edgeWidth),
                        afwGeom.ExtentI(nx, edgeWidth))
    bottom = afwGeom.Box2I(afwGeom.Point2I(0, 0),
                           afwGeom.ExtentI(nx, edgeWidth))

    for pos in [left, right, top, bottom]:
        msk = afwImage.Mask(mask, pos, deep=False)
        msk.set(msk.getPlaneBitMask('EDGE'))

    expos = afwImage.makeExposure(
        afwImage.makeMaskedImage(noise, mask, afwImage.ImageF(noise, True)))
    expos0 = afwImage.makeExposure(
        afwImage.makeMaskedImage(noise0, mask, afwImage.ImageF(noise0, True)))

    im = expos.getMaskedImage().getImage()
    im0 = expos0.getMaskedImage().getImage()
    im -= sky
    im0 -= sky

    return expos, goodAdded, expos0, goodAdded0
    def checkLinearize(self, transform, invertible):
        """Test whether a specific transform is correctly linearized.

        Parameters
        ----------
        transform: `lsst.afw.geom.Transform`
            the transform whose linearization will be tested. Should not be
            strongly curved within ~1 unit of the origin, or the test may rule
            the approximation isn't good enough.
        invertible: `bool`
            whether `transform` is invertible. The test will verify that the
            linearized form is invertible iff `transform` is. If `transform`
            is invertible, the test will also verify that the inverse of the
            linearization approximates the inverse of `transform`.
        """
        fromEndpoint = transform.fromEndpoint
        toEndpoint = transform.toEndpoint
        nIn = fromEndpoint.nAxes
        nOut = toEndpoint.nAxes
        msg = "TransformClass={}, nIn={}, nOut={}".format(
            type(transform).__name__, nIn, nOut)

        rawLinPoint = self.makeRawPointData(nIn)
        linPoint = fromEndpoint.pointFromData(rawLinPoint)
        affine = afwGeom.linearizeTransform(transform, linPoint)
        self.assertIsInstance(affine, afwGeom.AffineTransform)

        # Does affine match exact transform at linPoint?
        outPoint = transform.applyForward(linPoint)
        outPointLinearized = affine(linPoint)
        assert_allclose(toEndpoint.dataFromPoint(outPoint),
                        toEndpoint.dataFromPoint(outPointLinearized),
                        err_msg=msg)
        jacobian = transform.getJacobian(linPoint)
        jacobianLinearized = affine.getLinear().getMatrix()
        assert_allclose(jacobian, jacobianLinearized)

        # Is affine a local approximation around linPoint?
        for deltaFrom in (np.zeros(nIn), np.full(nIn, 0.1),
                          np.array([0.1, -0.15, 0.20, -0.05, 0.0,
                                    -0.1][0:nIn])):
            tweakedInPoint = fromEndpoint.pointFromData(rawLinPoint +
                                                        deltaFrom)
            tweakedOutPoint = transform.applyForward(tweakedInPoint)
            tweakedOutPointLinearized = affine(tweakedInPoint)
            assert_allclose(
                toEndpoint.dataFromPoint(tweakedOutPoint),
                toEndpoint.dataFromPoint(tweakedOutPointLinearized),
                atol=1e-3,
                err_msg=msg)

        # Is affine invertible?
        # AST lets all-zero MatrixMaps be invertible though inverse
        # ill-defined; exclude this case
        if invertible:
            rng = np.random.RandomState(42)
            nDelta = 100
            inverse = affine.invert()
            deltaFrom = rng.normal(0.0, 10.0, (nIn, nDelta))
            for i in range(nDelta):
                pointMsg = "{}, point={}".format(msg, tweakedInPoint)
                tweakedInPoint = fromEndpoint.pointFromData(rawLinPoint +
                                                            deltaFrom[:, i])
                tweakedOutPoint = affine(tweakedInPoint)

                roundTrip = inverse(tweakedOutPoint)
                assert_allclose(roundTrip, tweakedInPoint, err_msg=pointMsg)
                assert_allclose(inverse.getLinear().getMatrix(),
                                np.linalg.inv(jacobian),
                                err_msg=pointMsg)
        else:
            # TODO: replace with correct type after fixing DM-11248
            with self.assertRaises(Exception):
                affine.invert()
def plantSources(x0, y0, nx, ny, sky, nObj, wid, detector, useRandom=False):

    pixToTanPix = detector.getTransform(cameraGeom.PIXELS, cameraGeom.TAN_PIXELS)

    img0 = afwImage.ImageF(lsst.geom.ExtentI(nx, ny))
    img = afwImage.ImageF(lsst.geom.ExtentI(nx, ny))

    ixx0, iyy0, ixy0 = wid*wid, wid*wid, 0.0

    edgeBuffer = 40.0*wid

    flux = 1.0e4
    nkx, nky = int(10*wid) + 1, int(10*wid) + 1
    xhwid, yhwid = nkx//2, nky//2

    nRow = int(math.sqrt(nObj))
    xstep = (nx - 1 - 0.0*edgeBuffer)//(nRow+1)
    ystep = (ny - 1 - 0.0*edgeBuffer)//(nRow+1)

    if useRandom:
        nObj = nRow*nRow

    goodAdded0 = []
    goodAdded = []

    for i in range(nObj):

        # get our position
        if useRandom:
            xcen0, ycen0 = np.random.uniform(nx), np.random.uniform(ny)
        else:
            xcen0, ycen0 = xstep*((i % nRow) + 1), ystep*(int(i/nRow) + 1)
        ixcen0, iycen0 = int(xcen0), int(ycen0)

        # distort position and shape
        pTan = lsst.geom.Point2D(xcen0, ycen0)
        p = pixToTanPix.applyInverse(pTan)
        linTransform = afwGeom.linearizeTransform(pixToTanPix, p).inverted().getLinear()
        m = afwGeom.Quadrupole(ixx0, iyy0, ixy0)
        m.transform(linTransform)

        xcen, ycen = xcen0, ycen0  # p.getX(), p.getY()
        if (xcen < 1.0*edgeBuffer or (nx - xcen) < 1.0*edgeBuffer or
                ycen < 1.0*edgeBuffer or (ny - ycen) < 1.0*edgeBuffer):
            continue
        ixcen, iycen = int(xcen), int(ycen)
        ixx, iyy, ixy = m.getIxx(), m.getIyy(), m.getIxy()

        # plant the object
        tmp = 0.25*(ixx-iyy)**2 + ixy**2
        a2 = 0.5*(ixx+iyy) + np.sqrt(tmp)
        b2 = 0.5*(ixx+iyy) - np.sqrt(tmp)

        theta = 0.5*np.arctan2(2.0*ixy, ixx-iyy)
        a = np.sqrt(a2)
        b = np.sqrt(b2)

        c, s = math.cos(theta), math.sin(theta)
        good0, good = True, True
        for y in range(nky):
            iy = iycen + y - yhwid
            iy0 = iycen0 + y - yhwid

            for x in range(nkx):
                ix = ixcen + x - xhwid
                ix0 = ixcen0 + x - xhwid

                if ix >= 0 and ix < nx and iy >= 0 and iy < ny:
                    dx, dy = ix - xcen, iy - ycen
                    u = c*dx + s*dy
                    v = -s*dx + c*dy
                    I0 = flux/(2*math.pi*a*b)
                    val = I0*math.exp(-0.5*((u/a)**2 + (v/b)**2))
                    if val < 0:
                        val = 0
                    prevVal = img[ix, iy, afwImage.LOCAL]
                    img[ix, iy, afwImage.LOCAL] = val+prevVal
                else:
                    good = False

                if ix0 >= 0 and ix0 < nx and iy0 >= 0 and iy0 < ny:
                    dx, dy = ix - xcen, iy - ycen
                    I0 = flux/(2*math.pi*wid*wid)
                    val = I0*math.exp(-0.5*((dx/wid)**2 + (dy/wid)**2))
                    if val < 0:
                        val = 0
                    prevVal = img0[ix0, iy0, afwImage.LOCAL]
                    img0[ix0, iy0, afwImage.LOCAL] = val+prevVal
                else:
                    good0 = False

        if good0:
            goodAdded0.append([xcen, ycen])
        if good:
            goodAdded.append([xcen, ycen])

    # add sky and noise
    img += sky
    img0 += sky
    noise = afwImage.ImageF(lsst.geom.ExtentI(nx, ny))
    noise0 = afwImage.ImageF(lsst.geom.ExtentI(nx, ny))
    for i in range(nx):
        for j in range(ny):
            noise[i, j, afwImage.LOCAL] = np.random.poisson(img[i, j, afwImage.LOCAL])
            noise0[i, j, afwImage.LOCAL] = np.random.poisson(img0[i, j, afwImage.LOCAL])

    edgeWidth = int(0.5*edgeBuffer)
    mask = afwImage.Mask(lsst.geom.ExtentI(nx, ny))
    left = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.ExtentI(edgeWidth, ny))
    right = lsst.geom.Box2I(lsst.geom.Point2I(nx - edgeWidth, 0), lsst.geom.ExtentI(edgeWidth, ny))
    top = lsst.geom.Box2I(lsst.geom.Point2I(0, ny - edgeWidth), lsst.geom.ExtentI(nx, edgeWidth))
    bottom = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.ExtentI(nx, edgeWidth))

    for pos in [left, right, top, bottom]:
        msk = afwImage.Mask(mask, pos, deep=False)
        msk.set(msk.getPlaneBitMask('EDGE'))

    expos = afwImage.makeExposure(afwImage.makeMaskedImage(noise, mask, afwImage.ImageF(noise, True)))
    expos0 = afwImage.makeExposure(afwImage.makeMaskedImage(noise0, mask, afwImage.ImageF(noise0, True)))

    im = expos.getMaskedImage().getImage()
    im0 = expos0.getMaskedImage().getImage()
    im -= sky
    im0 -= sky

    return expos, goodAdded, expos0, goodAdded0
Exemple #5
0
    def selectSources(self, sourceCat, matches=None, exposure=None):
        """Return a selection of PSF candidates that represent likely stars.

        A list of PSF candidates may be used by a PSF fitter to construct a PSF.

        Parameters:
        -----------
        sourceCat : `lsst.afw.table.SourceCatalog`
            Catalog of sources to select from.
            This catalog must be contiguous in memory.
        matches : `list` of `lsst.afw.table.ReferenceMatch` or None
            Ignored in this SourceSelector.
        exposure : `lsst.afw.image.Exposure` or None
            The exposure the catalog was built from; used to get the detector
            to transform to TanPix, and for debug display.

        Return
        ------
        struct : `lsst.pipe.base.Struct`
            The struct contains the following data:

            - selected : `array` of `bool``
                Boolean array of sources that were selected, same length as
                sourceCat.
        """
        import lsstDebug
        display = lsstDebug.Info(__name__).display
        displayExposure = lsstDebug.Info(
            __name__).displayExposure  # display the Exposure + spatialCells
        plotMagSize = lsstDebug.Info(
            __name__).plotMagSize  # display the magnitude-size relation
        dumpData = lsstDebug.Info(
            __name__).dumpData  # dump data to pickle file?

        detector = None
        pixToTanPix = None
        if exposure:
            detector = exposure.getDetector()
        if detector:
            pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
        #
        # Look at the distribution of stars in the magnitude-size plane
        #
        flux = sourceCat.get(self.config.sourceFluxField)
        fluxErr = sourceCat.get(self.config.sourceFluxField + "Err")

        xx = numpy.empty(len(sourceCat))
        xy = numpy.empty_like(xx)
        yy = numpy.empty_like(xx)
        for i, source in enumerate(sourceCat):
            Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
            if pixToTanPix:
                p = lsst.geom.Point2D(source.getX(), source.getY())
                linTransform = afwGeom.linearizeTransform(pixToTanPix,
                                                          p).getLinear()
                m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
                m.transform(linTransform)
                Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()

            xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy

        width = numpy.sqrt(0.5 * (xx + yy))
        with numpy.errstate(invalid="ignore"):  # suppress NAN warnings
            bad = reduce(lambda x, y: numpy.logical_or(x, sourceCat.get(y)),
                         self.config.badFlags, False)
            bad = numpy.logical_or(bad,
                                   numpy.logical_not(numpy.isfinite(width)))
            bad = numpy.logical_or(bad,
                                   numpy.logical_not(numpy.isfinite(flux)))
            if self.config.doFluxLimit:
                bad = numpy.logical_or(bad, flux < self.config.fluxMin)
                if self.config.fluxMax > 0:
                    bad = numpy.logical_or(bad, flux > self.config.fluxMax)
            if self.config.doSignalToNoiseLimit:
                bad = numpy.logical_or(
                    bad, flux / fluxErr < self.config.signalToNoiseMin)
                if self.config.signalToNoiseMax > 0:
                    bad = numpy.logical_or(
                        bad, flux / fluxErr > self.config.signalToNoiseMax)
            bad = numpy.logical_or(bad, width < self.config.widthMin)
            bad = numpy.logical_or(bad, width > self.config.widthMax)
        good = numpy.logical_not(bad)

        if not numpy.any(good):
            raise RuntimeError(
                "No objects passed our cuts for consideration as psf stars")

        mag = -2.5 * numpy.log10(flux[good])
        width = width[good]
        #
        # Look for the maximum in the size histogram, then search upwards for the minimum that separates
        # the initial peak (of, we presume, stars) from the galaxies
        #
        if dumpData:
            import os
            import pickle as pickle
            _ii = 0
            while True:
                pickleFile = os.path.expanduser(
                    os.path.join("~", "widths-%d.pkl" % _ii))
                if not os.path.exists(pickleFile):
                    break
                _ii += 1

            with open(pickleFile, "wb") as fd:
                pickle.dump(mag, fd, -1)
                pickle.dump(width, fd, -1)

        centers, clusterId = _kcenters(
            width,
            nCluster=4,
            useMedian=True,
            widthStdAllowed=self.config.widthStdAllowed)

        if display and plotMagSize:
            fig = plot(
                mag,
                width,
                centers,
                clusterId,
                magType=self.config.sourceFluxField.split(".")[-1].title(),
                marker="+",
                markersize=3,
                markeredgewidth=None,
                ltype=':',
                clear=True)
        else:
            fig = None

        clusterId = _improveCluster(
            width,
            centers,
            clusterId,
            nsigma=self.config.nSigmaClip,
            widthStdAllowed=self.config.widthStdAllowed)

        if display and plotMagSize:
            plot(mag,
                 width,
                 centers,
                 clusterId,
                 marker="x",
                 markersize=3,
                 markeredgewidth=None,
                 clear=False)

        stellar = (clusterId == 0)
        #
        # We know enough to plot, if so requested
        #
        frame = 0

        if fig:
            if display and displayExposure:
                disp = afwDisplay.Display(frame=frame)
                disp.mtv(exposure.getMaskedImage(), title="PSF candidates")

                global eventHandler
                eventHandler = EventHandler(fig.get_axes()[0],
                                            mag,
                                            width,
                                            sourceCat.getX()[good],
                                            sourceCat.getY()[good],
                                            frames=[frame])

            fig.show()

            while True:
                try:
                    reply = input("continue? [c h(elp) q(uit) p(db)] ").strip()
                except EOFError:
                    reply = None
                if not reply:
                    reply = "c"

                if reply:
                    if reply[0] == "h":
                        print("""\
    We cluster the points; red are the stellar candidates and the other colours are other clusters.
    Points labelled + are rejects from the cluster (only for cluster 0).

    At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text

    If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in the
    image display.
    """)
                    elif reply[0] == "p":
                        import pdb
                        pdb.set_trace()
                    elif reply[0] == 'q':
                        sys.exit(1)
                    else:
                        break

        if display and displayExposure:
            mi = exposure.getMaskedImage()
            with disp.Buffering():
                for i, source in enumerate(sourceCat):
                    if good[i]:
                        ctype = afwDisplay.GREEN  # star candidate
                    else:
                        ctype = afwDisplay.RED  # not star

                    disp.dot("+",
                             source.getX() - mi.getX0(),
                             source.getY() - mi.getY0(),
                             ctype=ctype)

        # stellar only applies to good==True objects
        mask = good == True  # noqa (numpy bool comparison): E712
        good[mask] = stellar

        return Struct(selected=good)
    def selectSources(self, sourceCat, matches=None, exposure=None):
        """Return a selection of PSF candidates that represent likely stars.

        A list of PSF candidates may be used by a PSF fitter to construct a PSF.

        Parameters:
        -----------
        sourceCat : `lsst.afw.table.SourceCatalog`
            Catalog of sources to select from.
            This catalog must be contiguous in memory.
        matches : `list` of `lsst.afw.table.ReferenceMatch` or None
            Ignored in this SourceSelector.
        exposure : `lsst.afw.image.Exposure` or None
            The exposure the catalog was built from; used to get the detector
            to transform to TanPix, and for debug display.

        Return
        ------
        struct : `lsst.pipe.base.Struct`
            The struct contains the following data:

            - selected : `array` of `bool``
                Boolean array of sources that were selected, same length as
                sourceCat.
        """
        import lsstDebug
        display = lsstDebug.Info(__name__).display
        displayExposure = lsstDebug.Info(__name__).displayExposure     # display the Exposure + spatialCells
        plotMagSize = lsstDebug.Info(__name__).plotMagSize             # display the magnitude-size relation
        dumpData = lsstDebug.Info(__name__).dumpData                   # dump data to pickle file?

        detector = None
        pixToTanPix = None
        if exposure:
            detector = exposure.getDetector()
        if detector:
            pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
        #
        # Look at the distribution of stars in the magnitude-size plane
        #
        flux = sourceCat.get(self.config.sourceFluxField)
        fluxErr = sourceCat.get(self.config.sourceFluxField + "Err")

        xx = numpy.empty(len(sourceCat))
        xy = numpy.empty_like(xx)
        yy = numpy.empty_like(xx)
        for i, source in enumerate(sourceCat):
            Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
            if pixToTanPix:
                p = lsst.geom.Point2D(source.getX(), source.getY())
                linTransform = afwGeom.linearizeTransform(pixToTanPix, p).getLinear()
                m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
                m.transform(linTransform)
                Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()

            xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy

        width = numpy.sqrt(0.5*(xx + yy))
        with numpy.errstate(invalid="ignore"):  # suppress NAN warnings
            bad = reduce(lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags, False)
            bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
            bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
            if self.config.doFluxLimit:
                bad = numpy.logical_or(bad, flux < self.config.fluxMin)
                if self.config.fluxMax > 0:
                    bad = numpy.logical_or(bad, flux > self.config.fluxMax)
            if self.config.doSignalToNoiseLimit:
                bad = numpy.logical_or(bad, flux/fluxErr < self.config.signalToNoiseMin)
                if self.config.signalToNoiseMax > 0:
                    bad = numpy.logical_or(bad, flux/fluxErr > self.config.signalToNoiseMax)
            bad = numpy.logical_or(bad, width < self.config.widthMin)
            bad = numpy.logical_or(bad, width > self.config.widthMax)
        good = numpy.logical_not(bad)

        if not numpy.any(good):
            raise RuntimeError("No objects passed our cuts for consideration as psf stars")

        mag = -2.5*numpy.log10(flux[good])
        width = width[good]
        #
        # Look for the maximum in the size histogram, then search upwards for the minimum that separates
        # the initial peak (of, we presume, stars) from the galaxies
        #
        if dumpData:
            import os
            import pickle as pickle
            _ii = 0
            while True:
                pickleFile = os.path.expanduser(os.path.join("~", "widths-%d.pkl" % _ii))
                if not os.path.exists(pickleFile):
                    break
                _ii += 1

            with open(pickleFile, "wb") as fd:
                pickle.dump(mag, fd, -1)
                pickle.dump(width, fd, -1)

        centers, clusterId = _kcenters(width, nCluster=4, useMedian=True,
                                       widthStdAllowed=self.config.widthStdAllowed)

        if display and plotMagSize:
            fig = plot(mag, width, centers, clusterId,
                       magType=self.config.sourceFluxField.split(".")[-1].title(),
                       marker="+", markersize=3, markeredgewidth=None, ltype=':', clear=True)
        else:
            fig = None

        clusterId = _improveCluster(width, centers, clusterId,
                                    nsigma=self.config.nSigmaClip,
                                    widthStdAllowed=self.config.widthStdAllowed)

        if display and plotMagSize:
            plot(mag, width, centers, clusterId, marker="x", markersize=3, markeredgewidth=None, clear=False)

        stellar = (clusterId == 0)
        #
        # We know enough to plot, if so requested
        #
        frame = 0

        if fig:
            if display and displayExposure:
                disp = afwDisplay.Display(frame=frame)
                disp.mtv(exposure.getMaskedImage(), title="PSF candidates")

                global eventHandler
                eventHandler = EventHandler(fig.get_axes()[0], mag, width,
                                            sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])

            fig.show()

            while True:
                try:
                    reply = input("continue? [c h(elp) q(uit) p(db)] ").strip()
                except EOFError:
                    reply = None
                if not reply:
                    reply = "c"

                if reply:
                    if reply[0] == "h":
                        print("""\
    We cluster the points; red are the stellar candidates and the other colours are other clusters.
    Points labelled + are rejects from the cluster (only for cluster 0).

    At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text

    If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in the
    image display.
    """)
                    elif reply[0] == "p":
                        import pdb
                        pdb.set_trace()
                    elif reply[0] == 'q':
                        sys.exit(1)
                    else:
                        break

        if display and displayExposure:
            mi = exposure.getMaskedImage()
            with disp.Buffering():
                for i, source in enumerate(sourceCat):
                    if good[i]:
                        ctype = afwDisplay.GREEN  # star candidate
                    else:
                        ctype = afwDisplay.RED  # not star

                    disp.dot("+", source.getX() - mi.getX0(), source.getY() - mi.getY0(), ctype=ctype)

        # stellar only applies to good==True objects
        mask = good == True  # noqa (numpy bool comparison): E712
        good[mask] = stellar

        return Struct(selected=good)
    def checkLinearize(self, transform, invertible):
        """Test whether a specific transform is correctly linearized.

        Parameters
        ----------
        transform: `lsst.afw.geom.Transform`
            the transform whose linearization will be tested. Should not be
            strongly curved within ~1 unit of the origin, or the test may rule
            the approximation isn't good enough.
        invertible: `bool`
            whether `transform` is invertible. The test will verify that the
            linearized form is invertible iff `transform` is. If `transform`
            is invertible, the test will also verify that the inverse of the
            linearization approximates the inverse of `transform`.
        """
        fromEndpoint = transform.fromEndpoint
        toEndpoint = transform.toEndpoint
        nIn = fromEndpoint.nAxes
        nOut = toEndpoint.nAxes
        msg = "TransformClass={}, nIn={}, nOut={}".format(type(transform).__name__, nIn, nOut)

        rawLinPoint = self.makeRawPointData(nIn)
        linPoint = fromEndpoint.pointFromData(rawLinPoint)
        affine = afwGeom.linearizeTransform(transform, linPoint)
        self.assertIsInstance(affine, lsst.geom.AffineTransform)

        # Does affine match exact transform at linPoint?
        outPoint = transform.applyForward(linPoint)
        outPointLinearized = affine(linPoint)
        assert_allclose(toEndpoint.dataFromPoint(outPoint),
                        toEndpoint.dataFromPoint(outPointLinearized),
                        err_msg=msg)
        jacobian = transform.getJacobian(linPoint)
        jacobianLinearized = affine.getLinear().getMatrix()
        assert_allclose(jacobian, jacobianLinearized)

        # Is affine a local approximation around linPoint?
        for deltaFrom in (
            np.zeros(nIn),
            np.full(nIn, 0.1),
            np.array([0.1, -0.15, 0.20, -0.05, 0.0, -0.1][0:nIn])
        ):
            tweakedInPoint = fromEndpoint.pointFromData(
                rawLinPoint + deltaFrom)
            tweakedOutPoint = transform.applyForward(tweakedInPoint)
            tweakedOutPointLinearized = affine(tweakedInPoint)
            assert_allclose(
                toEndpoint.dataFromPoint(tweakedOutPoint),
                toEndpoint.dataFromPoint(tweakedOutPointLinearized),
                atol=1e-3,
                err_msg=msg)

        # Is affine invertible?
        # AST lets all-zero MatrixMaps be invertible though inverse
        # ill-defined; exclude this case
        if invertible:
            rng = np.random.RandomState(42)
            nDelta = 100
            inverse = affine.inverted()
            deltaFrom = rng.normal(0.0, 10.0, (nIn, nDelta))
            for i in range(nDelta):
                pointMsg = "{}, point={}".format(msg, tweakedInPoint)
                tweakedInPoint = fromEndpoint.pointFromData(
                    rawLinPoint + deltaFrom[:, i])
                tweakedOutPoint = affine(tweakedInPoint)

                roundTrip = inverse(tweakedOutPoint)
                assert_allclose(
                    roundTrip, tweakedInPoint,
                    err_msg=pointMsg)
                assert_allclose(
                    inverse.getLinear().getMatrix(),
                    np.linalg.inv(jacobian),
                    err_msg=pointMsg)
        else:
            # TODO: replace with correct type after fixing DM-11248
            with self.assertRaises(Exception):
                affine.inverted()