Skip to content

Commit

Permalink
Convert ForcedPhotCcd and ForcedPhotCoadd into pipelineTasks.
Browse files Browse the repository at this point in the history
The conversion of ForcedPhotCoadd was straightforward.  ForcedPhotCcd
required more handling, with duplicate operations in adjustArgsAndRun
to work around Gen-2 butler methods in the source filtering and
transformation code.
  • Loading branch information
czwa committed Jan 31, 2019
1 parent a4d4bcb commit e3d6ebd
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 4 deletions.
103 changes: 103 additions & 0 deletions python/lsst/meas/base/forcedPhotCcd.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,25 @@ class ForcedPhotCcdConfig(ForcedPhotImageConfig):
default=False
)

def setDefaults(self):
super().setDefaults()

# Override the Gen3 datasets from ForcedPhotImageTaskConfig.
# When these nameTemplate values are used in ForcedPhotCoadd,
# there is a coadd name that may change, depending on the
# coadd type. For CCD forced photometry, there will likely
# only ever be a single calexp type, but for consistency, use
# the nameTemplate with nothing to substitute.
self.outputSchema.nameTemplate = "forced_src_schema"
self.exposure.nameTemplate = "{inputName}"
self.exposure.dimensions = ["Instrument", "Visit", "Detector"]
self.measCat.nameTemplate = "forced_src"
self.measCat.dimensions = ["Instrument", "Visit", "Detector", "SkyMap", "Tract"]

self.formatTemplateNames({"inputName": "calexp",
"inputCoaddName": "deep"})
self.quantum.dimensions = ("Instrument", "Visit", "Detector", "SkyMap", "Tract")


class ForcedPhotCcdTask(ForcedPhotImageTask):
"""A command-line driver for performing forced measurement on CCD images.
Expand Down Expand Up @@ -186,6 +205,90 @@ class ForcedPhotCcdTask(ForcedPhotImageTask):
_DefaultName = "forcedPhotCcd"
dataPrefix = ""

def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler):
inputData['refCat'] = self.filterReferences(inputData['exposure'],
inputData['refCat'], inputData['refWcs'])

if 'measCat' not in inputData.keys():
inputData['measCat'] = self.generateMeasCat(inputDataIds['exposure'],
inputData['exposure'],
inputData['refCat'], inputData['refWcs'],
"VisitDetector", butler)
else:
raise ValueError("Unexpected measCat found in inputData for adaptArgsAndRun.")

return super().adaptArgsAndRun(inputData, inputDataIds, outputDataIds, butler)

def filterReferences(self, exposure, refCat, refWcs):
"""Filter reference catalog so that all sources are within the
boundaries of the exposure.
Parameters
----------
exposure : `lsst.afw.image.exposure.Exposure`
Exposure to generate the catalog for.
refCat : `lsst.afw.table.SourceCatalog`
Catalog of shapes and positions at which to force photometry.
refWcs : `lsst.afw.image.SkyWcs`
Reference world coordinate system.
Returns
-------
refSources : `lsst.afw.table.SourceCatalog`
Filtered catalog of forced sources to measure.
Notes
-----
Filtering the reference catalog is currently handled by Gen2
specific methods. To function for Gen3, this method copies
code segments to do the filtering and transformation. The
majority of this code is based on the methods of
lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
"""

# Step 1: Determine bounds of the exposure photometry will
# be performed on.
expWcs = exposure.getWcs()
expRegion = exposure.getBBox(lsst.afw.image.PARENT)
expBBox = lsst.geom.Box2D(expRegion)
expBoxCorners = expBBox.getCorners()
expSkyCorners = [expWcs.pixelToSky(corner).getVector() for
corner in expBoxCorners]
expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners)

# Step 2: Filter out reference catalog sources that are
# not contained within the exposure boundaries.
sources = type(refCat)(refCat.table)
for record in refCat:
if expPolygon.contains(record.getCoord().getVector()):
sources.append(record)
refCatIdDict = {ref.getId(): ref.getParent() for ref in sources}

# Step 3: Cull sources that do not have their parent
# source in the filtered catalog. Save two copies of each
# source.
refSources = type(refCat)(refCat.table)
for record in refCat:
if expPolygon.contains(record.getCoord().getVector()):
recordId = record.getId()
topId = recordId
while (topId > 0):
if topId in refCatIdDict:
topId = refCatIdDict[topId]
else:
break
if topId == 0:
refSources.append(record)

# Step 4: Transform source footprints from the reference
# coordinates to the exposure coordinates.
for refRecord in refSources:
refRecord.setFootprint(refRecord.getFootprint().transform(refWcs,
expWcs, expRegion))
# Step 5: Replace reference catalog with filtered source list.
return refSources

def makeIdFactory(self, dataRef):
"""Create an object that generates globally unique source IDs.
Expand Down
108 changes: 104 additions & 4 deletions python/lsst/meas/base/forcedPhotImage.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,49 @@
__all__ = ("ForcedPhotImageConfig", "ForcedPhotImageTask")


class ForcedPhotImageConfig(lsst.pex.config.Config):
class ForcedPhotImageConfig(lsst.pipe.base.PipelineTaskConfig):
"""Config class for forced measurement driver task."""
# Gen 3 options
inputSchema = lsst.pipe.base.InitInputDatasetField(
doc="Schema for the input measurement catalogs.",
nameTemplate="{inputCoaddName}Coadd_ref_schema",
storageClass="SourceCatalog",
)
outputSchema = lsst.pipe.base.InitOutputDatasetField(
doc="Schema for the output forced measurement catalogs.",
nameTemplate="{outputCoaddName}Coadd_forced_src_schema",
storageClass="SourceCatalog",
)
exposure = lsst.pipe.base.InputDatasetField(
doc="Input exposure to perform photometry on.",
nameTemplate="{inputCoaddName}Coadd",
scalar=True,
storageClass="ExposureF",
dimensions=["AbstractFilter", "SkyMap", "Tract", "Patch"],
)
refCat = lsst.pipe.base.InputDatasetField(
doc="Catalog of shapes and positions at which to force photometry.",
nameTemplate="{inputCoaddName}Coadd_ref",
scalar=True,
storageClass="SourceCatalog",
dimensions=["SkyMap", "Tract", "Patch"],
)
refWcs = lsst.pipe.base.InputDatasetField(
doc="Reference world coordinate system.",
nameTemplate="{inputCoaddName}Coadd.wcs",
scalar=True,
storageClass="TablePersistableWcs",
dimensions=["AbstractFilter", "SkyMap", "Tract", "Patch"],
)
measCat = lsst.pipe.base.OutputDatasetField(
doc="Output forced photometry catalog.",
nameTemplate="{outputCoaddName}Coadd_forced_src",
scalar=True,
storageClass="SourceCatalog",
dimensions=["AbstractFilter", "SkyMap", "Tract", "Patch"],
)

# ForcedPhotImage options
references = lsst.pex.config.ConfigurableField(
target=MultiBandReferencesTask,
doc="subtask to retrieve reference source catalog"
Expand Down Expand Up @@ -73,10 +113,16 @@ def setDefaults(self):
# Docstring inherited.
# Make catalogCalculation a no-op by default as no modelFlux is setup by default in
# ForcedMeasurementTask
super().setDefaults()

self.catalogCalculation.plugins.names = []
self.formatTemplateNames({"inputCoaddName": "deep",
"outputCoaddName": "deep",
"inputName": None})
self.quantum.dimensions = ("AbstractFilter", "SkyMap", "Tract", "Patch")


class ForcedPhotImageTask(lsst.pipe.base.CmdLineTask):
class ForcedPhotImageTask(lsst.pipe.base.PipelineTask, lsst.pipe.base.CmdLineTask):
"""A base class for command-line forced measurement drivers.
Parameters
Expand Down Expand Up @@ -111,8 +157,13 @@ class ForcedPhotImageTask(lsst.pipe.base.CmdLineTask):
ConfigClass = ForcedPhotImageConfig
_DefaultName = "processImageForcedTask"

def __init__(self, butler=None, refSchema=None, **kwds):
super(lsst.pipe.base.CmdLineTask, self).__init__(**kwds)
def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds):
super().__init__(**kwds)

if initInputs is not None:
if refSchema is None:
refSchema = initInputs['inputSchema'].schema

self.makeSubtask("references", butler=butler, schema=refSchema)
if refSchema is None:
refSchema = self.references.schema
Expand All @@ -123,6 +174,55 @@ def __init__(self, butler=None, refSchema=None, **kwds):
self.makeSubtask("applyApCorr", schema=self.measurement.schema)
self.makeSubtask('catalogCalculation', schema=self.measurement.schema)

def getInitOutputDatasets(self):
return {"outputSchema": lsst.afw.table.SourceCatalog(self.measurement.schema)}

def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler):
# ForcedPhotImageTask.run() expects to work in-place on measCat. This requires it to be
# constructed here before being passed as an input to run(). It should not be in the
# inputData dict(), but check that we don't clobber something.
if 'measCat' not in inputData.keys():
inputData['measCat'] = self.generateMeasCat(inputDataIds['exposure'],
inputData['exposure'],
inputData['refCat'], inputData['refWcs'],
"TractPatch", butler)
else:
raise ValueError("Unexpected measCat found in inputData for adaptArgsAndRun.")

return super().adaptArgsAndRun(inputData, inputDataIds, outputDataIds, butler)

def generateMeasCat(self, exposureDataId, exposure, refCat, refWcs, idPackerName, butler):
"""Generate a measurement catalog for Gen3.
Parameters
----------
exposureDataId : `DataId`
Butler dataId for this exposure.
exposure : `lsst.afw.image.exposure.Exposure`
Exposure to generate the catalog for.
refCat : `lsst.afw.table.SourceCatalog`
Catalog of shapes and positions at which to force photometry.
refWcs : `lsst.afw.image.SkyWcs`
Reference world coordinate system.
idPackerName : `str`
Type of ID packer to construct from the registry.
butler : `lsst.daf.persistence.butler.Butler`
Butler to use to construct id packer.
Returns
-------
measCat : `lsst.afw.table.SourceCatalog`
Catalog of forced sources to measure.
"""
packer = butler.registry.makeDataIdPacker(idPackerName, exposureDataId)
expId = packer.pack(exposureDataId)
expBits = packer.maxBits
idFactory = lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)

measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
idFactory=idFactory)
return measCat

def runDataRef(self, dataRef, psfCache=None):
"""Perform forced measurement on a single exposure.
Expand Down

0 comments on commit e3d6ebd

Please sign in to comment.