LSSTApplications  8.0.0.0+107,8.0.0.1+13,9.1+18,9.2,master-g084aeec0a4,master-g0aced2eed8+6,master-g15627eb03c,master-g28afc54ef9,master-g3391ba5ea0,master-g3d0fb8ae5f,master-g4432ae2e89+36,master-g5c3c32f3ec+17,master-g60f1e072bb+1,master-g6a3ac32d1b,master-g76a88a4307+1,master-g7bce1f4e06+57,master-g8ff4092549+31,master-g98e65bf68e,master-ga6b77976b1+53,master-gae20e2b580+3,master-gb584cd3397+53,master-gc5448b162b+1,master-gc54cf9771d,master-gc69578ece6+1,master-gcbf758c456+22,master-gcec1da163f+63,master-gcf15f11bcc,master-gd167108223,master-gf44c96c709
LSSTDataManagementBasePackage
forcedMeasurement.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # LSST Data Management System
4 # Copyright 2008, 2009, 2010, 2014 LSST Corporation.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <http://www.lsstcorp.org/LegalNotices/>.
22 #
23 """Base classes for forced measurement plugins and the driver task for these.
24 
25 In forced measurement, a reference catalog is used to define restricted measurements (usually just fluxes)
26 on an image. As the reference catalog may be deeper than the detection limit of the measurement image, we
27 do not assume that we can use detection and deblend information from the measurement image. Instead, we
28 assume this information is present in the reference catalog and can be "transformed" in some sense to
29 the measurement frame. At the very least, this means that Footprints from the reference catalog should
30 be transformed and installed as Footprints in the output measurement catalog. If we have a procedure that
31 can transform HeavyFootprints, we can then proceed with measurement as usual, but using the reference
32 catalog's id and parent fields to define deblend families. If this transformation does not preserve
33 HeavyFootprints (this is currently the case), then we will only be able to replace objects with noise
34 one deblend family at a time, and hence measurements run in single-object mode may be contaminated by
35 neighbors when run on objects with parent != 0.
36 
37 Measurements are generally recorded in the coordinate system of the image being measured (and all
38 slot-eligible fields must be), but non-slot fields may be recorded in other coordinate systems if necessary
39 to avoid information loss (this should, of course, be indicated in the field documentation). Note that
40 the reference catalog may be in a different coordinate system; it is the responsibility of plugins
41 to transform the data they need themselves, using the reference WCS provided. However, for plugins
42 that only require a position or shape, they may simply use output SourceCatalog's centroid or shape slots,
43 which will generally be set to the transformed position of the reference object before any other plugins are
44 run, and hence avoid using the reference catalog at all.
45 
46 Command-line driver tasks for forced measurement can be found in forcedPhotImage.py, including
47 ForcedPhotImageTask, ForcedPhotCcdTask, and ForcedPhotCoaddTask.
48 """
49 
50 import lsst.pex.config
51 import lsst.pipe.base
52 
53 from .base import *
54 
55 __all__ = ("ForcedPluginConfig", "ForcedPlugin",
56  "ForcedMeasurementConfig", "ForcedMeasurementTask")
57 
58 class ForcedPluginConfig(BasePluginConfig):
59  """Base class for configs of forced measurement plugins."""
60  pass
61 
62 class ForcedPlugin(BasePlugin):
63 
64  # All subclasses of ForcedPlugin should be registered here
65  registry = PluginRegistry(ForcedPluginConfig)
66 
67  ConfigClass = ForcedPluginConfig
68 
69  def __init__(self, config, name, schemaMapper, metadata):
70  """Initialize the measurement object.
71 
72  @param[in] config An instance of this class's ConfigClass.
73  @param[in] name The string the plugin was registered with.
74  @param[in,out] schemaMapper A SchemaMapper that maps reference catalog fields to output
75  catalog fields. Output fields should be added to the
76  output schema. While most plugins will not need to map
77  fields from the reference schema, if they do so, those fields
78  will be transferred before any plugins are run.
79  @param[in] metadata Plugin metadata that will be attached to the output catalog
80  """
81  BasePlugin.__init__(self)
82  self.config = config
83  self.name = name
84 
85  def measure(self, measRecord, exposure, refRecord, refWcs):
86  """Measure the properties of a source on a single image, given data from a
87  reference record.
88 
89  @param[in] exposure lsst.afw.image.ExposureF, containing the pixel data to
90  be measured and the associated Psf, Wcs, etc. All
91  other sources in the image will have been replaced by
92  noise according to deblender outputs.
93  @param[in,out] measRecord lsst.afw.table.SourceRecord to be filled with outputs,
94  and from which previously-measured quantities can be
95  retreived.
96  @param[in] refRecord lsst.afw.table.SimpleRecord that contains additional
97  parameters to define the fit, as measured elsewhere.
98  @param[in] refWcs The coordinate system for the reference catalog values.
99  An afw.geom.Angle may be passed, indicating that a
100  local tangent Wcs should be created for each object
101  using afw.image.makeLocalWcs and the given angle as
102  a pixel scale.
103 
104  In the normal mode of operation, the source centroid will be set to the
105  WCS-transformed position of the reference object, so plugins that only
106  require a reference position should not have to access the reference object
107  at all.
108  """
109  raise NotImplementedError()
110 
111  def measureN(self, measCat, exposure, refCat, refWcs):
112  """Measure the properties of a group of blended sources on a single image,
113  given data from a reference record.
114 
115  @param[in] exposure lsst.afw.image.ExposureF, containing the pixel data to
116  be measured and the associated Psf, Wcs, etc. Sources
117  not in the blended hierarchy to be measured will have
118  been replaced with noise using deblender outputs.
119  @param[in,out] measCat lsst.afw.table.SourceCatalog to be filled with outputs,
120  and from which previously-measured quantities can be
121  retrieved, containing only the sources that should be
122  measured together in this call.
123  @param[in] refCat lsst.afw.table.SimpleCatalog that contains additional
124  parameters to define the fit, as measured elsewhere.
125  Ordered such that zip(sources, references) may be used.
126  @param[in] refWcs The coordinate system for the reference catalog values.
127  An afw.geom.Angle may be passed, indicating that a
128  local tangent Wcs should be created for each object
129  using afw.image.makeLocalWcs and the given Angle as
130  a pixel scale.
131 
132  In the normal mode of operation, the source centroids will be set to the
133  WCS-transformed position of the reference object, so plugins that only
134  require a reference position should not have to access the reference object
135  at all.
136  """
137  raise NotImplementedError()
138 
139 class ForcedMeasurementConfig(BaseMeasurementConfig):
140  """Config class for forced measurement driver task."""
141 
142  plugins = ForcedPlugin.registry.makeField(
143  multi=True,
144  default=["base_TransformedCentroid",
145  "base_TransformedShape",
146  "base_GaussianFlux",
147  "base_NaiveFlux",
148  "base_PsfFlux",
149  "base_SincFlux",
150  ],
151  doc="Plugins to be run and their configuration"
152  )
153  algorithms = property(lambda self: self.plugins, doc="backwards-compatibility alias for plugins")
154 
155  copyColumns = lsst.pex.config.DictField(
156  keytype=str, itemtype=str, doc="Mapping of reference columns to source columns",
157  default={"id": "objectId", "parent":"parentObjectId"}
158  )
159 
160  def setDefaults(self):
161  self.slots.centroid = "base_TransformedCentroid"
162  self.slots.shape = "base_TransformedShape"
163  self.slots.apFlux = None
164  self.slots.modelFlux = None
165  self.slots.psfFlux = None
166  self.slots.instFlux = None
167 
168 ## @addtogroup LSST_task_documentation
169 ## @{
170 ## @page pageForcedMeasurementTask ForcedMeasurementTask
171 ## ForcedMeasurementTask
172 ## @copybrief ForcedMeasurementTask
173 ## @}
174 
175 class ForcedMeasurementTask(BaseMeasurementTask):
176  """!
177  A subtask for measuring the properties of sources on a single exposure, using an existing
178  "reference" catalog to constrain some aspects of the measurement.
179 
180  The task is configured with a list of "plugins": each plugin defines the values it
181  measures (i.e. the columns in a table it will fill) and conducts that measurement
182  on each detected source (see ForcedPlugin). The job of the
183  measurement task is to initialize the set of plugins (which includes setting up the
184  catalog schema) from their configuration, and then invoke each plugin on each
185  source.
186 
187  Most of the time, ForcedMeasurementTask will be used via one of the subclasses of
188  ForcedPhotImageTask, ForcedPhotCcdTask and ForcedPhotCoaddTask. These combine
189  this measurement subtask with a "references" subtask (see BaseReferencesTask and
190  CoaddSrcReferencesTask) to perform forced measurement using measurements performed on
191  another image as the references. There is generally little reason to use
192  ForcedMeasurementTask outside of one of these drivers, unless it is necessary to avoid
193  using the Butler for I/O.
194 
195  ForcedMeasurementTask has only three methods: __init__(), run(), and generateSources().
196  For configuration options, see SingleFrameMeasurementConfig.
197  """
198 
199  ConfigClass = ForcedMeasurementConfig
200 
201  def __init__(self, refSchema, algMetadata=None, **kwds):
202  """!
203  Initialize the task. Set up the execution order of the plugins and initialize
204  the plugins, giving each plugin an opportunity to add its measurement fields to
205  the output schema and to record information in the task metadata.
206 
207  Note that while SingleFrameMeasurementTask is passed an initial Schema that is
208  appended to in order to create the output Schema, ForcedMeasurementTask is
209  initialized with the Schema of the reference catalog, from which a new Schema
210  for the output catalog is created. Fields to be copied directly from the
211  reference Schema are added before Plugin fields are added.
212 
213  @param[in] refSchema Schema of the reference catalog. Must match the catalog
214  later passed to generateSources() and/or run().
215  @param[in,out] algMetadata lsst.daf.base.PropertyList used to record information about
216  each algorithm. An empty PropertyList will be created if None.
217  @param[in] **kwds Keyword arguments passed from lsst.pipe.base.Task.__init__
218  """
219  super(ForcedMeasurementTask, self).__init__(algMetadata=algMetadata, **kwds)
221  self.mapper.addMinimalSchema(lsst.afw.table.SourceTable.makeMinimalSchema())
222  self.config.slots.setupSchema(self.mapper.editOutputSchema())
223  for refName, targetName in self.config.copyColumns.items():
224  refItem = refSchema.find(refName)
225  self.mapper.addMapping(refItem.key, targetName)
226  # Make a place at the beginning for the centroid plugin to run first (because it's an OrderedDict,
227  # adding an empty element in advance means it will get run first when it's reassigned to the
228  # actual Plugin).
229  if self.config.slots.centroid != None:
230  self.plugins[self.config.slots.centroid] = None
231  # Init the plugins, sorted by execution order. At the same time add to the schema
232  for executionOrder, name, config, PluginClass in sorted(self.config.plugins.apply()):
233  self.plugins[name] = PluginClass(config, name, self.mapper, metadata=self.algMetadata)
234 
235  def run(self, exposure, refCat, refWcs, idFactory=None):
236  """!
237  Perform forced measurement.
238 
239  @param[in] exposure lsst.afw.image.ExposureF to be measured; must have at least a Wcs attached.
240  @param[in] refCat A sequence of SourceRecord objects that provide reference information
241  for the measurement. These will be passed to each Plugin in addition
242  to the output SourceRecord.
243  @param[in] refWcs Wcs that defines the X,Y coordinate system of refCat
244  @param[in] idFactory factory for creating IDs for sources
245 
246  @return SourceCatalog containing measurement results.
247 
248  Delegates creating the initial empty SourceCatalog to generateSources(), then fills it.
249  """
250  # First create a refList from the original which excludes children when a member
251  # of the parent chain is not within the list. This can occur at boundaries when
252  # the parent is outside and one of the children is within. Currently, the parent
253  # chain is always only one deep, but just in case, this code removes any reference
254  # when the parent chain to its topmost parent is broken
255 
256  # Construct a footprints dict which does not contain the footprint, just the parentID
257  # We will add the footprint from the transformed sources from generateSources later.
258  footprints = {ref.getId(): ref.getParent() for ref in refCat}
259  refList = list()
260  for ref in refCat:
261  refId = ref.getId()
262  topId = refId
263  while(topId > 0):
264  if not topId in footprints.keys():
265  footprints.pop(refId)
266  break
267  topId = footprints[topId]
268  if topId == 0:
269  refList.append(ref)
270  # now generate transformed source corresponding to the cleanup up refLst
271  sources = self.generateSources(exposure, refList, refWcs, idFactory)
272 
273  # Steal the transformed source footprint and use it to complete the footprints dict,
274  # which then looks like {ref.getId(): (ref.getParent(), source.getFootprint())}
275  for (reference, source) in zip(refList, sources):
276  footprints[reference.getId()] = (reference.getParent(), source.getFootprint())
277 
278  self.log.info("Performing forced measurement on %d sources" % len(refList))
279 
280  # Build a catalog of just the references we intend to measure
281  referenceCat = lsst.afw.table.SourceCatalog(self.mapper.getInputSchema())
282  referenceCat.extend(refList)
283 
284  # convert the footprints to the coordinate system of the exposure
285  if self.config.doReplaceWithNoise:
286  noiseReplacer = NoiseReplacer(self.config.noiseReplacer, exposure, footprints, log=self.log)
287  else:
288  noiseReplacer = DummyNoiseReplacer()
289 
290  # Create parent cat which slices both the referenceCat and measCat (sources)
291  # first, get the reference and source records which have no parent
292  refParentCat, measParentCat = referenceCat.getChildren(0, sources)
293  for parentIdx, (refParentRecord, measParentRecord) in enumerate(zip(refParentCat,measParentCat)):
294 
295  # first process the records which have the current parent as children
296  refChildCat, measChildCat = referenceCat.getChildren(refParentRecord.getId(), sources)
297  # TODO: skip this loop if there are no plugins configured for single-object mode
298  for refChildRecord, measChildRecord in zip(refChildCat, measChildCat):
299  noiseReplacer.insertSource(refChildRecord.getId())
300  self.callMeasure(measChildRecord, exposure, refChildRecord, refWcs)
301  noiseReplacer.removeSource(refChildRecord.getId())
302 
303  # then process the parent record
304  noiseReplacer.insertSource(refParentRecord.getId())
305  self.callMeasure(measParentRecord, exposure, refParentRecord, refWcs)
306  self.callMeasureN(measParentCat[parentIdx:parentIdx+1], exposure,
307  refParentCat[parentIdx:parentIdx+1])
308  # measure all the children simultaneously
309  self.callMeasureN(measChildCat, exposure, refChildCat)
310  noiseReplacer.removeSource(refParentRecord.getId())
311  noiseReplacer.end()
312  return lsst.pipe.base.Struct(sources=sources)
313 
314  def generateSources(self, exposure, refCat, refWcs, idFactory=None):
315  """!
316  Initialize an output SourceCatalog using information from the reference catalog.
317 
318  This generate a new blank SourceRecord for each record in refCat, copying any
319  fields in ForcedMeasurementConfig.copyColumns. This also transforms the Footprints
320  in refCat to the measurement coordinate system if it differs from refWcs, and attaches
321  these to the new SourceRecords. Note that we do not currently have the ability to
322  transform heavy footprints, so when the reference and measure WCSs are different,
323  HeavyFootprints will be converted to regular Footprints, which makes it impossible
324  to properly measure blended objects.
325 
326  @param[in] exposure Exposure to be measured
327  @param[in] refCat Sequence (not necessarily a SourceCatalog) of reference SourceRecords.
328  @param[in] refWcs Wcs that defines the X,Y coordinate system of refCat
329  @param[in] idFactory factory for creating IDs for sources
330 
331  @return Source catalog ready for measurement
332  """
333  if idFactory == None:
335  schema = self.mapper.getOutputSchema()
336  table = lsst.afw.table.SourceTable.make(schema, idFactory)
337  sources = lsst.afw.table.SourceCatalog(table)
338  table = sources.table
339  table.setMetadata(self.algMetadata)
340  table.preallocate(len(refCat))
341  expRegion = exposure.getBBox()
342  targetWcs = exposure.getWcs()
343  for ref in refCat:
344  newSource = sources.addNew()
345  newSource.assign(ref, self.mapper)
346  footprint = newSource.getFootprint()
347  if footprint is not None:
348  # if heavy, just transform the "light footprint" and leave the rest behind
349  if footprint.isHeavy():
350  footprint = lsst.afw.detection.Footprint(footprint)
351  if not refWcs == targetWcs:
352  footprint = footprint.transform(refWcs, targetWcs, expRegion, True)
353  newSource.setFootprint(footprint)
354  return sources
Defines the fields and offsets for a table.
Definition: Schema.h:46
def generateSources
Initialize an output SourceCatalog using information from the reference catalog.
A mapping between the keys of two Schemas, used to copy data between them.
Definition: SchemaMapper.h:19
A subtask for measuring the properties of sources on a single exposure, using an existing &quot;reference&quot;...
static boost::shared_ptr< IdFactory > makeSimple()
Return a simple IdFactory that simply counts from 1.
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:55
static Schema makeMinimalSchema()
Return a minimal schema for Source tables and records.
Definition: Source.h:233
A set of pixels in an Image.
Definition: Footprint.h:73
static boost::shared_ptr< SourceTable > make(Schema const &schema, boost::shared_ptr< IdFactory > const &idFactory)
Construct a new table.