Пример #1
0
def get_approx_psf_size_and_shape(bbox,
                                  psf,
                                  wcs,
                                  ra,
                                  dec,
                                  nx=20,
                                  ny=20,
                                  orderx=2,
                                  ordery=2):
    pts = [
        lsst.geom.SpherePoint(r * lsst.geom.degrees, d * lsst.geom.degrees)
        for r, d in zip(ra, dec)
    ]
    pixels = wcs.skyToPixel(pts)

    ctrl = ChebyshevBoundedFieldControl()
    ctrl.orderX = orderx
    ctrl.orderY = ordery
    ctrl.triangular = False

    xSteps = np.linspace(bbox.getMinX(), bbox.getMaxX(), nx)
    ySteps = np.linspace(bbox.getMinY(), bbox.getMaxY(), ny)
    x = np.tile(xSteps, nx)
    y = np.repeat(ySteps, ny)

    psf_size = np.zeros(x.size)
    psf_e1 = np.zeros(x.size)
    psf_e2 = np.zeros(x.size)

    for i in range(x.size):
        shape = psf.computeShape(lsst.geom.Point2D(x[i], y[i]))
        psf_size[i] = shape.getDeterminantRadius()
        ixx = shape.getIxx()
        iyy = shape.getIyy()
        ixy = shape.getIxy()

        psf_e1[i] = (ixx - iyy) / (ixx + iyy + 2. * psf_size[i]**2.)
        psf_e2[i] = (2. * ixy) / (ixx + iyy + 2. * psf_size[i]**2.)

    pixel_x = np.array([pix.getX() for pix in pixels])
    pixel_y = np.array([pix.getY() for pix in pixels])

    cheb_size = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y,
                                          psf_size, ctrl)
    psf_size_pts = cheb_size.evaluate(pixel_x, pixel_y)
    cheb_e1 = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_e1,
                                        ctrl)
    psf_e1_pts = cheb_e1.evaluate(pixel_x, pixel_y)
    cheb_e2 = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_e2,
                                        ctrl)
    psf_e2_pts = cheb_e2.evaluate(pixel_x, pixel_y)

    return psf_size_pts, psf_e1_pts, psf_e2_pts
Пример #2
0
    def run(self, exposure, catalog):
        """!Measure aperture correction

        @param[in]  exposure  Exposure aperture corrections are being measured
                              on. The bounding box is retrieved from it, and
                              it is passed to the sourceSelector.
                              The output aperture correction map is *not*
                              added to the exposure; this is left to the
                              caller.

        @param[in]  catalog   SourceCatalog containing measurements to be used
                              to compute aperturecorrections.

        @return an lsst.pipe.base.Struct containing:
        - apCorrMap: an aperture correction map (lsst.afw.image.ApCorrMap) that contains two entries
            for each flux field:
            - flux field (e.g. base_PsfFlux_instFlux): 2d model
            - flux sigma field (e.g. base_PsfFlux_instFluxErr): 2d model of error
        """
        bbox = exposure.getBBox()
        import lsstDebug
        display = lsstDebug.Info(__name__).display
        doPause = lsstDebug.Info(__name__).doPause

        self.log.info("Measuring aperture corrections for %d flux fields" %
                      (len(self.toCorrect), ))
        # First, create a subset of the catalog that contains only selected stars
        # with non-flagged reference fluxes.
        subset1 = [
            record for record in self.sourceSelector.run(
                catalog, exposure=exposure).sourceCat
            if (not record.get(self.refFluxKeys.flag)
                and numpy.isfinite(record.get(self.refFluxKeys.flux)))
        ]

        apCorrMap = ApCorrMap()

        # Outer loop over the fields we want to correct
        for name, keys in self.toCorrect.items():
            fluxName = name + "_instFlux"
            fluxErrName = name + "_instFluxErr"

            # Create a more restricted subset with only the objects where the to-be-correct flux
            # is not flagged.
            fluxes = numpy.fromiter(
                (record.get(keys.flux) for record in subset1), float)
            with numpy.errstate(invalid="ignore"):  # suppress NAN warnings
                isGood = numpy.logical_and.reduce([
                    numpy.fromiter((not record.get(keys.flag)
                                    for record in subset1), bool),
                    numpy.isfinite(fluxes),
                    fluxes > 0.0,
                ])
            subset2 = [record for record, good in zip(subset1, isGood) if good]

            # Check that we have enough data points that we have at least the minimum of degrees of
            # freedom specified in the config.
            if len(subset2) - 1 < self.config.minDegreesOfFreedom:
                if name in self.config.allowFailure:
                    self.log.warn(
                        "Unable to measure aperture correction for '%s': "
                        "only %d sources, but require at least %d." %
                        (name, len(subset2),
                         self.config.minDegreesOfFreedom + 1))
                    continue
                raise RuntimeError(
                    "Unable to measure aperture correction for required algorithm '%s': "
                    "only %d sources, but require at least %d." %
                    (name, len(subset2), self.config.minDegreesOfFreedom + 1))

            # If we don't have enough data points to constrain the fit, reduce the order until we do
            ctrl = self.config.fitConfig.makeControl()
            while len(subset2) - ctrl.computeSize(
            ) < self.config.minDegreesOfFreedom:
                if ctrl.orderX > 0:
                    ctrl.orderX -= 1
                if ctrl.orderY > 0:
                    ctrl.orderY -= 1

            # Fill numpy arrays with positions and the ratio of the reference flux to the to-correct flux
            x = numpy.zeros(len(subset2), dtype=float)
            y = numpy.zeros(len(subset2), dtype=float)
            apCorrData = numpy.zeros(len(subset2), dtype=float)
            indices = numpy.arange(len(subset2), dtype=int)
            for n, record in enumerate(subset2):
                x[n] = record.getX()
                y[n] = record.getY()
                apCorrData[n] = record.get(self.refFluxKeys.flux) / record.get(
                    keys.flux)

            for _i in range(self.config.numIter):

                # Do the fit, save it in the output map
                apCorrField = ChebyshevBoundedField.fit(
                    bbox, x, y, apCorrData, ctrl)

                if display:
                    plotApCorr(bbox, x, y, apCorrData, apCorrField,
                               "%s, iteration %d" % (name, _i), doPause)

                # Compute errors empirically, using the RMS difference between the true reference flux and the
                # corrected to-be-corrected flux.
                apCorrDiffs = apCorrField.evaluate(x, y)
                apCorrDiffs -= apCorrData
                apCorrErr = numpy.mean(apCorrDiffs**2)**0.5

                # Clip bad data points
                apCorrDiffLim = self.config.numSigmaClip * apCorrErr
                with numpy.errstate(invalid="ignore"):  # suppress NAN warning
                    keep = numpy.fabs(apCorrDiffs) <= apCorrDiffLim
                x = x[keep]
                y = y[keep]
                apCorrData = apCorrData[keep]
                indices = indices[keep]

            # Final fit after clipping
            apCorrField = ChebyshevBoundedField.fit(bbox, x, y, apCorrData,
                                                    ctrl)

            self.log.info(
                "Aperture correction for %s: RMS %f from %d" %
                (name, numpy.mean((apCorrField.evaluate(x, y) - apCorrData)**2)
                 **0.5, len(indices)))

            if display:
                plotApCorr(bbox, x, y, apCorrData, apCorrField,
                           "%s, final" % (name, ), doPause)

            # Save the result in the output map
            # The error is constant spatially (we could imagine being
            # more clever, but we're not yet sure if it's worth the effort).
            # We save the errors as a 0th-order ChebyshevBoundedField
            apCorrMap[fluxName] = apCorrField
            apCorrErrCoefficients = numpy.array([[apCorrErr]], dtype=float)
            apCorrMap[fluxErrName] = ChebyshevBoundedField(
                bbox, apCorrErrCoefficients)

            # Record which sources were used
            for i in indices:
                subset2[i].set(keys.used, True)

        return Struct(apCorrMap=apCorrMap, )
    def run(self, exposure, catalog):
        """!Measure aperture correction

        @param[in]  exposure  Exposure aperture corrections are being measured
                              on. The bounding box is retrieved from it, and
                              it is passed to the sourceSelector.
                              The output aperture correction map is *not*
                              added to the exposure; this is left to the
                              caller.

        @param[in]  catalog   SourceCatalog containing measurements to be used
                              to compute aperturecorrections.

        @return an lsst.pipe.base.Struct containing:
        - apCorrMap: an aperture correction map (lsst.afw.image.ApCorrMap) that contains two entries
            for each flux field:
            - flux field (e.g. base_PsfFlux_instFlux): 2d model
            - flux sigma field (e.g. base_PsfFlux_instFluxErr): 2d model of error
        """
        bbox = exposure.getBBox()
        import lsstDebug
        display = lsstDebug.Info(__name__).display
        doPause = lsstDebug.Info(__name__).doPause

        self.log.info("Measuring aperture corrections for %d flux fields" % (len(self.toCorrect),))
        # First, create a subset of the catalog that contains only selected stars
        # with non-flagged reference fluxes.
        subset1 = [record for record in self.sourceSelector.run(catalog, exposure=exposure).sourceCat
                   if (not record.get(self.refFluxKeys.flag) and
                       numpy.isfinite(record.get(self.refFluxKeys.flux)))]

        apCorrMap = ApCorrMap()

        # Outer loop over the fields we want to correct
        for name, keys in self.toCorrect.items():
            fluxName = name + "_instFlux"
            fluxErrName = name + "_instFluxErr"

            # Create a more restricted subset with only the objects where the to-be-correct flux
            # is not flagged.
            fluxes = numpy.fromiter((record.get(keys.flux) for record in subset1), float)
            with numpy.errstate(invalid="ignore"):  # suppress NAN warnings
                isGood = numpy.logical_and.reduce([
                    numpy.fromiter((not record.get(keys.flag) for record in subset1), bool),
                    numpy.isfinite(fluxes),
                    fluxes > 0.0,
                ])
            subset2 = [record for record, good in zip(subset1, isGood) if good]

            # Check that we have enough data points that we have at least the minimum of degrees of
            # freedom specified in the config.
            if len(subset2) - 1 < self.config.minDegreesOfFreedom:
                if name in self.config.allowFailure:
                    self.log.warn("Unable to measure aperture correction for '%s': "
                                  "only %d sources, but require at least %d." %
                                  (name, len(subset2), self.config.minDegreesOfFreedom+1))
                    continue
                raise RuntimeError("Unable to measure aperture correction for required algorithm '%s': "
                                   "only %d sources, but require at least %d." %
                                   (name, len(subset2), self.config.minDegreesOfFreedom+1))

            # If we don't have enough data points to constrain the fit, reduce the order until we do
            ctrl = self.config.fitConfig.makeControl()
            while len(subset2) - ctrl.computeSize() < self.config.minDegreesOfFreedom:
                if ctrl.orderX > 0:
                    ctrl.orderX -= 1
                if ctrl.orderY > 0:
                    ctrl.orderY -= 1

            # Fill numpy arrays with positions and the ratio of the reference flux to the to-correct flux
            x = numpy.zeros(len(subset2), dtype=float)
            y = numpy.zeros(len(subset2), dtype=float)
            apCorrData = numpy.zeros(len(subset2), dtype=float)
            indices = numpy.arange(len(subset2), dtype=int)
            for n, record in enumerate(subset2):
                x[n] = record.getX()
                y[n] = record.getY()
                apCorrData[n] = record.get(self.refFluxKeys.flux)/record.get(keys.flux)

            for _i in range(self.config.numIter):

                # Do the fit, save it in the output map
                apCorrField = ChebyshevBoundedField.fit(bbox, x, y, apCorrData, ctrl)

                if display:
                    plotApCorr(bbox, x, y, apCorrData, apCorrField, "%s, iteration %d" % (name, _i), doPause)

                # Compute errors empirically, using the RMS difference between the true reference flux and the
                # corrected to-be-corrected flux.
                apCorrDiffs = apCorrField.evaluate(x, y)
                apCorrDiffs -= apCorrData
                apCorrErr = numpy.mean(apCorrDiffs**2)**0.5

                # Clip bad data points
                apCorrDiffLim = self.config.numSigmaClip * apCorrErr
                with numpy.errstate(invalid="ignore"):  # suppress NAN warning
                    keep = numpy.fabs(apCorrDiffs) <= apCorrDiffLim
                x = x[keep]
                y = y[keep]
                apCorrData = apCorrData[keep]
                indices = indices[keep]

            # Final fit after clipping
            apCorrField = ChebyshevBoundedField.fit(bbox, x, y, apCorrData, ctrl)

            self.log.info("Aperture correction for %s: RMS %f from %d" %
                          (name, numpy.mean((apCorrField.evaluate(x, y) - apCorrData)**2)**0.5, len(indices)))

            if display:
                plotApCorr(bbox, x, y, apCorrData, apCorrField, "%s, final" % (name,), doPause)

            # Save the result in the output map
            # The error is constant spatially (we could imagine being
            # more clever, but we're not yet sure if it's worth the effort).
            # We save the errors as a 0th-order ChebyshevBoundedField
            apCorrMap[fluxName] = apCorrField
            apCorrErrCoefficients = numpy.array([[apCorrErr]], dtype=float)
            apCorrMap[fluxErrName] = ChebyshevBoundedField(bbox, apCorrErrCoefficients)

            # Record which sources were used
            for i in indices:
                subset2[i].set(keys.used, True)

        return Struct(
            apCorrMap=apCorrMap,
        )
def compute_approx_psf_size_and_shape(ccd_row,
                                      ra,
                                      dec,
                                      nx=20,
                                      ny=20,
                                      orderx=2,
                                      ordery=2):
    """Compute the approximate psf size and shape.

    This routine fits how the psf size and shape varies over a field by approximating
    with a Chebyshev bounded field.

    Parameters
    ----------
    ccd_row : `lsst.afw.table.ExposureRecord`
        Exposure metadata for a given detector exposure.
    ra : `np.ndarray`
        Right ascension of points to compute size and shape (degrees).
    dec : `np.ndarray`
        Declination of points to compute size and shape (degrees).
    nx : `int`, optional
        Number of sampling points in the x direction.
    ny : `int`, optional
        Number of sampling points in the y direction.
    orderx : `int`, optional
        Chebyshev polynomial order for fit in x direction.
    ordery : `int`, optional
        Chebyshev polynomial order for fit in y direction.

    Returns
    -------
    psf_array : `np.ndarray`
        Record array with "psf_size", "psf_e1", "psf_e2".
    """
    pts = [
        lsst.geom.SpherePoint(r * lsst.geom.degrees, d * lsst.geom.degrees)
        for r, d in zip(ra, dec)
    ]
    pixels = ccd_row.getWcs().skyToPixel(pts)

    ctrl = ChebyshevBoundedFieldControl()
    ctrl.orderX = orderx
    ctrl.orderY = ordery
    ctrl.triangular = False

    bbox = ccd_row.getBBox()
    xSteps = np.linspace(bbox.getMinX(), bbox.getMaxX(), nx)
    ySteps = np.linspace(bbox.getMinY(), bbox.getMaxY(), ny)
    x = np.tile(xSteps, nx)
    y = np.repeat(ySteps, ny)

    psf_size = np.zeros(x.size)
    psf_e1 = np.zeros(x.size)
    psf_e2 = np.zeros(x.size)
    psf_area = np.zeros(x.size)

    psf = ccd_row.getPsf()
    for i in range(x.size):
        shape = psf.computeShape(lsst.geom.Point2D(x[i], y[i]))
        psf_size[i] = shape.getDeterminantRadius()
        ixx = shape.getIxx()
        iyy = shape.getIyy()
        ixy = shape.getIxy()

        psf_e1[i] = (ixx - iyy) / (ixx + iyy + 2. * psf_size[i]**2.)
        psf_e2[i] = (2. * ixy) / (ixx + iyy + 2. * psf_size[i]**2.)

        im = psf.computeKernelImage(lsst.geom.Point2D(x[i], y[i]))
        psf_area[i] = np.sum(im.array) / np.sum(im.array**2.)

    pixel_x = np.array([pix.getX() for pix in pixels])
    pixel_y = np.array([pix.getY() for pix in pixels])

    psf_array = np.zeros(pixel_x.size,
                         dtype=[("psf_size", "f8"), ("psf_e1", "f8"),
                                ("psf_e2", "f8"), ("psf_area", "f8")])

    cheb_size = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y,
                                          psf_size, ctrl)
    psf_array["psf_size"] = cheb_size.evaluate(pixel_x, pixel_y)
    cheb_e1 = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_e1,
                                        ctrl)
    psf_array["psf_e1"] = cheb_e1.evaluate(pixel_x, pixel_y)
    cheb_e2 = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_e2,
                                        ctrl)
    psf_array["psf_e2"] = cheb_e2.evaluate(pixel_x, pixel_y)
    cheb_area = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y,
                                          psf_area, ctrl)
    psf_array["psf_area"] = cheb_area.evaluate(pixel_x, pixel_y)

    return psf_array
    def run(self, bbox, catalog):
        """!Measure aperture correction

        @return an lsst.pipe.base.Struct containing:
        - apCorrMap: an aperture correction map (lsst.afw.image.ApCorrMap) that contains two entries
            for each flux field:
            - flux field (e.g. base_PsfFlux_flux): 2d model
            - flux sigma field (e.g. base_PsfFlux_fluxSigma): 2d model of error
        """
        self.log.info("Measuring aperture corrections for %d flux fields" % (len(self.toCorrect),))
        # First, create a subset of the catalog that contains only objects with inputFilterFlag set
        # and non-flagged reference fluxes.
        subset1 = [record for record in catalog
                   if record.get(self.inputFilterFlag) and not record.get(self.refFluxKeys.flag)]

        apCorrMap = ApCorrMap()

        # Outer loop over the fields we want to correct
        for name, keys in self.toCorrect.iteritems():
            fluxName = name + "_flux"
            fluxSigmaName = name + "_fluxSigma"

            # Create a more restricted subset with only the objects where the to-be-correct flux
            # is not flagged.
            subset2 = [record for record in subset1 if not record.get(keys.flag)]

            # Check that we have enough data points that we have at least the minimum of degrees of
            # freedom specified in the config.
            if len(subset2) - 1 < self.config.minDegreesOfFreedom:
                self.log.warn("Only %d sources for calculation of aperture correction for '%s'; "
                              "setting to 1.0" % (len(subset2), name,))
                apCorrMap[fluxName] = ChebyshevBoundedField(bbox, numpy.ones((1,1), dtype=float))
                apCorrMap[fluxSigmaName] = ChebyshevBoundedField(bbox, numpy.zeros((1,1), dtype=float))
                continue

            # If we don't have enough data points to constrain the fit, reduce the order until we do
            ctrl = self.config.fitConfig.makeControl()
            while len(subset2) - ctrl.computeSize() < self.config.minDegreesOfFreedom:
                if ctrl.orderX > 0:
                    ctrl.orderX -= 1
                if ctrl.orderY > 0:
                    ctrl.orderY -= 1

            # Fill numpy arrays with positions and the ratio of the reference flux to the to-correct flux
            x = numpy.zeros(len(subset2), dtype=float)
            y = numpy.zeros(len(subset2), dtype=float)
            apCorrData = numpy.zeros(len(subset2), dtype=float)
            indices = numpy.arange(len(subset2), dtype=int)
            for n, record in enumerate(subset2):
                x[n] = record.getX()
                y[n] = record.getY()
                apCorrData[n] = record.get(self.refFluxKeys.flux)/record.get(keys.flux)

            for _i in range(self.config.numIter):

                # Do the fit, save it in the output map
                apCorrField = ChebyshevBoundedField.fit(bbox, x, y, apCorrData, ctrl)

                # Compute errors empirically, using the RMS difference between the true reference flux and the
                # corrected to-be-corrected flux.
                apCorrDiffs = apCorrField.evaluate(x, y)
                apCorrDiffs -= apCorrData
                apCorrErr = numpy.mean(apCorrDiffs**2)**0.5

                # Clip bad data points
                apCorrDiffLim = self.config.numSigmaClip * apCorrErr
                keep = numpy.fabs(apCorrDiffs) <= apCorrDiffLim
                x = x[keep]
                y = y[keep]
                apCorrData = apCorrData[keep]
                indices = indices[keep]

            # Final fit after clipping
            apCorrField = ChebyshevBoundedField.fit(bbox, x, y, apCorrData, ctrl)

            self.log.info("Aperture correction for %s: RMS %f from %d" %
                          (name, numpy.mean((apCorrField.evaluate(x, y) - apCorrData)**2)**0.5, len(indices)))

            # Save the result in the output map
            # The error is constant spatially (we could imagine being
            # more clever, but we're not yet sure if it's worth the effort).
            # We save the errors as a 0th-order ChebyshevBoundedField
            apCorrMap[fluxName] = apCorrField
            apCorrErrCoefficients = numpy.array([[apCorrErr]], dtype=float)
            apCorrMap[fluxSigmaName] = ChebyshevBoundedField(bbox, apCorrErrCoefficients)

            # Record which sources were used
            for i in indices:
                subset2[i].set(keys.used, True)

        return Struct(
            apCorrMap = apCorrMap,
        )
Пример #6
0
    def run(self, exposure, catalog):
        """!Measure aperture correction

        @param[in]  exposure  Exposure aperture corrections are being measured
                              on.  Aside from the bounding box, the exposure
                              is only used by the starSelector subtask (which
                              may need it to construct PsfCandidates, as
                              PsfCanidate construction can do some filtering).
                              The output aperture correction map is *not*
                              added to the exposure; this is left to the
                              caller.

        @param[in]  catalog   SourceCatalog containing measurements to be used
                              to compute aperturecorrections.

        @return an lsst.pipe.base.Struct containing:
        - apCorrMap: an aperture correction map (lsst.afw.image.ApCorrMap) that contains two entries
            for each flux field:
            - flux field (e.g. base_PsfFlux_flux): 2d model
            - flux sigma field (e.g. base_PsfFlux_fluxSigma): 2d model of error
        """
        bbox = exposure.getBBox()

        self.log.info("Measuring aperture corrections for %d flux fields" %
                      (len(self.toCorrect), ))
        # First, create a subset of the catalog that contains only selected stars
        # with non-flagged reference fluxes.
        subset1 = [
            record for record in self.starSelector.selectStars(
                exposure, catalog).starCat
            if not record.get(self.refFluxKeys.flag)
        ]

        apCorrMap = ApCorrMap()

        # Outer loop over the fields we want to correct
        for name, keys in self.toCorrect.iteritems():
            fluxName = name + "_flux"
            fluxSigmaName = name + "_fluxSigma"

            # Create a more restricted subset with only the objects where the to-be-correct flux
            # is not flagged.
            subset2 = [
                record for record in subset1 if not record.get(keys.flag)
            ]

            # Check that we have enough data points that we have at least the minimum of degrees of
            # freedom specified in the config.
            if len(subset2) - 1 < self.config.minDegreesOfFreedom:
                raise RuntimeError(
                    "Only %d sources for calculation of aperture correction for '%s'; "
                    "require at least %d." %
                    (len(subset2), name, self.config.minDegreesOfFreedom + 1))
                apCorrMap[fluxName] = ChebyshevBoundedField(
                    bbox, numpy.ones((1, 1), dtype=float))
                apCorrMap[fluxSigmaName] = ChebyshevBoundedField(
                    bbox, numpy.zeros((1, 1), dtype=float))
                continue

            # If we don't have enough data points to constrain the fit, reduce the order until we do
            ctrl = self.config.fitConfig.makeControl()
            while len(subset2) - ctrl.computeSize(
            ) < self.config.minDegreesOfFreedom:
                if ctrl.orderX > 0:
                    ctrl.orderX -= 1
                if ctrl.orderY > 0:
                    ctrl.orderY -= 1

            # Fill numpy arrays with positions and the ratio of the reference flux to the to-correct flux
            x = numpy.zeros(len(subset2), dtype=float)
            y = numpy.zeros(len(subset2), dtype=float)
            apCorrData = numpy.zeros(len(subset2), dtype=float)
            indices = numpy.arange(len(subset2), dtype=int)
            for n, record in enumerate(subset2):
                x[n] = record.getX()
                y[n] = record.getY()
                apCorrData[n] = record.get(self.refFluxKeys.flux) / record.get(
                    keys.flux)

            for _i in range(self.config.numIter):

                # Do the fit, save it in the output map
                apCorrField = ChebyshevBoundedField.fit(
                    bbox, x, y, apCorrData, ctrl)

                # Compute errors empirically, using the RMS difference between the true reference flux and the
                # corrected to-be-corrected flux.
                apCorrDiffs = apCorrField.evaluate(x, y)
                apCorrDiffs -= apCorrData
                apCorrErr = numpy.mean(apCorrDiffs**2)**0.5

                # Clip bad data points
                apCorrDiffLim = self.config.numSigmaClip * apCorrErr
                keep = numpy.fabs(apCorrDiffs) <= apCorrDiffLim
                x = x[keep]
                y = y[keep]
                apCorrData = apCorrData[keep]
                indices = indices[keep]

            # Final fit after clipping
            apCorrField = ChebyshevBoundedField.fit(bbox, x, y, apCorrData,
                                                    ctrl)

            self.log.info(
                "Aperture correction for %s: RMS %f from %d" %
                (name, numpy.mean((apCorrField.evaluate(x, y) - apCorrData)**2)
                 **0.5, len(indices)))

            # Save the result in the output map
            # The error is constant spatially (we could imagine being
            # more clever, but we're not yet sure if it's worth the effort).
            # We save the errors as a 0th-order ChebyshevBoundedField
            apCorrMap[fluxName] = apCorrField
            apCorrErrCoefficients = numpy.array([[apCorrErr]], dtype=float)
            apCorrMap[fluxSigmaName] = ChebyshevBoundedField(
                bbox, apCorrErrCoefficients)

            # Record which sources were used
            for i in indices:
                subset2[i].set(keys.used, True)

        return Struct(apCorrMap=apCorrMap, )