Esempio n. 1
0
    def runQuantum(self, butlerQC, inputRefs, outputRefs):
        inputs = butlerQC.get(inputRefs)
        expId, expBits = butlerQC.quantum.dataId.pack("visit_detector",
                                                      returnMaxBits=True)
        inputs['exposureIdInfo'] = ExposureIdInfo(expId, expBits)

        if self.config.doAstrometry:
            refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
                                                          for ref in inputRefs.astromRefCat],
                                                 refCats=inputs.pop('astromRefCat'),
                                                 config=self.config.astromRefObjLoader, log=self.log)
            self.pixelMargin = refObjLoader.config.pixelMargin
            self.astrometry.setRefObjLoader(refObjLoader)

        if self.config.doPhotoCal:
            photoRefObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
                                                      for ref in inputRefs.photoRefCat],
                                                      refCats=inputs.pop('photoRefCat'),
                                                      config=self.config.photoRefObjLoader,
                                                      log=self.log)
            self.pixelMargin = photoRefObjLoader.config.pixelMargin
            self.photoCal.match.setRefObjLoader(photoRefObjLoader)

        outputs = self.run(**inputs)

        if self.config.doWriteMatches and self.config.doAstrometry:
            normalizedMatches = afwTable.packMatches(outputs.astromMatches)
            normalizedMatches.table.setMetadata(outputs.matchMeta)
            if self.config.doWriteMatchesDenormalized:
                denormMatches = denormalizeMatches(outputs.astromMatches, outputs.matchMeta)
                outputs.matchesDenormalized = denormMatches
            outputs.matches = normalizedMatches
        butlerQC.put(outputs, outputRefs)
Esempio n. 2
0
    def test_loadSkyCircle(self):
        """Test the loadSkyCircle routine."""
        loader = ReferenceObjectLoader(
            [dataRef.dataId for dataRef in self.datasetRefs], self.handles)
        center = lsst.geom.SpherePoint(180.0 * lsst.geom.degrees,
                                       0.0 * lsst.geom.degrees)
        cat = loader.loadSkyCircle(
            center,
            30.0 * lsst.geom.degrees,
            filterName='a',
        ).refCat
        # Check that the max distance is less than the radius
        dist = sphdist(180.0, 0.0, np.rad2deg(cat['coord_ra']),
                       np.rad2deg(cat['coord_dec']))
        self.assertLess(np.max(dist), 30.0)

        # Check that all the objects from the two catalogs are here.
        dist = sphdist(180.0, 0.0, self.skyCatalog['ra_icrs'],
                       self.skyCatalog['dec_icrs'])
        inside, = (dist < 30.0).nonzero()
        self.assertEqual(len(cat), len(inside))

        self.assertTrue(cat.isContiguous())
        self.assertEqual(len(np.unique(cat['id'])), len(cat))
        # A default-loaded sky circle should not have centroids
        self.assertNotIn('centroid_x', cat.schema)
        self.assertNotIn('centroid_y', cat.schema)
        self.assertNotIn('hasCentroid', cat.schema)
Esempio n. 3
0
    def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler):
        expId, expBits = butler.registry.packDataId("visit_detector",
                                                    inputDataIds['exposure'],
                                                    returnMaxBits=True)
        inputData['exposureIdInfo'] = ExposureIdInfo(expId, expBits)

        if self.config.doAstrometry:
            refObjLoader = ReferenceObjectLoader(dataIds=inputDataIds['astromRefCat'],
                                                 butler=butler,
                                                 config=self.config.astromRefObjLoader,
                                                 log=self.log)
            self.pixelMargin = refObjLoader.config.pixelMargin
            self.astrometry.setRefObjLoader(refObjLoader)

        if self.config.doPhotoCal:
            photoRefObjLoader = ReferenceObjectLoader(inputDataIds['photoRefCat'],
                                                      butler,
                                                      self.config.photoRefObjLoader,
                                                      self.log)
            self.pixelMargin = photoRefObjLoader.config.pixelMargin
            self.photoCal.match.setRefObjLoader(photoRefObjLoader)

        results = self.run(**inputData)

        if self.config.doWriteMatches:
            normalizedMatches = afwTable.packMatches(results.astromMatches)
            normalizedMatches.table.setMetadata(results.matchMeta)
            if self.config.doWriteMatchesDenormalized:
                denormMatches = denormalizeMatches(results.astromMatches, results.matchMeta)
                results.matchesDenormalized = denormMatches
            results.matches = normalizedMatches
        return results
Esempio n. 4
0
    def test_loadPixelBox(self):
        """Test the loadPixelBox routine."""
        # This will create a box 50 degrees on a side.
        loaderConfig = ReferenceObjectLoader.ConfigClass()
        loaderConfig.pixelMargin = 0
        loader = ReferenceObjectLoader(
            [dataRef.dataId for dataRef in self.datasetRefs],
            self.handles,
            config=loaderConfig)
        bbox = lsst.geom.Box2I(corner=lsst.geom.Point2I(0, 0),
                               dimensions=lsst.geom.Extent2I(1000, 1000))
        crpix = lsst.geom.Point2D(500, 500)
        crval = lsst.geom.SpherePoint(180.0 * lsst.geom.degrees,
                                      0.0 * lsst.geom.degrees)
        cdMatrix = afwGeom.makeCdMatrix(scale=0.05 * lsst.geom.degrees)
        wcs = afwGeom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix)

        cat = loader.loadPixelBox(bbox, wcs, 'a', bboxToSpherePadding=0).refCat

        # This is a sanity check on the ranges; the exact selection depends
        # on cos(dec) and the tangent-plane projection.
        self.assertLess(np.max(np.rad2deg(cat['coord_ra'])), 180.0 + 25.0)
        self.assertGreater(np.max(np.rad2deg(cat['coord_ra'])), 180.0 - 25.0)
        self.assertLess(np.max(np.rad2deg(cat['coord_dec'])), 25.0)
        self.assertGreater(np.min(np.rad2deg(cat['coord_dec'])), -25.0)

        # The following is to ensure the reference catalog coords are
        # getting corrected for proper motion when an epoch is provided.
        # Use an extreme epoch so that differences in corrected coords
        # will be significant.  Note that this simply tests that the coords
        # do indeed change when the epoch is passed.  It makes no attempt
        # at assessing the correctness of the change.  This is left to the
        # explicit testProperMotion() test below.
        catWithEpoch = loader.loadPixelBox(bbox,
                                           wcs,
                                           'a',
                                           bboxToSpherePadding=0,
                                           epoch=astropy.time.Time(
                                               30000,
                                               format='mjd',
                                               scale='tai')).refCat

        self.assertFloatsNotEqual(cat['coord_ra'],
                                  catWithEpoch['coord_ra'],
                                  rtol=1.0e-4)
        self.assertFloatsNotEqual(cat['coord_dec'],
                                  catWithEpoch['coord_dec'],
                                  rtol=1.0e-4)
    def getRefObjLoader(self, refCatalogList):
        """
        Create a `ReferenceObjectLoader` from available reference catalogs
        in the repository.

        Parameters
        ----------
        refCatalogList : `list`
            List of deferred butler references for the reference catalogs.

        Returns
        -------
        `lsst.meas.algorithms.ReferenceObjectsLoader`
            Object to conduct spatial searches through the reference catalogs
        """

        refObjLoader = ReferenceObjectLoader(
            dataIds=[ref.dataId for ref in refCatalogList],
            refCats=refCatalogList,
            config=LoadReferenceObjectsConfig(),
        )
        # This removes the padding around the border of detector BBox when
        # matching to reference catalog.
        # We remove this since we only want sources within detector.
        refObjLoader.config.pixelMargin = 0

        return refObjLoader
Esempio n. 6
0
    def runQuantum(self, butlerQC, inputRefs, outputRefs):
        inputs = butlerQC.get(inputRefs)
        refObjLoader = ReferenceObjectLoader(
            dataIds=[ref.datasetRef.dataId for ref in inputRefs.astromRefCat],
            refCats=inputs.pop('astromRefCat'),
            config=self.config.astromRefObjLoader,
            log=self.log)

        refObjLoader.pixelMargin = 1000
        self.astrometry.setRefObjLoader(refObjLoader)

        # See L603 (def runQuantum(self, butlerQC, inputRefs, outputRefs):)
        # in calibrate.py to put photocal back in

        outputs = self.run(**inputs)
        butlerQC.put(outputs, outputRefs)
Esempio n. 7
0
    def test_requireProperMotion(self):
        """Tests of the requireProperMotion config field."""
        loaderConfig = ReferenceObjectLoader.ConfigClass()
        loaderConfig.requireProperMotion = True
        loader = ReferenceObjectLoader(
            [dataRef.dataId for dataRef in self.datasetRefs],
            self.handles,
            config=loaderConfig)
        center = lsst.geom.SpherePoint(180.0 * lsst.geom.degrees,
                                       0.0 * lsst.geom.degrees)

        # Test that we require an epoch set.
        msg = 'requireProperMotion=True but epoch not provided to loader'
        with self.assertRaisesRegex(RuntimeError, msg):
            loader.loadSkyCircle(center,
                                 30.0 * lsst.geom.degrees,
                                 filterName='a')
Esempio n. 8
0
 def test_filterMap(self):
     """Test filterMap parameters."""
     loaderConfig = ReferenceObjectLoader.ConfigClass()
     loaderConfig.filterMap = {'aprime': 'a'}
     loader = ReferenceObjectLoader(
         [dataRef.dataId for dataRef in self.datasetRefs],
         self.handles,
         config=loaderConfig)
     center = lsst.geom.SpherePoint(180.0 * lsst.geom.degrees,
                                    0.0 * lsst.geom.degrees)
     result = loader.loadSkyCircle(
         center,
         30.0 * lsst.geom.degrees,
         filterName='aprime',
     )
     self.assertEqual(result.fluxField, 'aprime_camFlux')
     self.assertFloatsEqual(result.refCat['aprime_camFlux'],
                            result.refCat['a_flux'])
Esempio n. 9
0
 def runQuantum(self, butlerQC, inputRefs, outputRefs):
     inputs = butlerQC.get(inputRefs)
     inputs['dataId'] = str(butlerQC.quantum.dataId)
     refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
                                                   for ref in inputRefs.refCat],
                                          refCats=inputs.pop("refCat"),
                                          config=self.config.refObjLoader)
     output = self.run(**inputs, refObjLoader=refObjLoader)
     butlerQC.put(output, outputRefs)
Esempio n. 10
0
    def __init__(self, dataIds=None, refCats=None, butler=None, **kwargs):
        if dataIds is not None and refCats is not None:
            pipeBase.Task.__init__(self, **kwargs)
            refConfig = self.config.refObjLoader
            self.refObjLoader = ReferenceObjectLoader(dataIds=dataIds,
                                                      refCats=refCats,
                                                      config=refConfig,
                                                      log=self.log)
        elif butler is not None:
            raise RuntimeError("LoadReferenceCatalogTask does not support Gen2 Butlers.")
        else:
            raise RuntimeError("Must instantiate LoadReferenceCatalogTask with "
                               "dataIds and refCats (Gen3)")

        if self.config.doReferenceSelection:
            self.makeSubtask('referenceSelector')
        self._fluxFilters = None
        self._fluxFields = None
        self._referenceFilter = None
    def __init__(self, dataIds, refCats, **kwargs):

        super().__init__(**kwargs)
        refConfig = self.config.refObjLoader
        # refObjLoader handles the interaction with the butler repository
        # needed to get the pieces of the reference catalogs we need.
        self.refObjLoader = ReferenceObjectLoader(dataIds=dataIds,
                                                  refCats=refCats,
                                                  config=refConfig,
                                                  log=self.log)

        if self.config.doReferenceSelection:
            self.makeSubtask("referenceSelector")

        self.filterName = self.config.filterName
        self.config.refObjLoader.pixelMargin = 0
        self.config.refObjLoader.anyFilterMapsToThis = self.filterName
        self.config.referenceSelector.magLimit.fluxField = f"{self.filterName}_flux"
        self.config.referenceSelector.signalToNoise.fluxField = (
            f"{self.filterName}_flux")
        self.config.donutSelector.fluxField = f"{self.filterName}_flux"
        if self.config.doDonutSelection:
            self.makeSubtask("donutSelector")
Esempio n. 12
0
    def test_properMotion(self):
        """Test proper motion correction."""
        loaderConfig = ReferenceObjectLoader.ConfigClass()
        loaderConfig.filterMap = {'aprime': 'a'}
        loader = ReferenceObjectLoader(
            [dataRef.dataId for dataRef in self.datasetRefs],
            self.handles,
            config=loaderConfig)
        center = lsst.geom.SpherePoint(180.0 * lsst.geom.degrees,
                                       0.0 * lsst.geom.degrees)
        cat = loader.loadSkyCircle(center,
                                   30.0 * lsst.geom.degrees,
                                   filterName='a').refCat

        # Zero epoch change --> no proper motion correction (except minor numerical effects)
        cat_pm = loader.loadSkyCircle(center,
                                      30.0 * lsst.geom.degrees,
                                      filterName='a',
                                      epoch=self.epoch).refCat

        self.assertFloatsAlmostEqual(cat_pm['coord_ra'],
                                     cat['coord_ra'],
                                     rtol=1.0e-14)
        self.assertFloatsAlmostEqual(cat_pm['coord_dec'],
                                     cat['coord_dec'],
                                     rtol=1.0e-14)
        self.assertFloatsEqual(cat_pm['coord_raErr'], cat['coord_raErr'])
        self.assertFloatsEqual(cat_pm['coord_decErr'], cat['coord_decErr'])

        # One year difference
        cat_pm = loader.loadSkyCircle(center,
                                      30.0 * lsst.geom.degrees,
                                      filterName='a',
                                      epoch=self.epoch +
                                      1.0 * astropy.units.yr).refCat

        self.assertFloatsEqual(cat_pm['pm_raErr'], cat['pm_raErr'])
        self.assertFloatsEqual(cat_pm['pm_decErr'], cat['pm_decErr'])
        for orig, ref in zip(cat, cat_pm):
            self.assertAnglesAlmostEqual(orig.getCoord().separation(
                ref.getCoord()),
                                         self.properMotionAmt,
                                         maxDiff=1.0e-6 * lsst.geom.arcseconds)
            self.assertAnglesAlmostEqual(orig.getCoord().bearingTo(
                ref.getCoord()),
                                         self.properMotionDir,
                                         maxDiff=1.0e-4 * lsst.geom.arcseconds)
        predictedRaErr = np.hypot(cat["coord_raErr"], cat["pm_raErr"])
        predictedDecErr = np.hypot(cat["coord_decErr"], cat["pm_decErr"])
        self.assertFloatsAlmostEqual(cat_pm["coord_raErr"], predictedRaErr)
        self.assertFloatsAlmostEqual(cat_pm["coord_decErr"], predictedDecErr)
Esempio n. 13
0
    def test_fgcmLoadReference(self):
        """
        Test loading of the fgcm reference catalogs.
        """

        filterList = ['HSC-R', 'HSC-I']

        config = fgcmcal.FgcmLoadReferenceCatalogConfig()
        config.applyColorTerms = True
        config.filterMap = {'HSC-R': 'r', 'HSC-I': 'i'}
        config.colorterms.data = {}
        config.colorterms.data[
            'ps1*'] = lsst.pipe.tasks.colorterms.ColortermDict()
        config.colorterms.data['ps1*'].data = {}
        config.colorterms.data['ps1*'].data[
            'HSC-R'] = lsst.pipe.tasks.colorterms.Colorterm()
        config.colorterms.data['ps1*'].data['HSC-R'].primary = 'r'
        config.colorterms.data['ps1*'].data['HSC-R'].secondary = 'i'
        config.colorterms.data['ps1*'].data['HSC-R'].c0 = -0.000144
        config.colorterms.data['ps1*'].data['HSC-R'].c1 = 0.001369
        config.colorterms.data['ps1*'].data['HSC-R'].c2 = -0.008380
        config.colorterms.data['ps1*'].data[
            'HSC-I'] = lsst.pipe.tasks.colorterms.Colorterm()
        config.colorterms.data['ps1*'].data['HSC-I'].primary = 'i'
        config.colorterms.data['ps1*'].data['HSC-I'].secondary = 'z'
        config.colorterms.data['ps1*'].data['HSC-I'].c0 = 0.000643
        config.colorterms.data['ps1*'].data['HSC-I'].c1 = -0.130078
        config.colorterms.data['ps1*'].data['HSC-I'].c2 = -0.006855

        refCatName = 'ps1_pv3_3pi_20170110'

        butler = lsst.daf.butler.Butler(
            self.repo,
            instrument='HSC',
            collections=['HSC/testdata', 'refcats/gen2'])
        refs = set(butler.registry.queryDatasets(refCatName))
        dataIds = [butler.registry.expandDataId(ref.dataId) for ref in refs]
        refCats = [butler.getDirectDeferred(ref) for ref in refs]

        refConfig = LoadReferenceObjectsConfig()
        refConfig.filterMap = config.filterMap

        refObjLoader = ReferenceObjectLoader(dataIds=dataIds,
                                             refCats=refCats,
                                             config=refConfig)

        loadCat = fgcmcal.FgcmLoadReferenceCatalogTask(
            refObjLoader=refObjLoader, refCatName=refCatName, config=config)

        ra = 337.656174
        dec = 0.823595
        rad = 0.1

        refCat = loadCat.getFgcmReferenceStarsSkyCircle(
            ra, dec, rad, filterList)

        # Check the number of mags and ranges
        self.assertEqual(len(filterList), refCat['refMag'].shape[1])
        self.assertEqual(len(filterList), refCat['refMagErr'].shape[1])
        self.assertLess(np.max(refCat['refMag'][:, 0]), 99.1)
        self.assertLess(np.max(refCat['refMag'][:, 1]), 99.1)
        self.assertLess(np.max(refCat['refMagErr'][:, 0]), 99.1)
        self.assertLess(np.max(refCat['refMagErr'][:, 1]), 99.1)
        test, = np.where((refCat['refMag'][:, 0] < 30.0)
                         & (refCat['refMag'][:, 1] < 30.0))
        self.assertGreater(test.size, 0)

        # Check the separations from the center
        self.assertLess(
            np.max(esutil.coords.sphdist(ra, dec, refCat['ra'],
                                         refCat['dec'])), rad)

        # And load a healpixel
        nside = 256
        pixel = 387520

        refCat = loadCat.getFgcmReferenceStarsHealpix(nside, pixel, filterList)

        ipring = hp.ang2pix(nside, np.radians(90.0 - refCat['dec']),
                            np.radians(refCat['ra']))
        self.assertEqual(pixel, np.max(ipring))
        self.assertEqual(pixel, np.min(ipring))
Esempio n. 14
0
class LoadReferenceCatalogTask(pipeBase.Task):
    """Load multi-band reference objects from a reference catalog.

    Parameters
    ----------
    dataIds : iterable of `lsst.daf.butler.dataId`, optional
        An iterable object of dataIds which point to reference catalogs
        in a Gen3 repository.  Required for Gen3.
    refCats : iterable of `lsst.daf.butler.DeferredDatasetHandle`, optional
        An iterable object of dataset refs for reference catalogs in
        a Gen3 repository.  Required for Gen3.
    butler : `lsst.daf.persistence.Butler`, optional
        A Gen2 butler.  Required for Gen2.
    """
    ConfigClass = LoadReferenceCatalogConfig
    _DefaultName = "loadReferenceCatalog"

    def __init__(self, dataIds=None, refCats=None, butler=None, **kwargs):
        if dataIds is not None and refCats is not None:
            pipeBase.Task.__init__(self, **kwargs)
            refConfig = self.config.refObjLoader
            self.refObjLoader = ReferenceObjectLoader(dataIds=dataIds,
                                                      refCats=refCats,
                                                      config=refConfig,
                                                      log=self.log)
        elif butler is not None:
            raise RuntimeError("LoadReferenceCatalogTask does not support Gen2 Butlers.")
        else:
            raise RuntimeError("Must instantiate LoadReferenceCatalogTask with "
                               "dataIds and refCats (Gen3)")

        if self.config.doReferenceSelection:
            self.makeSubtask('referenceSelector')
        self._fluxFilters = None
        self._fluxFields = None
        self._referenceFilter = None

    def getPixelBoxCatalog(self, bbox, wcs, filterList, epoch=None,
                           bboxToSpherePadding=None):
        """Get a multi-band reference catalog by specifying a bounding box and WCS.

        The catalog will be in `numpy.ndarray`, with positions proper-motion
        corrected to "epoch" (if specified, and if the reference catalog has
        proper motions); sources cut on a reference selector (if
        "config.doReferenceSelection = True"); and color-terms applied (if
        "config.doApplyColorTerms = True").

        The format of the reference catalog will be of the format:

        dtype = [('ra', 'np.float64'),
                 ('dec', 'np.float64'),
                 ('refMag', 'np.float32', (len(filterList), )),
                 ('refMagErr', 'np.float32', (len(filterList), ))]

        Reference magnitudes (AB) and errors will be 99 for non-detections
        in a given band.

        Parameters
        ----------
        bbox : `lsst.geom.Box2I`
            Box which bounds a region in pixel space.
        wcs : `lsst.afw.geom.SkyWcs`
            Wcs object defining the pixel to sky (and reverse) transform for
            the supplied bbox.
        filterList : `List` [ `str` ]
            List of camera physicalFilter names to retrieve magnitudes.
        epoch : `astropy.time.Time`, optional
            Epoch to which to correct proper motion and parallax
            (if available), or `None` to not apply such corrections.
        bboxToSpherePadding : `int`, optional
            Padding to account for translating a set of corners into a
            spherical (convex) boundary that is certain to encompass the
            entire area covered by the bbox.

        Returns
        -------
        refCat : `numpy.ndarray`
            Reference catalog.
        """
        # Check if we have previously cached values for the fluxFields
        if self._fluxFilters is None or self._fluxFilters != filterList:
            center = wcs.pixelToSky(bbox.getCenter())
            self._determineFluxFields(center, filterList)

        skyBox = self.refObjLoader.loadPixelBox(bbox, wcs, self._referenceFilter,
                                                epoch=epoch,
                                                bboxToSpherePadding=bboxToSpherePadding)

        if not skyBox.refCat.isContiguous():
            refCat = skyBox.refCat.copy(deep=True)
        else:
            refCat = skyBox.refCat

        return self._formatCatalog(refCat, filterList)

    def getSkyCircleCatalog(self, center, radius, filterList, epoch=None,
                            catalogFormat='numpy'):
        """Get a multi-band reference catalog by specifying a center and radius.

        The catalog will be in `numpy.ndarray`, with positions proper-motion
        corrected to "epoch" (if specified, and if the reference catalog has
        proper motions); sources cut on a reference selector (if
        "config.doReferenceSelection = True"); and color-terms applied (if
        "config.doApplyColorTerms = True").

        The format of the reference catalog will be of the format:

        dtype = [('ra', 'np.float64'),
                 ('dec', 'np.float64'),
                 ('refMag', 'np.float32', (len(filterList), )),
                 ('refMagErr', 'np.float32', (len(filterList), ))]

        Reference magnitudes (AB) and errors will be 99 for non-detections
        in a given band.

        Parameters
        ----------
        center : `lsst.geom.SpherePoint`
            Point defining the center of the circular region.
        radius : `lsst.geom.Angle`
            Defines the angular radius of the circular region.
        filterList : `List` [ `str` ]
            List of camera physicalFilter names to retrieve magnitudes.
        epoch : `astropy.time.Time`, optional
            Epoch to which to correct proper motion and parallax
            (if available), or `None` to not apply such corrections.

        Parameters
        ----------
        refCat : `numpy.ndarray`
            Reference catalog.
        """
        # Check if we have previously cached values for the fluxFields
        if self._fluxFilters is None or self._fluxFilters != filterList:
            self._determineFluxFields(center, filterList)

        skyCircle = self.refObjLoader.loadSkyCircle(center, radius,
                                                    self._referenceFilter,
                                                    epoch=epoch)

        if not skyCircle.refCat.isContiguous():
            refCat = skyCircle.refCat.copy(deep=True)
        else:
            refCat = skyCircle.refCat

        return self._formatCatalog(refCat, filterList)

    def _formatCatalog(self, refCat, filterList):
        """Format a reference afw table into the final format.

        This method applies reference selections and color terms as specified
        by the config.

        Parameters
        ----------
        refCat : `lsst.afw.table.SourceCatalog`
            Reference catalog in afw format.
        filterList : `list` [`str`]
            List of camera physicalFilter names to apply color terms.

        Returns
        -------
        refCat : `numpy.ndarray`
            Reference catalog.
        """
        if self.config.doReferenceSelection:
            goodSources = self.referenceSelector.selectSources(refCat)
            selected = goodSources.selected
        else:
            selected = np.ones(len(refCat), dtype=bool)

        npRefCat = np.zeros(np.sum(selected), dtype=[('ra', 'f8'),
                                                     ('dec', 'f8'),
                                                     ('refMag', 'f4', (len(filterList), )),
                                                     ('refMagErr', 'f4', (len(filterList), ))])

        if npRefCat.size == 0:
            # Return an empty catalog if we don't have any selected sources.
            return npRefCat

        # Natively "coord_ra" and "coord_dec" are stored in radians.
        # Doing this as an array rather than by row with the coord access is
        # approximately 600x faster.
        npRefCat['ra'] = np.rad2deg(refCat['coord_ra'][selected])
        npRefCat['dec'] = np.rad2deg(refCat['coord_dec'][selected])

        # Default (unset) values are 99.0
        npRefCat['refMag'][:, :] = 99.0
        npRefCat['refMagErr'][:, :] = 99.0

        if self.config.doApplyColorTerms:
            if isinstance(self.refObjLoader, ReferenceObjectLoader):
                # Gen3
                refCatName = self.refObjLoader.config.value.ref_dataset_name
            else:
                # Gen2
                refCatName = self.refObjLoader.ref_dataset_name

            for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters, self._fluxFields)):
                if fluxField is None:
                    # There is no matching reference band.
                    # This will leave the column filled with 99s
                    continue
                self.log.debug("Applying color terms for filterName='%s'", filterName)

                colorterm = self.config.colorterms.getColorterm(filterName, refCatName, doRaise=True)

                refMag, refMagErr = colorterm.getCorrectedMagnitudes(refCat)

                # nan_to_num replaces nans with zeros, and this ensures
                # that we select magnitudes that both filter out nans and are
                # not very large (corresponding to very small fluxes), as "99"
                # is a commen sentinel for illegal magnitudes.
                good, = np.where((np.nan_to_num(refMag[selected], nan=99.0) < 90.0)
                                 & (np.nan_to_num(refMagErr[selected], nan=99.0) < 90.0)
                                 & (np.nan_to_num(refMagErr[selected]) > 0.0))

                npRefCat['refMag'][good, i] = refMag[selected][good]
                npRefCat['refMagErr'][good, i] = refMagErr[selected][good]
        else:
            # No color terms to apply
            for i, (filterName, fluxField) in enumerate(zip(self._fluxFilters, self._fluxFields)):
                # nan_to_num replaces nans with zeros, and this ensures that
                # we select fluxes that both filter out nans and are positive.
                good, = np.where((np.nan_to_num(refCat[fluxField][selected]) > 0.0)
                                 & (np.nan_to_num(refCat[fluxField+'Err'][selected]) > 0.0))
                refMag = (refCat[fluxField][selected][good]*units.nJy).to_value(units.ABmag)
                refMagErr = abMagErrFromFluxErr(refCat[fluxField+'Err'][selected][good],
                                                refCat[fluxField][selected][good])
                npRefCat['refMag'][good, i] = refMag
                npRefCat['refMagErr'][good, i] = refMagErr

        return npRefCat

    def _determineFluxFields(self, center, filterList):
        """Determine the flux field names for a reference catalog.

        This method sets self._fluxFields, self._referenceFilter.

        Parameters
        ----------
        center : `lsst.geom.SpherePoint`
            The center around which to load test sources.
        filterList : `list` [`str`]
            List of camera physicalFilter names.
        """
        # Search for a good filter to use to load the reference catalog
        # via the refObjLoader task which requires a valid filterName
        foundReferenceFilter = False

        # Store the original config
        _config = self.refObjLoader.config

        configTemp = LoadReferenceObjectsConfig()
        configIntersection = {k: getattr(self.refObjLoader.config, k)
                              for k, v in self.refObjLoader.config.toDict().items()
                              if (k in configTemp.keys() and k != "connections")}
        # We must turn off the proper motion checking to find the refFilter.
        configIntersection['requireProperMotion'] = False
        configTemp.update(**configIntersection)

        self.refObjLoader.config = configTemp

        for filterName in filterList:
            if self.config.refObjLoader.anyFilterMapsToThis is not None:
                refFilterName = self.config.refObjLoader.anyFilterMapsToThis
            else:
                refFilterName = self.config.refObjLoader.filterMap.get(filterName)
            if refFilterName is None:
                continue
            try:
                results = self.refObjLoader.loadSkyCircle(center,
                                                          0.05*lsst.geom.degrees,
                                                          refFilterName)
                foundReferenceFilter = True
                self._referenceFilter = refFilterName
                break
            except RuntimeError as err:
                # This just means that the filterName wasn't listed
                # in the reference catalog.  This is okay.
                if 'not find flux' in err.args[0]:
                    # The filterName wasn't listed in the reference catalog.
                    # This is not a fatal failure (yet)
                    pass
                else:
                    raise err

        self.refObjLoader.config = _config

        if not foundReferenceFilter:
            raise RuntimeError("Could not find any valid flux field(s) %s" %
                               (", ".join(filterList)))

        # Record self._fluxFilters for checks on subsequent calls
        self._fluxFilters = filterList

        # Retrieve all the fluxField names
        self._fluxFields = []
        for filterName in filterList:
            fluxField = None

            if self.config.refObjLoader.anyFilterMapsToThis is not None:
                refFilterName = self.config.refObjLoader.anyFilterMapsToThis
            else:
                refFilterName = self.config.refObjLoader.filterMap.get(filterName)

            if refFilterName is not None:
                try:
                    fluxField = getRefFluxField(results.refCat.schema, filterName=refFilterName)
                except RuntimeError:
                    # This flux field isn't available.  Set to None
                    fluxField = None

            if fluxField is None:
                self.log.warning('No reference flux field for camera filter %s', filterName)

            self._fluxFields.append(fluxField)
Esempio n. 15
0
    def runQuantum(self, butlerQC, inputRefs, outputRefs):
        inputRefDict = butlerQC.get(inputRefs)

        sourceTableHandles = inputRefDict['sourceTable_visit']

        self.log.info("Running with %d sourceTable_visit handles",
                      len(sourceTableHandles))

        sourceTableHandleDict = {
            sourceTableHandle.dataId['visit']: sourceTableHandle
            for sourceTableHandle in sourceTableHandles
        }

        if self.config.doReferenceMatches:
            # Get the LUT handle
            lutHandle = inputRefDict['fgcmLookUpTable']

            # Prepare the reference catalog loader
            refConfig = LoadReferenceObjectsConfig()
            refConfig.filterMap = self.config.fgcmLoadReferenceCatalog.filterMap
            refObjLoader = ReferenceObjectLoader(
                dataIds=[ref.datasetRef.dataId for ref in inputRefs.refCat],
                refCats=butlerQC.get(inputRefs.refCat),
                log=self.log,
                config=refConfig)
            self.makeSubtask('fgcmLoadReferenceCatalog',
                             refObjLoader=refObjLoader,
                             refCatName=self.config.connections.refCat)
        else:
            lutHandle = None

        # Compute aperture radius if necessary.  This is useful to do now before
        # any heave lifting has happened (fail early).
        calibFluxApertureRadius = None
        if self.config.doSubtractLocalBackground:
            try:
                calibFluxApertureRadius = computeApertureRadiusFromName(
                    self.config.instFluxField)
            except RuntimeError as e:
                raise RuntimeError(
                    "Could not determine aperture radius from %s. "
                    "Cannot use doSubtractLocalBackground." %
                    (self.config.instFluxField)) from e

        visitSummaryHandles = inputRefDict['visitSummary']
        visitSummaryHandleDict = {
            visitSummaryHandle.dataId['visit']: visitSummaryHandle
            for visitSummaryHandle in visitSummaryHandles
        }

        camera = inputRefDict['camera']
        groupedHandles = self._groupHandles(sourceTableHandleDict,
                                            visitSummaryHandleDict)

        if self.config.doModelErrorsWithBackground:
            bkgHandles = inputRefDict['background']
            bkgHandleDict = {(bkgHandle.dataId.byName()['visit'],
                              bkgHandle.dataId.byName()['detector']): bkgHandle
                             for bkgHandle in bkgHandles}
        else:
            bkgHandleDict = None

        visitCat = self.fgcmMakeVisitCatalog(camera,
                                             groupedHandles,
                                             bkgHandleDict=bkgHandleDict)

        rad = calibFluxApertureRadius
        fgcmStarObservationCat = self.fgcmMakeAllStarObservations(
            groupedHandles,
            visitCat,
            self.sourceSchema,
            camera,
            calibFluxApertureRadius=rad)

        butlerQC.put(visitCat, outputRefs.fgcmVisitCatalog)
        butlerQC.put(fgcmStarObservationCat, outputRefs.fgcmStarObservations)

        fgcmStarIdCat, fgcmStarIndicesCat, fgcmRefCat = self.fgcmMatchStars(
            visitCat, fgcmStarObservationCat, lutHandle=lutHandle)

        butlerQC.put(fgcmStarIdCat, outputRefs.fgcmStarIds)
        butlerQC.put(fgcmStarIndicesCat, outputRefs.fgcmStarIndices)
        if fgcmRefCat is not None:
            butlerQC.put(fgcmRefCat, outputRefs.fgcmReferenceStars)
Esempio n. 16
0
    def runQuantum(self, butlerQC, inputRefs, outputRefs):
        handleDict = {}
        handleDict['camera'] = butlerQC.get(inputRefs.camera)
        handleDict['fgcmLookUpTable'] = butlerQC.get(inputRefs.fgcmLookUpTable)
        handleDict['fgcmVisitCatalog'] = butlerQC.get(
            inputRefs.fgcmVisitCatalog)
        handleDict['fgcmStandardStars'] = butlerQC.get(
            inputRefs.fgcmStandardStars)

        if self.config.doZeropointOutput:
            handleDict['fgcmZeropoints'] = butlerQC.get(
                inputRefs.fgcmZeropoints)
            photoCalibRefDict = {
                photoCalibRef.dataId.byName()['visit']: photoCalibRef
                for photoCalibRef in outputRefs.fgcmPhotoCalib
            }

        if self.config.doAtmosphereOutput:
            handleDict['fgcmAtmosphereParameters'] = butlerQC.get(
                inputRefs.fgcmAtmosphereParameters)
            atmRefDict = {
                atmRef.dataId.byName()['visit']: atmRef
                for atmRef in outputRefs.fgcmTransmissionAtmosphere
            }

        if self.config.doReferenceCalibration:
            refConfig = LoadReferenceObjectsConfig()
            self.refObjLoader = ReferenceObjectLoader(
                dataIds=[ref.datasetRef.dataId for ref in inputRefs.refCat],
                refCats=butlerQC.get(inputRefs.refCat),
                log=self.log,
                config=refConfig)
        else:
            self.refObjLoader = None

        struct = self.run(handleDict, self.config.physicalFilterMap)

        # Output the photoCalib exposure catalogs
        if struct.photoCalibCatalogs is not None:
            self.log.info("Outputting photoCalib catalogs.")
            for visit, expCatalog in struct.photoCalibCatalogs:
                butlerQC.put(expCatalog, photoCalibRefDict[visit])
            self.log.info("Done outputting photoCalib catalogs.")

        # Output the atmospheres
        if struct.atmospheres is not None:
            self.log.info("Outputting atmosphere transmission files.")
            for visit, atm in struct.atmospheres:
                butlerQC.put(atm, atmRefDict[visit])
            self.log.info("Done outputting atmosphere files.")

        if self.config.doReferenceCalibration:
            # Turn offset into simple catalog for persistence if necessary
            schema = afwTable.Schema()
            schema.addField('offset',
                            type=np.float64,
                            doc="Post-process calibration offset (mag)")
            offsetCat = afwTable.BaseCatalog(schema)
            offsetCat.resize(len(struct.offsets))
            offsetCat['offset'][:] = struct.offsets

            butlerQC.put(offsetCat, outputRefs.fgcmOffsets)

        return
Esempio n. 17
0
class FgcmOutputProductsTask(pipeBase.PipelineTask):
    """
    Output products from FGCM global calibration.
    """

    ConfigClass = FgcmOutputProductsConfig
    _DefaultName = "fgcmOutputProducts"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def runQuantum(self, butlerQC, inputRefs, outputRefs):
        handleDict = {}
        handleDict['camera'] = butlerQC.get(inputRefs.camera)
        handleDict['fgcmLookUpTable'] = butlerQC.get(inputRefs.fgcmLookUpTable)
        handleDict['fgcmVisitCatalog'] = butlerQC.get(
            inputRefs.fgcmVisitCatalog)
        handleDict['fgcmStandardStars'] = butlerQC.get(
            inputRefs.fgcmStandardStars)

        if self.config.doZeropointOutput:
            handleDict['fgcmZeropoints'] = butlerQC.get(
                inputRefs.fgcmZeropoints)
            photoCalibRefDict = {
                photoCalibRef.dataId.byName()['visit']: photoCalibRef
                for photoCalibRef in outputRefs.fgcmPhotoCalib
            }

        if self.config.doAtmosphereOutput:
            handleDict['fgcmAtmosphereParameters'] = butlerQC.get(
                inputRefs.fgcmAtmosphereParameters)
            atmRefDict = {
                atmRef.dataId.byName()['visit']: atmRef
                for atmRef in outputRefs.fgcmTransmissionAtmosphere
            }

        if self.config.doReferenceCalibration:
            refConfig = LoadReferenceObjectsConfig()
            self.refObjLoader = ReferenceObjectLoader(
                dataIds=[ref.datasetRef.dataId for ref in inputRefs.refCat],
                refCats=butlerQC.get(inputRefs.refCat),
                log=self.log,
                config=refConfig)
        else:
            self.refObjLoader = None

        struct = self.run(handleDict, self.config.physicalFilterMap)

        # Output the photoCalib exposure catalogs
        if struct.photoCalibCatalogs is not None:
            self.log.info("Outputting photoCalib catalogs.")
            for visit, expCatalog in struct.photoCalibCatalogs:
                butlerQC.put(expCatalog, photoCalibRefDict[visit])
            self.log.info("Done outputting photoCalib catalogs.")

        # Output the atmospheres
        if struct.atmospheres is not None:
            self.log.info("Outputting atmosphere transmission files.")
            for visit, atm in struct.atmospheres:
                butlerQC.put(atm, atmRefDict[visit])
            self.log.info("Done outputting atmosphere files.")

        if self.config.doReferenceCalibration:
            # Turn offset into simple catalog for persistence if necessary
            schema = afwTable.Schema()
            schema.addField('offset',
                            type=np.float64,
                            doc="Post-process calibration offset (mag)")
            offsetCat = afwTable.BaseCatalog(schema)
            offsetCat.resize(len(struct.offsets))
            offsetCat['offset'][:] = struct.offsets

            butlerQC.put(offsetCat, outputRefs.fgcmOffsets)

        return

    def run(self, handleDict, physicalFilterMap):
        """Run the output products task.

        Parameters
        ----------
        handleDict : `dict`
            All handles are `lsst.daf.butler.DeferredDatasetHandle`
            handle dictionary with keys:

            ``"camera"``
                Camera object (`lsst.afw.cameraGeom.Camera`)
            ``"fgcmLookUpTable"``
                handle for the FGCM look-up table.
            ``"fgcmVisitCatalog"``
                handle for visit summary catalog.
            ``"fgcmStandardStars"``
                handle for the output standard star catalog.
            ``"fgcmZeropoints"``
                handle for the zeropoint data catalog.
            ``"fgcmAtmosphereParameters"``
                handle for the atmosphere parameter catalog.
            ``"fgcmBuildStarsTableConfig"``
                Config for `lsst.fgcmcal.fgcmBuildStarsTableTask`.
        physicalFilterMap : `dict`
            Dictionary of mappings from physical filter to FGCM band.

        Returns
        -------
        retStruct : `lsst.pipe.base.Struct`
            Output structure with keys:

            offsets : `np.ndarray`
                Final reference offsets, per band.
            atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
                Generator that returns (visit, transmissionCurve) tuples.
            photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
                Generator that returns (visit, exposureCatalog) tuples.
        """
        stdCat = handleDict['fgcmStandardStars'].get()
        md = stdCat.getMetadata()
        bands = md.getArray('BANDS')

        if self.config.doReferenceCalibration:
            lutCat = handleDict['fgcmLookUpTable'].get()
            offsets = self._computeReferenceOffsets(stdCat, lutCat,
                                                    physicalFilterMap, bands)
        else:
            offsets = np.zeros(len(bands))

        del stdCat

        if self.config.doZeropointOutput:
            zptCat = handleDict['fgcmZeropoints'].get()
            visitCat = handleDict['fgcmVisitCatalog'].get()

            pcgen = self._outputZeropoints(handleDict['camera'], zptCat,
                                           visitCat, offsets, bands,
                                           physicalFilterMap)
        else:
            pcgen = None

        if self.config.doAtmosphereOutput:
            atmCat = handleDict['fgcmAtmosphereParameters'].get()
            atmgen = self._outputAtmospheres(handleDict, atmCat)
        else:
            atmgen = None

        retStruct = pipeBase.Struct(offsets=offsets, atmospheres=atmgen)
        retStruct.photoCalibCatalogs = pcgen

        return retStruct

    def generateTractOutputProducts(self, handleDict, tract, visitCat, zptCat,
                                    atmCat, stdCat, fgcmBuildStarsConfig):
        """
        Generate the output products for a given tract, as specified in the config.

        This method is here to have an alternate entry-point for
        FgcmCalibrateTract.

        Parameters
        ----------
        handleDict : `dict`
            All handles are `lsst.daf.butler.DeferredDatasetHandle`
            handle dictionary with keys:

            ``"camera"``
                Camera object (`lsst.afw.cameraGeom.Camera`)
            ``"fgcmLookUpTable"``
                handle for the FGCM look-up table.
        tract : `int`
            Tract number
        visitCat : `lsst.afw.table.BaseCatalog`
            FGCM visitCat from `FgcmBuildStarsTask`
        zptCat : `lsst.afw.table.BaseCatalog`
            FGCM zeropoint catalog from `FgcmFitCycleTask`
        atmCat : `lsst.afw.table.BaseCatalog`
            FGCM atmosphere parameter catalog from `FgcmFitCycleTask`
        stdCat : `lsst.afw.table.SimpleCatalog`
            FGCM standard star catalog from `FgcmFitCycleTask`
        fgcmBuildStarsConfig : `lsst.fgcmcal.FgcmBuildStarsConfig`
            Configuration object from `FgcmBuildStarsTask`

        Returns
        -------
        retStruct : `lsst.pipe.base.Struct`
            Output structure with keys:

            offsets : `np.ndarray`
                Final reference offsets, per band.
            atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
                Generator that returns (visit, transmissionCurve) tuples.
            photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
                Generator that returns (visit, exposureCatalog) tuples.
        """
        physicalFilterMap = fgcmBuildStarsConfig.physicalFilterMap

        md = stdCat.getMetadata()
        bands = md.getArray('BANDS')

        if self.config.doComposeWcsJacobian and not fgcmBuildStarsConfig.doApplyWcsJacobian:
            raise RuntimeError(
                "Cannot compose the WCS jacobian if it hasn't been applied "
                "in fgcmBuildStarsTask.")

        if not self.config.doComposeWcsJacobian and fgcmBuildStarsConfig.doApplyWcsJacobian:
            self.log.warning(
                "Jacobian was applied in build-stars but doComposeWcsJacobian is not set."
            )

        if self.config.doReferenceCalibration:
            lutCat = handleDict['fgcmLookUpTable'].get()
            offsets = self._computeReferenceOffsets(stdCat, lutCat, bands,
                                                    physicalFilterMap)
        else:
            offsets = np.zeros(len(bands))

        if self.config.doZeropointOutput:
            pcgen = self._outputZeropoints(handleDict['camera'], zptCat,
                                           visitCat, offsets, bands,
                                           physicalFilterMap)
        else:
            pcgen = None

        if self.config.doAtmosphereOutput:
            atmgen = self._outputAtmospheres(handleDict, atmCat)
        else:
            atmgen = None

        retStruct = pipeBase.Struct(offsets=offsets, atmospheres=atmgen)
        retStruct.photoCalibCatalogs = pcgen

        return retStruct

    def _computeReferenceOffsets(self, stdCat, lutCat, physicalFilterMap,
                                 bands):
        """
        Compute offsets relative to a reference catalog.

        This method splits the star catalog into healpix pixels
        and computes the calibration transfer for a sample of
        these pixels to approximate the 'absolute' calibration
        values (on for each band) to apply to transfer the
        absolute scale.

        Parameters
        ----------
        stdCat : `lsst.afw.table.SimpleCatalog`
            FGCM standard stars
        lutCat : `lsst.afw.table.SimpleCatalog`
            FGCM Look-up table
        physicalFilterMap : `dict`
            Dictionary of mappings from physical filter to FGCM band.
        bands : `list` [`str`]
            List of band names from FGCM output
        Returns
        -------
        offsets : `numpy.array` of floats
            Per band zeropoint offsets
        """

        # Only use stars that are observed in all the bands that were actually used
        # This will ensure that we use the same healpix pixels for the absolute
        # calibration of each band.
        minObs = stdCat['ngood'].min(axis=1)

        goodStars = (minObs >= 1)
        stdCat = stdCat[goodStars]

        self.log.info(
            "Found %d stars with at least 1 good observation in each band" %
            (len(stdCat)))

        # Associate each band with the appropriate physicalFilter and make
        # filterLabels
        filterLabels = []

        lutPhysicalFilters = lutCat[0]['physicalFilters'].split(',')
        lutStdPhysicalFilters = lutCat[0]['stdPhysicalFilters'].split(',')
        physicalFilterMapBands = list(physicalFilterMap.values())
        physicalFilterMapFilters = list(physicalFilterMap.keys())
        for band in bands:
            # Find a physical filter associated from the band by doing
            # a reverse lookup on the physicalFilterMap dict
            physicalFilterMapIndex = physicalFilterMapBands.index(band)
            physicalFilter = physicalFilterMapFilters[physicalFilterMapIndex]
            # Find the appropriate fgcm standard physicalFilter
            lutPhysicalFilterIndex = lutPhysicalFilters.index(physicalFilter)
            stdPhysicalFilter = lutStdPhysicalFilters[lutPhysicalFilterIndex]
            filterLabels.append(
                afwImage.FilterLabel(band=band, physical=stdPhysicalFilter))

        # We have to make a table for each pixel with flux/fluxErr
        # This is a temporary table generated for input to the photoCal task.
        # These fluxes are not instFlux (they are top-of-the-atmosphere approximate and
        # have had chromatic corrections applied to get to the standard system
        # specified by the atmosphere/instrumental parameters), nor are they
        # in Jansky (since they don't have a proper absolute calibration: the overall
        # zeropoint is estimated from the telescope size, etc.)
        sourceMapper = afwTable.SchemaMapper(stdCat.schema)
        sourceMapper.addMinimalSchema(afwTable.SimpleTable.makeMinimalSchema())
        sourceMapper.editOutputSchema().addField(
            'instFlux', type=np.float64, doc="instrumental flux (counts)")
        sourceMapper.editOutputSchema().addField(
            'instFluxErr',
            type=np.float64,
            doc="instrumental flux error (counts)")
        badStarKey = sourceMapper.editOutputSchema().addField('flag_badStar',
                                                              type='Flag',
                                                              doc="bad flag")

        # Split up the stars
        # Note that there is an assumption here that the ra/dec coords stored
        # on-disk are in radians, and therefore that starObs['coord_ra'] /
        # starObs['coord_dec'] return radians when used as an array of numpy float64s.
        theta = np.pi / 2. - stdCat['coord_dec']
        phi = stdCat['coord_ra']

        ipring = hp.ang2pix(self.config.referencePixelizationNside, theta, phi)
        h, rev = esutil.stat.histogram(ipring, rev=True)

        gdpix, = np.where(h >= self.config.referencePixelizationMinStars)

        self.log.info(
            "Found %d pixels (nside=%d) with at least %d good stars" %
            (gdpix.size, self.config.referencePixelizationNside,
             self.config.referencePixelizationMinStars))

        if gdpix.size < self.config.referencePixelizationNPixels:
            self.log.warning(
                "Found fewer good pixels (%d) than preferred in configuration (%d)"
                % (gdpix.size, self.config.referencePixelizationNPixels))
        else:
            # Sample out the pixels we want to use
            gdpix = np.random.choice(
                gdpix,
                size=self.config.referencePixelizationNPixels,
                replace=False)

        results = np.zeros(gdpix.size,
                           dtype=[('hpix', 'i4'), ('nstar', 'i4', len(bands)),
                                  ('nmatch', 'i4', len(bands)),
                                  ('zp', 'f4', len(bands)),
                                  ('zpErr', 'f4', len(bands))])
        results['hpix'] = ipring[rev[rev[gdpix]]]

        # We need a boolean index to deal with catalogs...
        selected = np.zeros(len(stdCat), dtype=bool)

        refFluxFields = [None] * len(bands)

        for p_index, pix in enumerate(gdpix):
            i1a = rev[rev[pix]:rev[pix + 1]]

            # the stdCat afwTable can only be indexed with boolean arrays,
            # and not numpy index arrays (see DM-16497).  This little trick
            # converts the index array into a boolean array
            selected[:] = False
            selected[i1a] = True

            for b_index, filterLabel in enumerate(filterLabels):
                struct = self._computeOffsetOneBand(sourceMapper, badStarKey,
                                                    b_index, filterLabel,
                                                    stdCat, selected,
                                                    refFluxFields)
                results['nstar'][p_index, b_index] = len(i1a)
                results['nmatch'][p_index, b_index] = len(struct.arrays.refMag)
                results['zp'][p_index, b_index] = struct.zp
                results['zpErr'][p_index, b_index] = struct.sigma

        # And compute the summary statistics
        offsets = np.zeros(len(bands))

        for b_index, band in enumerate(bands):
            # make configurable
            ok, = np.where(
                results['nmatch'][:, b_index] >= self.config.referenceMinMatch)
            offsets[b_index] = np.median(results['zp'][ok, b_index])
            # use median absolute deviation to estimate Normal sigma
            # see https://en.wikipedia.org/wiki/Median_absolute_deviation
            madSigma = 1.4826 * np.median(
                np.abs(results['zp'][ok, b_index] - offsets[b_index]))
            self.log.info(
                "Reference catalog offset for %s band: %.12f +/- %.12f", band,
                offsets[b_index], madSigma)

        return offsets

    def _computeOffsetOneBand(self, sourceMapper, badStarKey, b_index,
                              filterLabel, stdCat, selected, refFluxFields):
        """
        Compute the zeropoint offset between the fgcm stdCat and the reference
        stars for one pixel in one band

        Parameters
        ----------
        sourceMapper : `lsst.afw.table.SchemaMapper`
            Mapper to go from stdCat to calibratable catalog
        badStarKey : `lsst.afw.table.Key`
            Key for the field with bad stars
        b_index : `int`
            Index of the band in the star catalog
        filterLabel : `lsst.afw.image.FilterLabel`
            filterLabel with band and physical filter
        stdCat : `lsst.afw.table.SimpleCatalog`
            FGCM standard stars
        selected : `numpy.array(dtype=bool)`
            Boolean array of which stars are in the pixel
        refFluxFields : `list`
            List of names of flux fields for reference catalog
        """

        sourceCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
        sourceCat.reserve(selected.sum())
        sourceCat.extend(stdCat[selected], mapper=sourceMapper)
        sourceCat['instFlux'] = 10.**(
            stdCat['mag_std_noabs'][selected, b_index] / (-2.5))
        sourceCat['instFluxErr'] = (np.log(10.) / 2.5) * (
            stdCat['magErr_std'][selected, b_index] * sourceCat['instFlux'])
        # Make sure we only use stars that have valid measurements
        # (This is perhaps redundant with requirements above that the
        # stars be observed in all bands, but it can't hurt)
        badStar = (stdCat['mag_std_noabs'][selected, b_index] > 90.0)
        for rec in sourceCat[badStar]:
            rec.set(badStarKey, True)

        exposure = afwImage.ExposureF()
        exposure.setFilterLabel(filterLabel)

        if refFluxFields[b_index] is None:
            # Need to find the flux field in the reference catalog
            # to work around limitations of DirectMatch in PhotoCal
            ctr = stdCat[0].getCoord()
            rad = 0.05 * lsst.geom.degrees
            refDataTest = self.refObjLoader.loadSkyCircle(
                ctr, rad, filterLabel.bandLabel)
            refFluxFields[b_index] = refDataTest.fluxField

        # Make a copy of the config so that we can modify it
        calConfig = copy.copy(self.config.photoCal.value)
        calConfig.match.referenceSelection.signalToNoise.fluxField = refFluxFields[
            b_index]
        calConfig.match.referenceSelection.signalToNoise.errField = refFluxFields[
            b_index] + 'Err'
        calTask = self.config.photoCal.target(refObjLoader=self.refObjLoader,
                                              config=calConfig,
                                              schema=sourceCat.getSchema())

        struct = calTask.run(exposure, sourceCat)

        return struct

    def _formatCatalog(self, fgcmStarCat, offsets, bands):
        """
        Turn an FGCM-formatted star catalog, applying zeropoint offsets.

        Parameters
        ----------
        fgcmStarCat : `lsst.afw.Table.SimpleCatalog`
            SimpleCatalog as output by fgcmcal
        offsets : `list` with len(self.bands) entries
            Zeropoint offsets to apply
        bands : `list` [`str`]
            List of band names from FGCM output

        Returns
        -------
        formattedCat: `lsst.afw.table.SimpleCatalog`
           SimpleCatalog suitable for using as a reference catalog
        """

        sourceMapper = afwTable.SchemaMapper(fgcmStarCat.schema)
        minSchema = LoadIndexedReferenceObjectsTask.makeMinimalSchema(
            bands, addCentroid=False, addIsResolved=True, coordErrDim=0)
        sourceMapper.addMinimalSchema(minSchema)
        for band in bands:
            sourceMapper.editOutputSchema().addField('%s_nGood' % (band),
                                                     type=np.int32)
            sourceMapper.editOutputSchema().addField('%s_nTotal' % (band),
                                                     type=np.int32)
            sourceMapper.editOutputSchema().addField('%s_nPsfCandidate' %
                                                     (band),
                                                     type=np.int32)

        formattedCat = afwTable.SimpleCatalog(sourceMapper.getOutputSchema())
        formattedCat.reserve(len(fgcmStarCat))
        formattedCat.extend(fgcmStarCat, mapper=sourceMapper)

        # Note that we don't have to set `resolved` because the default is False

        for b, band in enumerate(bands):
            mag = fgcmStarCat['mag_std_noabs'][:, b].astype(
                np.float64) + offsets[b]
            # We want fluxes in nJy from calibrated AB magnitudes
            # (after applying offset).  Updated after RFC-549 and RFC-575.
            flux = (mag * units.ABmag).to_value(units.nJy)
            fluxErr = (np.log(10.) /
                       2.5) * flux * fgcmStarCat['magErr_std'][:, b].astype(
                           np.float64)

            formattedCat['%s_flux' % (band)][:] = flux
            formattedCat['%s_fluxErr' % (band)][:] = fluxErr
            formattedCat['%s_nGood' % (band)][:] = fgcmStarCat['ngood'][:, b]
            formattedCat['%s_nTotal' % (band)][:] = fgcmStarCat['ntotal'][:, b]
            formattedCat['%s_nPsfCandidate' %
                         (band)][:] = fgcmStarCat['npsfcand'][:, b]

        addRefCatMetadata(formattedCat)

        return formattedCat

    def _outputZeropoints(self,
                          camera,
                          zptCat,
                          visitCat,
                          offsets,
                          bands,
                          physicalFilterMap,
                          tract=None):
        """Output the zeropoints in fgcm_photoCalib format.

        Parameters
        ----------
        camera : `lsst.afw.cameraGeom.Camera`
            Camera from the butler.
        zptCat : `lsst.afw.table.BaseCatalog`
            FGCM zeropoint catalog from `FgcmFitCycleTask`.
        visitCat : `lsst.afw.table.BaseCatalog`
            FGCM visitCat from `FgcmBuildStarsTask`.
        offsets : `numpy.array`
            Float array of absolute calibration offsets, one for each filter.
        bands : `list` [`str`]
            List of band names from FGCM output.
        physicalFilterMap : `dict`
            Dictionary of mappings from physical filter to FGCM band.
        tract: `int`, optional
            Tract number to output.  Default is None (global calibration)

        Returns
        -------
        photoCalibCatalogs : `generator` [(`int`, `lsst.afw.table.ExposureCatalog`)]
            Generator that returns (visit, exposureCatalog) tuples.
        """
        # Select visit/ccds where we have a calibration
        # This includes ccds where we were able to interpolate from neighboring
        # ccds.
        cannot_compute = fgcm.fgcmUtilities.zpFlagDict[
            'CANNOT_COMPUTE_ZEROPOINT']
        selected = (((zptCat['fgcmFlag'] & cannot_compute) == 0)
                    & (zptCat['fgcmZptVar'] > 0.0)
                    & (zptCat['fgcmZpt'] > FGCM_ILLEGAL_VALUE))

        # Log warnings for any visit which has no valid zeropoints
        badVisits = np.unique(zptCat['visit'][~selected])
        goodVisits = np.unique(zptCat['visit'][selected])
        allBadVisits = badVisits[~np.isin(badVisits, goodVisits)]
        for allBadVisit in allBadVisits:
            self.log.warning(f'No suitable photoCalib for visit {allBadVisit}')

        # Get a mapping from filtername to the offsets
        offsetMapping = {}
        for f in physicalFilterMap:
            # Not every filter in the map will necesarily have a band.
            if physicalFilterMap[f] in bands:
                offsetMapping[f] = offsets[bands.index(physicalFilterMap[f])]

        # Get a mapping from "ccd" to the ccd index used for the scaling
        ccdMapping = {}
        for ccdIndex, detector in enumerate(camera):
            ccdMapping[detector.getId()] = ccdIndex

        # And a mapping to get the flat-field scaling values
        scalingMapping = {}
        for rec in visitCat:
            scalingMapping[rec['visit']] = rec['scaling']

        if self.config.doComposeWcsJacobian:
            approxPixelAreaFields = computeApproxPixelAreaFields(camera)

        # The zptCat is sorted by visit, which is useful
        lastVisit = -1
        zptVisitCatalog = None

        metadata = dafBase.PropertyList()
        metadata.add("COMMENT", "Catalog id is detector id, sorted.")
        metadata.add("COMMENT", "Only detectors with data have entries.")

        for rec in zptCat[selected]:
            # Retrieve overall scaling
            scaling = scalingMapping[rec['visit']][ccdMapping[rec['detector']]]

            # The postCalibrationOffset describe any zeropoint offsets
            # to apply after the fgcm calibration.  The first part comes
            # from the reference catalog match (used in testing).  The
            # second part comes from the mean chromatic correction
            # (if configured).
            postCalibrationOffset = offsetMapping[rec['filtername']]
            if self.config.doApplyMeanChromaticCorrection:
                postCalibrationOffset += rec['fgcmDeltaChrom']

            fgcmSuperStarField = self._getChebyshevBoundedField(
                rec['fgcmfZptSstarCheb'], rec['fgcmfZptChebXyMax'])
            # Convert from FGCM AB to nJy
            fgcmZptField = self._getChebyshevBoundedField(
                (rec['fgcmfZptCheb'] * units.AB).to_value(units.nJy),
                rec['fgcmfZptChebXyMax'],
                offset=postCalibrationOffset,
                scaling=scaling)

            if self.config.doComposeWcsJacobian:

                fgcmField = afwMath.ProductBoundedField([
                    approxPixelAreaFields[rec['detector']], fgcmSuperStarField,
                    fgcmZptField
                ])
            else:
                # The photoCalib is just the product of the fgcmSuperStarField and the
                # fgcmZptField
                fgcmField = afwMath.ProductBoundedField(
                    [fgcmSuperStarField, fgcmZptField])

            # The "mean" calibration will be set to the center of the ccd for reference
            calibCenter = fgcmField.evaluate(fgcmField.getBBox().getCenter())
            calibErr = (np.log(10.0) / 2.5) * calibCenter * np.sqrt(
                rec['fgcmZptVar'])
            photoCalib = afwImage.PhotoCalib(calibrationMean=calibCenter,
                                             calibrationErr=calibErr,
                                             calibration=fgcmField,
                                             isConstant=False)

            # Return full per-visit exposure catalogs
            if rec['visit'] != lastVisit:
                # This is a new visit.  If the last visit was not -1, yield
                # the ExposureCatalog
                if lastVisit > -1:
                    # ensure that the detectors are in sorted order, for fast lookups
                    zptVisitCatalog.sort()
                    yield (int(lastVisit), zptVisitCatalog)
                else:
                    # We need to create a new schema
                    zptExpCatSchema = afwTable.ExposureTable.makeMinimalSchema(
                    )
                    zptExpCatSchema.addField('visit',
                                             type='L',
                                             doc='Visit number')

                # And start a new one
                zptVisitCatalog = afwTable.ExposureCatalog(zptExpCatSchema)
                zptVisitCatalog.setMetadata(metadata)

                lastVisit = int(rec['visit'])

            catRecord = zptVisitCatalog.addNew()
            catRecord['id'] = int(rec['detector'])
            catRecord['visit'] = rec['visit']
            catRecord.setPhotoCalib(photoCalib)

        # Final output of last exposure catalog
        # ensure that the detectors are in sorted order, for fast lookups
        zptVisitCatalog.sort()
        yield (int(lastVisit), zptVisitCatalog)

    def _getChebyshevBoundedField(self,
                                  coefficients,
                                  xyMax,
                                  offset=0.0,
                                  scaling=1.0):
        """
        Make a ChebyshevBoundedField from fgcm coefficients, with optional offset
        and scaling.

        Parameters
        ----------
        coefficients: `numpy.array`
           Flattened array of chebyshev coefficients
        xyMax: `list` of length 2
           Maximum x and y of the chebyshev bounding box
        offset: `float`, optional
           Absolute calibration offset.  Default is 0.0
        scaling: `float`, optional
           Flat scaling value from fgcmBuildStars.  Default is 1.0

        Returns
        -------
        boundedField: `lsst.afw.math.ChebyshevBoundedField`
        """

        orderPlus1 = int(np.sqrt(coefficients.size))
        pars = np.zeros((orderPlus1, orderPlus1))

        bbox = lsst.geom.Box2I(lsst.geom.Point2I(0.0, 0.0),
                               lsst.geom.Point2I(*xyMax))

        pars[:, :] = (coefficients.reshape(orderPlus1, orderPlus1) *
                      (10.**(offset / -2.5)) * scaling)

        boundedField = afwMath.ChebyshevBoundedField(bbox, pars)

        return boundedField

    def _outputAtmospheres(self, handleDict, atmCat):
        """
        Output the atmospheres.

        Parameters
        ----------
        handleDict : `dict`
            All data handles are `lsst.daf.butler.DeferredDatasetHandle`
            The handleDict has the follownig keys:

            ``"fgcmLookUpTable"``
                handle for the FGCM look-up table.
        atmCat : `lsst.afw.table.BaseCatalog`
            FGCM atmosphere parameter catalog from fgcmFitCycleTask.

        Returns
        -------
        atmospheres : `generator` [(`int`, `lsst.afw.image.TransmissionCurve`)]
            Generator that returns (visit, transmissionCurve) tuples.
        """
        # First, we need to grab the look-up table and key info
        lutCat = handleDict['fgcmLookUpTable'].get()

        atmosphereTableName = lutCat[0]['tablename']
        elevation = lutCat[0]['elevation']
        atmLambda = lutCat[0]['atmLambda']
        lutCat = None

        # Make the atmosphere table if possible
        try:
            atmTable = fgcm.FgcmAtmosphereTable.initWithTableName(
                atmosphereTableName)
            atmTable.loadTable()
        except IOError:
            atmTable = None

        if atmTable is None:
            # Try to use MODTRAN instead
            try:
                modGen = fgcm.ModtranGenerator(elevation)
                lambdaRange = np.array([atmLambda[0], atmLambda[-1]]) / 10.
                lambdaStep = (atmLambda[1] - atmLambda[0]) / 10.
            except (ValueError, IOError) as e:
                raise RuntimeError(
                    "FGCM look-up-table generated with modtran, "
                    "but modtran not configured to run.") from e

        zenith = np.degrees(np.arccos(1. / atmCat['secZenith']))

        for i, visit in enumerate(atmCat['visit']):
            if atmTable is not None:
                # Interpolate the atmosphere table
                atmVals = atmTable.interpolateAtmosphere(
                    pmb=atmCat[i]['pmb'],
                    pwv=atmCat[i]['pwv'],
                    o3=atmCat[i]['o3'],
                    tau=atmCat[i]['tau'],
                    alpha=atmCat[i]['alpha'],
                    zenith=zenith[i],
                    ctranslamstd=[atmCat[i]['cTrans'], atmCat[i]['lamStd']])
            else:
                # Run modtran
                modAtm = modGen(
                    pmb=atmCat[i]['pmb'],
                    pwv=atmCat[i]['pwv'],
                    o3=atmCat[i]['o3'],
                    tau=atmCat[i]['tau'],
                    alpha=atmCat[i]['alpha'],
                    zenith=zenith[i],
                    lambdaRange=lambdaRange,
                    lambdaStep=lambdaStep,
                    ctranslamstd=[atmCat[i]['cTrans'], atmCat[i]['lamStd']])
                atmVals = modAtm['COMBINED']

            # Now need to create something to persist...
            curve = TransmissionCurve.makeSpatiallyConstant(
                throughput=atmVals,
                wavelengths=atmLambda,
                throughputAtMin=atmVals[0],
                throughputAtMax=atmVals[-1])

            yield (int(visit), curve)
Esempio n. 18
0
    def runQuantum(self, butlerQC, inputRefs, outputRefs):
        handleDict = butlerQC.get(inputRefs)

        self.log.info("Running with %d sourceTable_visit handles",
                      (len(handleDict['source_catalogs'])))

        # Run the build stars tasks
        tract = butlerQC.quantum.dataId['tract']

        handleDict['sourceSchema'] = self.sourceSchema

        sourceTableHandles = handleDict['source_catalogs']
        sourceTableHandleDict = {
            sourceTableHandle.dataId['visit']: sourceTableHandle
            for sourceTableHandle in sourceTableHandles
        }

        visitSummaryHandles = handleDict['visitSummary']
        visitSummaryHandleDict = {
            visitSummaryHandle.dataId['visit']: visitSummaryHandle
            for visitSummaryHandle in visitSummaryHandles
        }

        handleDict['sourceTableHandleDict'] = sourceTableHandleDict
        handleDict['visitSummaryHandleDict'] = visitSummaryHandleDict

        # And the outputs
        if self.config.fgcmOutputProducts.doZeropointOutput:
            photoCalibRefDict = {
                photoCalibRef.dataId.byName()['visit']: photoCalibRef
                for photoCalibRef in outputRefs.fgcmPhotoCalib
            }
            handleDict['fgcmPhotoCalibs'] = photoCalibRefDict

        if self.config.fgcmOutputProducts.doAtmosphereOutput:
            atmRefDict = {
                atmRef.dataId.byName()['visit']: atmRef
                for atmRef in outputRefs.fgcmTransmissionAtmosphere
            }
            handleDict['fgcmTransmissionAtmospheres'] = atmRefDict

        if self.config.fgcmBuildStars.doReferenceMatches:
            refConfig = LoadReferenceObjectsConfig()
            refConfig.filterMap = self.config.fgcmBuildStars.fgcmLoadReferenceCatalog.filterMap
            loader = ReferenceObjectLoader(
                dataIds=[ref.datasetRef.dataId for ref in inputRefs.refCat],
                refCats=butlerQC.get(inputRefs.refCat),
                config=refConfig,
                log=self.log)
            buildStarsRefObjLoader = loader
        else:
            buildStarsRefObjLoader = None

        if self.config.fgcmOutputProducts.doReferenceCalibration:
            refConfig = self.config.fgcmOutputProducts.refObjLoader
            loader = ReferenceObjectLoader(
                dataIds=[ref.datasetRef.dataId for ref in inputRefs.refCat],
                refCats=butlerQC.get(inputRefs.refCat),
                config=refConfig,
                log=self.log)
            self.fgcmOutputProducts.refObjLoader = loader

        struct = self.run(handleDict,
                          tract,
                          buildStarsRefObjLoader=buildStarsRefObjLoader)

        if struct.photoCalibCatalogs is not None:
            self.log.info("Outputting photoCalib catalogs.")
            for visit, expCatalog in struct.photoCalibCatalogs:
                butlerQC.put(expCatalog, photoCalibRefDict[visit])
            self.log.info("Done outputting photoCalib catalogs.")

        if struct.atmospheres is not None:
            self.log.info("Outputting atmosphere transmission files.")
            for visit, atm in struct.atmospheres:
                butlerQC.put(atm, atmRefDict[visit])
            self.log.info("Done outputting atmosphere files.")

        # Turn raw repeatability into simple catalog for persistence
        schema = afwTable.Schema()
        schema.addField('rawRepeatability',
                        type=np.float64,
                        doc="Per-band raw repeatability in FGCM calibration.")
        repeatabilityCat = afwTable.BaseCatalog(schema)
        repeatabilityCat.resize(len(struct.repeatability))
        repeatabilityCat['rawRepeatability'][:] = struct.repeatability

        butlerQC.put(repeatabilityCat, outputRefs.fgcmRepeatability)

        return
Esempio n. 19
0
    def test_fgcmLoadReferenceOtherFilters(self):
        """
        Test loading of the fgcm reference catalogs using unmatched filter names.
        """

        filterList = ['HSC-R2', 'HSC-I2']

        config = fgcmcal.FgcmLoadReferenceCatalogConfig()
        config.applyColorTerms = True
        config.filterMap = {'HSC-R2': 'r', 'HSC-I2': 'i'}
        config.colorterms.data = {}
        config.colorterms.data[
            'ps1*'] = lsst.pipe.tasks.colorterms.ColortermDict()
        config.colorterms.data['ps1*'].data = {}
        config.colorterms.data['ps1*'].data[
            'HSC-R2'] = lsst.pipe.tasks.colorterms.Colorterm()
        config.colorterms.data['ps1*'].data['HSC-R2'].primary = 'r'
        config.colorterms.data['ps1*'].data['HSC-R2'].secondary = 'i'
        config.colorterms.data['ps1*'].data['HSC-R2'].c0 = -0.000032
        config.colorterms.data['ps1*'].data['HSC-R2'].c1 = -0.002866
        config.colorterms.data['ps1*'].data['HSC-R2'].c2 = -0.012638
        config.colorterms.data['ps1*'].data[
            'HSC-I2'] = lsst.pipe.tasks.colorterms.Colorterm()
        config.colorterms.data['ps1*'].data['HSC-I2'].primary = 'i'
        config.colorterms.data['ps1*'].data['HSC-I2'].secondary = 'z'
        config.colorterms.data['ps1*'].data['HSC-I2'].c0 = 0.001625
        config.colorterms.data['ps1*'].data['HSC-I2'].c1 = -0.200406
        config.colorterms.data['ps1*'].data['HSC-I2'].c2 = -0.013666

        refCatName = 'ps1_pv3_3pi_20170110'

        butler = lsst.daf.butler.Butler(
            self.repo,
            instrument='HSC',
            collections=['HSC/testdata', 'refcats/gen2'])
        refs = set(butler.registry.queryDatasets(refCatName))
        dataIds = [butler.registry.expandDataId(ref.dataId) for ref in refs]
        refCats = [butler.getDirectDeferred(ref) for ref in refs]

        refConfig = LoadReferenceObjectsConfig()
        refConfig.filterMap = config.filterMap

        refObjLoader = ReferenceObjectLoader(dataIds=dataIds,
                                             refCats=refCats,
                                             config=refConfig)

        loadCat = fgcmcal.FgcmLoadReferenceCatalogTask(
            refObjLoader=refObjLoader, refCatName=refCatName, config=config)

        ra = 337.656174
        dec = 0.823595
        rad = 0.1

        refCat = loadCat.getFgcmReferenceStarsSkyCircle(
            ra, dec, rad, filterList)

        self.assertEqual(len(filterList), refCat['refMag'].shape[1])
        self.assertEqual(len(filterList), refCat['refMagErr'].shape[1])
        test, = np.where((refCat['refMag'][:, 0] < 30.0)
                         & (refCat['refMag'][:, 1] < 30.0))
        self.assertGreater(test.size, 0)
class GenerateDonutCatalogOnlineTask(pipeBase.Task):
    """
    Construct a source catalog from reference catalogs
    and pointing information.

    Parameters
    ----------
    dataIds : `list`
        List of the dataIds for the reference catalog shards.
    refCats : `list`
        List of the deferred dataset references pointing to the pieces
        of the reference catalog we want in the butler.
    **kwargs : dict[str, any]
        Dictionary of input argument: new value for that input argument.
    """

    ConfigClass = GenerateDonutCatalogOnlineTaskConfig
    _DefaultName = "generateDonutCatalogOnlineTask"

    def __init__(self, dataIds, refCats, **kwargs):

        super().__init__(**kwargs)
        refConfig = self.config.refObjLoader
        # refObjLoader handles the interaction with the butler repository
        # needed to get the pieces of the reference catalogs we need.
        self.refObjLoader = ReferenceObjectLoader(dataIds=dataIds,
                                                  refCats=refCats,
                                                  config=refConfig,
                                                  log=self.log)

        if self.config.doReferenceSelection:
            self.makeSubtask("referenceSelector")

        self.filterName = self.config.filterName
        self.config.refObjLoader.pixelMargin = 0
        self.config.refObjLoader.anyFilterMapsToThis = self.filterName
        self.config.referenceSelector.magLimit.fluxField = f"{self.filterName}_flux"
        self.config.referenceSelector.signalToNoise.fluxField = (
            f"{self.filterName}_flux")
        self.config.donutSelector.fluxField = f"{self.filterName}_flux"
        if self.config.doDonutSelection:
            self.makeSubtask("donutSelector")

    @timeMethod
    def run(self, detector, wcs):
        """Get the data from the reference catalog only from the
        shards of the reference catalogs that overlap our pointing.

        Parameters
        ----------
        detector : `lsst.afw.cameraGeom.Detector`
            Detector object from the camera.
        wcs : `lsst.afw.geom.SkyWcs`
            Wcs object defining the pixel to sky (and inverse) transform for
            the supplied ``bbox``.

        Returns
        -------
        struct : `lsst.pipe.base.Struct`
            The struct contains the following data:
                - DonutCatalog: `pandas.DataFrame`
                    The final donut source catalog for the region.
        """
        bbox = detector.getBBox()
        # Get the refcatalog shard
        skyBox = self.refObjLoader.loadPixelBox(bbox,
                                                wcs,
                                                filterName=self.filterName,
                                                bboxToSpherePadding=0)

        if not skyBox.refCat.isContiguous():
            refCat = skyBox.refCat.copy(deep=True)
        else:
            refCat = skyBox.refCat

        donutCatalog = self._formatCatalog(refCat, detector)

        return pipeBase.Struct(donutCatalog=donutCatalog)

    def _formatCatalog(self, refCat, detector):
        """Format a reference afw table into the final format.

        Parameters
        ----------
        refCat : `lsst.afw.table.SourceCatalog`
            Reference catalog in afw format.
        detector : `lsst.afw.cameraGeom.Detector`
            Detector object from the camera.

        Returns
        -------
        refCat : `pandas.DataFrame`
            Reference catalog.
        """

        if self.config.doReferenceSelection:
            goodSources = self.referenceSelector.selectSources(refCat)
            refSelected = goodSources.selected
        else:
            refSelected = np.ones(len(refCat), dtype=bool)

        if self.config.doDonutSelection:
            self.log.info("Running Donut Selector")
            donutSelection = self.donutSelector.run(refCat, detector)
            donutSelected = donutSelection.selected
        else:
            donutSelected = np.ones(len(refCat), dtype=bool)

        selected = refSelected & donutSelected

        npRefCat = np.zeros(
            np.sum(selected),
            dtype=[
                ("coord_ra", "f8"),
                ("coord_dec", "f8"),
                ("centroid_x", "f8"),
                ("centroid_y", "f8"),
                ("source_flux", "f8"),
            ],
        )

        if npRefCat.size == 0:
            # Return an empty catalog if we don't have any selected sources.
            return npRefCat

        # Natively "coord_ra" and "coord_dec" are stored in radians.
        # Doing this as an array rather than by row with the coord access is
        # approximately 600x faster.
        npRefCat["coord_ra"] = refCat["coord_ra"][selected]
        npRefCat["coord_dec"] = refCat["coord_dec"][selected]

        npRefCat["centroid_x"] = refCat["centroid_x"][selected]
        npRefCat["centroid_y"] = refCat["centroid_y"][selected]

        fluxField = f"{self.filterName}_flux"

        # nan_to_num replaces nans with zeros, and this ensures that
        # we select fluxes that both filter out nans and are positive.
        (good, ) = np.where(
            (np.nan_to_num(refCat[fluxField][selected]) > 0.0)
            & (np.nan_to_num(refCat[fluxField + "Err"][selected]) > 0.0))
        npRefCat["source_flux"][good] = refCat[fluxField][selected][good]

        return pd.DataFrame.from_records(npRefCat)
Esempio n. 21
0
        def runTest(withRaDecErr):
            # Generate a second catalog, with different ids
            inTempDir1 = tempfile.TemporaryDirectory()
            inPath1 = inTempDir1.name
            skyCatalogFile1, _, skyCatalog1 = self.makeSkyCatalog(inPath1,
                                                                  idStart=25,
                                                                  seed=123)
            inTempDir2 = tempfile.TemporaryDirectory()
            inPath2 = inTempDir2.name
            skyCatalogFile2, _, skyCatalog2 = self.makeSkyCatalog(inPath2,
                                                                  idStart=5432,
                                                                  seed=11)
            # override some field names, and use multiple cores
            config = ingestIndexTestBase.makeConvertConfig(
                withRaDecErr=withRaDecErr,
                withMagErr=True,
                withPm=True,
                withPmErr=True)
            # use a very small HTM pixelization depth to ensure there will be collisions when
            # ingesting the files in parallel
            depth = 2
            config.dataset_config.indexer.active.depth = depth
            # np.savetxt prepends '# ' to the header lines, so use a reader that understands that
            config.file_reader.format = 'ascii.commented_header'
            config.n_processes = 2  # use multiple cores for this test only
            config.id_name = 'id'  # Use the ids from the generated catalogs
            repoPath = os.path.join(
                self.outPath, "output_multifile_parallel",
                "_withRaDecErr" if withRaDecErr else "_noRaDecErr")

            # Convert the input data files to our HTM indexed format.
            dataTempDir = tempfile.TemporaryDirectory()
            dataPath = dataTempDir.name
            converter = ConvertReferenceCatalogTask(output_dir=dataPath,
                                                    config=config)
            converter.run([skyCatalogFile1, skyCatalogFile2])

            # Make a temporary butler to ingest them into.
            butler = self.makeTemporaryRepo(
                repoPath, config.dataset_config.indexer.active.depth)
            dimensions = [f"htm{depth}"]
            datasetType = DatasetType(config.dataset_config.ref_dataset_name,
                                      dimensions,
                                      "SimpleCatalog",
                                      universe=butler.registry.dimensions,
                                      isCalibration=False)
            butler.registry.registerDatasetType(datasetType)

            # Ingest the files into the new butler.
            run = "testingRun"
            htmTableFile = os.path.join(dataPath, "filename_to_htm.ecsv")
            ingest_files(repoPath,
                         config.dataset_config.ref_dataset_name,
                         run,
                         htmTableFile,
                         transfer="auto")

            # Test if we can get back the catalogs, with a new butler.
            butler = lsst.daf.butler.Butler(repoPath)
            datasetRefs = list(
                butler.registry.queryDatasets(
                    config.dataset_config.ref_dataset_name,
                    collections=[run]).expanded())
            handlers = []
            for dataRef in datasetRefs:
                handlers.append(
                    DeferredDatasetHandle(butler=butler,
                                          ref=dataRef,
                                          parameters=None))
            loaderConfig = ReferenceObjectLoader.ConfigClass()
            loader = ReferenceObjectLoader(
                [dataRef.dataId for dataRef in datasetRefs],
                handlers,
                config=loaderConfig,
                log=self.logger)
            self.checkAllRowsInRefcat(loader, skyCatalog1, config)
            self.checkAllRowsInRefcat(loader, skyCatalog2, config)

            inTempDir1.cleanup()
            inTempDir2.cleanup()
            dataTempDir.cleanup()