LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
forcedMeasurement.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # LSST Data Management System
4 # Copyright 2008-2015 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, at least for CCD forced photometry), then we will only
34 be able to replace objects with noise one deblend family at a time, and hence measurements run in
35 single-object mode may be contaminated by 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 from builtins import zip
50 
51 import lsst.pex.config
52 import lsst.pipe.base
53 
54 from .pluginRegistry import PluginRegistry
55 from .baseMeasurement import (BaseMeasurementPluginConfig, BaseMeasurementPlugin,
56  BaseMeasurementConfig, BaseMeasurementTask)
57 from .noiseReplacer import NoiseReplacer, DummyNoiseReplacer
58 
59 __all__ = ("ForcedPluginConfig", "ForcedPlugin",
60  "ForcedMeasurementConfig", "ForcedMeasurementTask")
61 
62 
63 class ForcedPluginConfig(BaseMeasurementPluginConfig):
64  """Base class for configs of forced measurement plugins."""
65  pass
66 
67 
68 class ForcedPlugin(BaseMeasurementPlugin):
69 
70  # All subclasses of ForcedPlugin should be registered here
71  registry = PluginRegistry(ForcedPluginConfig)
72 
73  ConfigClass = ForcedPluginConfig
74 
75  def __init__(self, config, name, schemaMapper, metadata):
76  """Initialize the measurement object.
77 
78  @param[in] config An instance of this class's ConfigClass.
79  @param[in] name The string the plugin was registered with.
80  @param[in,out] schemaMapper A SchemaMapper that maps reference catalog fields to output
81  catalog fields. Output fields should be added to the
82  output schema. While most plugins will not need to map
83  fields from the reference schema, if they do so, those fields
84  will be transferred before any plugins are run.
85  @param[in] metadata Plugin metadata that will be attached to the output catalog
86  """
87  BaseMeasurementPlugin.__init__(self, config, name)
88 
89  def measure(self, measRecord, exposure, refRecord, refWcs):
90  """Measure the properties of a source on a single image, given data from a
91  reference record.
92 
93  @param[in] exposure lsst.afw.image.ExposureF, containing the pixel data to
94  be measured and the associated Psf, Wcs, etc. All
95  other sources in the image will have been replaced by
96  noise according to deblender outputs.
97  @param[in,out] measRecord lsst.afw.table.SourceRecord to be filled with outputs,
98  and from which previously-measured quantities can be
99  retreived.
100  @param[in] refRecord lsst.afw.table.SimpleRecord that contains additional
101  parameters to define the fit, as measured elsewhere.
102  @param[in] refWcs The coordinate system for the reference catalog values.
103  An afw.geom.Angle may be passed, indicating that a
104  local tangent Wcs should be created for each object
105  using afw.image.makeLocalWcs and the given angle as
106  a pixel scale.
107 
108  In the normal mode of operation, the source centroid will be set to the
109  WCS-transformed position of the reference object, so plugins that only
110  require a reference position should not have to access the reference object
111  at all.
112  """
113  raise NotImplementedError()
114 
115  def measureN(self, measCat, exposure, refCat, refWcs):
116  """Measure the properties of a group of blended sources on a single image,
117  given data from a reference record.
118 
119  @param[in] exposure lsst.afw.image.ExposureF, containing the pixel data to
120  be measured and the associated Psf, Wcs, etc. Sources
121  not in the blended hierarchy to be measured will have
122  been replaced with noise using deblender outputs.
123  @param[in,out] measCat lsst.afw.table.SourceCatalog to be filled with outputs,
124  and from which previously-measured quantities can be
125  retrieved, containing only the sources that should be
126  measured together in this call.
127  @param[in] refCat lsst.afw.table.SimpleCatalog that contains additional
128  parameters to define the fit, as measured elsewhere.
129  Ordered such that zip(sources, references) may be used.
130  @param[in] refWcs The coordinate system for the reference catalog values.
131  An afw.geom.Angle may be passed, indicating that a
132  local tangent Wcs should be created for each object
133  using afw.image.makeLocalWcs and the given Angle as
134  a pixel scale.
135 
136  In the normal mode of operation, the source centroids will be set to the
137  WCS-transformed position of the reference object, so plugins that only
138  require a reference position should not have to access the reference object
139  at all.
140  """
141  raise NotImplementedError()
142 
143 
144 class ForcedMeasurementConfig(BaseMeasurementConfig):
145  """Config class for forced measurement driver task."""
146 
147  plugins = ForcedPlugin.registry.makeField(
148  multi=True,
149  default=["base_TransformedCentroid",
150  "base_TransformedShape",
151  "base_GaussianFlux",
152  "base_CircularApertureFlux",
153  "base_PsfFlux",
154  ],
155  doc="Plugins to be run and their configuration"
156  )
157  algorithms = property(lambda self: self.plugins, doc="backwards-compatibility alias for plugins")
158  undeblended = ForcedPlugin.registry.makeField(
159  multi=True,
160  default=[],
161  doc="Plugins to run on undeblended image"
162  )
163 
164  copyColumns = lsst.pex.config.DictField(
165  keytype=str, itemtype=str, doc="Mapping of reference columns to source columns",
166  default={"id": "objectId", "parent": "parentObjectId"}
167  )
168 
169  checkUnitsParseStrict = lsst.pex.config.Field(
170  doc="Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
171  dtype=str,
172  default="raise",
173  )
174 
175  def setDefaults(self):
176  self.slots.centroid = "base_TransformedCentroid"
177  self.slots.shape = "base_TransformedShape"
178  self.slots.apFlux = None
179  self.slots.modelFlux = None
180  self.slots.psfFlux = None
181  self.slots.instFlux = None
182  self.slots.calibFlux = None
183 
184 ## \addtogroup LSST_task_documentation
185 ## \{
186 ## \page ForcedMeasurementTask
187 ## \ref ForcedMeasurementTask_ "ForcedMeasurementTask"
188 ## \copybrief ForcedMeasurementTask
189 ## \}
190 
191 
192 class ForcedMeasurementTask(BaseMeasurementTask):
193  """!
194  \anchor ForcedMeasurementTask_
195 
196  \brief A subtask for measuring the properties of sources on a single
197  exposure, using an existing "reference" catalog to constrain some aspects
198  of the measurement.
199 
200  The task is configured with a list of "plugins": each plugin defines the values it
201  measures (i.e. the columns in a table it will fill) and conducts that measurement
202  on each detected source (see ForcedPlugin). The job of the
203  measurement task is to initialize the set of plugins (which includes setting up the
204  catalog schema) from their configuration, and then invoke each plugin on each
205  source.
206 
207  Most of the time, ForcedMeasurementTask will be used via one of the subclasses of
208  ForcedPhotImageTask, ForcedPhotCcdTask and ForcedPhotCoaddTask. These combine
209  this measurement subtask with a "references" subtask (see BaseReferencesTask and
210  CoaddSrcReferencesTask) to perform forced measurement using measurements performed on
211  another image as the references. There is generally little reason to use
212  ForcedMeasurementTask outside of one of these drivers, unless it is necessary to avoid
213  using the Butler for I/O.
214 
215  ForcedMeasurementTask has only three methods: __init__(), run(), and generateMeasCat().
216  For configuration options, see SingleFrameMeasurementConfig.
217  """
218 
219  ConfigClass = ForcedMeasurementConfig
220 
221  def __init__(self, refSchema, algMetadata=None, **kwds):
222  """!
223  Initialize the task. Set up the execution order of the plugins and initialize
224  the plugins, giving each plugin an opportunity to add its measurement fields to
225  the output schema and to record information in the task metadata.
226 
227  Note that while SingleFrameMeasurementTask is passed an initial Schema that is
228  appended to in order to create the output Schema, ForcedMeasurementTask is
229  initialized with the Schema of the reference catalog, from which a new Schema
230  for the output catalog is created. Fields to be copied directly from the
231  reference Schema are added before Plugin fields are added.
232 
233  @param[in] refSchema Schema of the reference catalog. Must match the catalog
234  later passed to generateMeasCat() and/or run().
235  @param[in,out] algMetadata lsst.daf.base.PropertyList used to record information about
236  each algorithm. An empty PropertyList will be created if None.
237  @param[in] **kwds Keyword arguments passed from lsst.pipe.base.Task.__init__
238  """
239  super(ForcedMeasurementTask, self).__init__(algMetadata=algMetadata, **kwds)
241  self.mapper.addMinimalSchema(lsst.afw.table.SourceTable.makeMinimalSchema(), False)
242  self.config.slots.setupSchema(self.mapper.editOutputSchema())
243  for refName, targetName in self.config.copyColumns.items():
244  refItem = refSchema.find(refName)
245  self.mapper.addMapping(refItem.key, targetName)
246  self.config.slots.setupSchema(self.mapper.editOutputSchema())
247  self.initializePlugins(schemaMapper=self.mapper)
248  self.schema = self.mapper.getOutputSchema()
249  self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
250 
251  def run(self, measCat, exposure, refCat, refWcs, exposureId=None, beginOrder=None, endOrder=None):
252  """!
253  Perform forced measurement.
254 
255  @param[in] exposure lsst.afw.image.ExposureF to be measured; must have at least a Wcs attached.
256  @param[in] measCat Source catalog for measurement results; must be initialized with empty
257  records already corresponding to those in refCat (via e.g. generateMeasCat).
258  @param[in] refCat A sequence of SourceRecord objects that provide reference information
259  for the measurement. These will be passed to each Plugin in addition
260  to the output SourceRecord.
261  @param[in] refWcs Wcs that defines the X,Y coordinate system of refCat
262  @param[in] exposureId optional unique exposureId used to calculate random number
263  generator seed in the NoiseReplacer.
264  @param[in] beginOrder beginning execution order (inclusive): measurements with
265  executionOrder < beginOrder are not executed. None for no limit.
266  @param[in] endOrder ending execution order (exclusive): measurements with
267  executionOrder >= endOrder are not executed. None for no limit.
268 
269  Fills the initial empty SourceCatalog with forced measurement results. Two steps must occur
270  before run() can be called:
271  - generateMeasCat() must be called to create the output measCat argument.
272  - Footprints appropriate for the forced sources must be attached to the measCat records. The
273  attachTransformedFootprints() method can be used to do this, but this degrades HeavyFootprints
274  to regular Footprints, leading to non-deblended measurement, so most callers should provide
275  Footprints some other way. Typically, calling code will have access to information that will
276  allow them to provide HeavyFootprints - for instance, ForcedPhotCoaddTask uses the HeavyFootprints
277  from deblending run in the same band just before non-forced is run measurement in that band.
278  """
279  # First check that the reference catalog does not contain any children for which
280  # any member of their parent chain is not within the list. This can occur at
281  # boundaries when the parent is outside and one of the children is within.
282  # Currently, the parent chain is always only one deep, but just in case, this
283  # code checks for any case where when the parent chain to a child's topmost
284  # parent is broken and raises an exception if it occurs.
285  #
286  # I.e. this code checks that this precondition is satisfied by whatever reference
287  # catalog provider is being paired with it.
288  refCatIdDict = {ref.getId(): ref.getParent() for ref in refCat}
289  for ref in refCat:
290  refId = ref.getId()
291  topId = refId
292  while(topId > 0):
293  if topId not in refCatIdDict:
294  raise RuntimeError("Reference catalog contains a child for which at least "
295  "one parent in its parent chain is not in the catalog.")
296  topId = refCatIdDict[topId]
297 
298  # Construct a footprints dict which looks like
299  # {ref.getId(): (ref.getParent(), source.getFootprint())}
300  # (i.e. getting the footprint from the transformed source footprint)
301  footprints = {ref.getId(): (ref.getParent(), measRecord.getFootprint())
302  for (ref, measRecord) in zip(refCat, measCat)}
303 
304  self.log.info("Performing forced measurement on %d sources" % (len(refCat),))
305 
306  if self.config.doReplaceWithNoise:
307  noiseReplacer = NoiseReplacer(self.config.noiseReplacer, exposure,
308  footprints, log=self.log, exposureId=exposureId)
309  algMetadata = measCat.getTable().getMetadata()
310  if algMetadata is not None:
311  algMetadata.addInt("NOISE_SEED_MULTIPLIER", self.config.noiseReplacer.noiseSeedMultiplier)
312  algMetadata.addString("NOISE_SOURCE", self.config.noiseReplacer.noiseSource)
313  algMetadata.addDouble("NOISE_OFFSET", self.config.noiseReplacer.noiseOffset)
314  if exposureId is not None:
315  algMetadata.addLong("NOISE_EXPOSURE_ID", exposureId)
316  else:
317  noiseReplacer = DummyNoiseReplacer()
318 
319  # Create parent cat which slices both the refCat and measCat (sources)
320  # first, get the reference and source records which have no parent
321  refParentCat, measParentCat = refCat.getChildren(0, measCat)
322  for parentIdx, (refParentRecord, measParentRecord) in enumerate(zip(refParentCat, measParentCat)):
323 
324  # first process the records which have the current parent as children
325  refChildCat, measChildCat = refCat.getChildren(refParentRecord.getId(), measCat)
326  # TODO: skip this loop if there are no plugins configured for single-object mode
327  for refChildRecord, measChildRecord in zip(refChildCat, measChildCat):
328  noiseReplacer.insertSource(refChildRecord.getId())
329  self.callMeasure(measChildRecord, exposure, refChildRecord, refWcs,
330  beginOrder=beginOrder, endOrder=endOrder)
331  noiseReplacer.removeSource(refChildRecord.getId())
332 
333  # then process the parent record
334  noiseReplacer.insertSource(refParentRecord.getId())
335  self.callMeasure(measParentRecord, exposure, refParentRecord, refWcs,
336  beginOrder=beginOrder, endOrder=endOrder)
337  self.callMeasureN(measParentCat[parentIdx:parentIdx+1], exposure,
338  refParentCat[parentIdx:parentIdx+1],
339  beginOrder=beginOrder, endOrder=endOrder)
340  # measure all the children simultaneously
341  self.callMeasureN(measChildCat, exposure, refChildCat,
342  beginOrder=beginOrder, endOrder=endOrder)
343  noiseReplacer.removeSource(refParentRecord.getId())
344  noiseReplacer.end()
345 
346  # Undeblended plugins only fire if we're running everything
347  if endOrder is None:
348  for measRecord, refRecord in zip(measCat, refCat):
349  for plugin in self.undeblendedPlugins.iter():
350  self.doMeasurement(plugin, measRecord, exposure, refRecord, refWcs)
351 
352  def generateMeasCat(self, exposure, refCat, refWcs, idFactory=None):
353  """!Initialize an output SourceCatalog using information from the reference catalog.
354 
355  This generates a new blank SourceRecord for each record in refCat. Note that this
356  method does not attach any Footprints. Doing so is up to the caller (who may
357  call attachedTransformedFootprints or define their own method - see run() for more
358  information).
359 
360  @param[in] exposure Exposure to be measured
361  @param[in] refCat Sequence (not necessarily a SourceCatalog) of reference SourceRecords.
362  @param[in] refWcs Wcs that defines the X,Y coordinate system of refCat
363  @param[in] idFactory factory for creating IDs for sources
364 
365  @return Source catalog ready for measurement
366  """
367  if idFactory is None:
369  table = lsst.afw.table.SourceTable.make(self.schema, idFactory)
370  measCat = lsst.afw.table.SourceCatalog(table)
371  table = measCat.table
372  table.setMetadata(self.algMetadata)
373  table.preallocate(len(refCat))
374  for ref in refCat:
375  newSource = measCat.addNew()
376  newSource.assign(ref, self.mapper)
377  return measCat
378 
379  def attachTransformedFootprints(self, sources, refCat, exposure, refWcs):
380  """!Default implementation for attaching Footprints to blank sources prior to measurement
381 
382  Footprints for forced photometry must be in the pixel coordinate system of the image being
383  measured, while the actual detections may start out in a different coordinate system.
384  This default implementation transforms the Footprints from the reference catalog from the
385  refWcs to the exposure's Wcs, which downgrades HeavyFootprints into regular Footprints,
386  destroying deblend information.
387 
388  Note that ForcedPhotImageTask delegates to this method in its own attachFootprints method.
389  attachFootprints can then be overridden by its subclasses to define how their Footprints
390  should be generated.
391 
392  See the documentation for run() for information about the relationships between run(),
393  generateMeasCat(), and attachTransformedFootprints().
394  """
395  exposureWcs = exposure.getWcs()
396  region = exposure.getBBox(lsst.afw.image.PARENT)
397  for srcRecord, refRecord in zip(sources, refCat):
398  srcRecord.setFootprint(refRecord.getFootprint().transform(refWcs, exposureWcs, region))
static boost::shared_ptr< IdFactory > makeSimple()
Return a simple IdFactory that simply counts from 1.
static boost::shared_ptr< SourceTable > make(Schema const &schema, boost::shared_ptr< IdFactory > const &idFactory)
Construct a new table.
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 Schema makeMinimalSchema()
Return a minimal schema for Source tables and records.
Definition: Source.h:241
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
def attachTransformedFootprints
Default implementation for attaching Footprints to blank sources prior to measurement.
def generateMeasCat
Initialize an output SourceCatalog using information from the reference catalog.