Coverage for python/lsst/meas/base/forcedPhotCcd.py : 25%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of meas_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
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 GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import collections
24import lsst.pex.config
25import lsst.pex.exceptions
26from lsst.log import Log
27import lsst.pipe.base
28import lsst.geom
29import lsst.afw.geom
30import lsst.afw.image
31import lsst.afw.table
32import lsst.sphgeom
34from lsst.pipe.base import PipelineTaskConnections
35import lsst.pipe.base.connectionTypes as cT
37import lsst.pipe.base as pipeBase
38from lsst.skymap import BaseSkyMap
40from .references import MultiBandReferencesTask
41from .forcedMeasurement import ForcedMeasurementTask
42from .applyApCorr import ApplyApCorrTask
43from .catalogCalculation import CatalogCalculationTask
45try:
46 from lsst.meas.mosaic import applyMosaicResults
47except ImportError:
48 applyMosaicResults = None
50__all__ = ("PerTractCcdDataIdContainer", "ForcedPhotCcdConfig", "ForcedPhotCcdTask", "imageOverlapsTract")
53class PerTractCcdDataIdContainer(pipeBase.DataIdContainer):
54 """A data ID container which combines raw data IDs with a tract.
56 Notes
57 -----
58 Required because we need to add "tract" to the raw data ID keys (defined as
59 whatever we use for ``src``) when no tract is provided (so that the user is
60 not required to know which tracts are spanned by the raw data ID).
62 This subclass of `~lsst.pipe.base.DataIdContainer` assumes that a calexp is
63 being measured using the detection information, a set of reference
64 catalogs, from the set of coadds which intersect with the calexp. It needs
65 the calexp id (e.g. visit, raft, sensor), but is also uses the tract to
66 decide what set of coadds to use. The references from the tract whose
67 patches intersect with the calexp are used.
68 """
70 def makeDataRefList(self, namespace):
71 """Make self.refList from self.idList
72 """
73 if self.datasetType is None:
74 raise RuntimeError("Must call setDatasetType first")
75 log = Log.getLogger("meas.base.forcedPhotCcd.PerTractCcdDataIdContainer")
76 skymap = None
77 visitTract = collections.defaultdict(set) # Set of tracts for each visit
78 visitRefs = collections.defaultdict(list) # List of data references for each visit
79 for dataId in self.idList:
80 if "tract" not in dataId:
81 # Discover which tracts the data overlaps
82 log.info("Reading WCS for components of dataId=%s to determine tracts", dict(dataId))
83 if skymap is None:
84 skymap = namespace.butler.get(namespace.config.coaddName + "Coadd_skyMap")
86 for ref in namespace.butler.subset("calexp", dataId=dataId):
87 if not ref.datasetExists("calexp"):
88 continue
90 visit = ref.dataId["visit"]
91 visitRefs[visit].append(ref)
93 md = ref.get("calexp_md", immediate=True)
94 wcs = lsst.afw.geom.makeSkyWcs(md)
95 box = lsst.geom.Box2D(lsst.afw.image.bboxFromMetadata(md))
96 # Going with just the nearest tract. Since we're throwing all tracts for the visit
97 # together, this shouldn't be a problem unless the tracts are much smaller than a CCD.
98 tract = skymap.findTract(wcs.pixelToSky(box.getCenter()))
99 if imageOverlapsTract(tract, wcs, box):
100 visitTract[visit].add(tract.getId())
101 else:
102 self.refList.extend(ref for ref in namespace.butler.subset(self.datasetType, dataId=dataId))
104 # Ensure all components of a visit are kept together by putting them all in the same set of tracts
105 for visit, tractSet in visitTract.items():
106 for ref in visitRefs[visit]:
107 for tract in tractSet:
108 self.refList.append(namespace.butler.dataRef(datasetType=self.datasetType,
109 dataId=ref.dataId, tract=tract))
110 if visitTract:
111 tractCounter = collections.Counter()
112 for tractSet in visitTract.values():
113 tractCounter.update(tractSet)
114 log.info("Number of visits for each tract: %s", dict(tractCounter))
117def imageOverlapsTract(tract, imageWcs, imageBox):
118 """Return whether the given bounding box overlaps the tract given a WCS.
120 Parameters
121 ----------
122 tract : `lsst.skymap.TractInfo`
123 TractInfo specifying a tract.
124 imageWcs : `lsst.afw.geom.SkyWcs`
125 World coordinate system for the image.
126 imageBox : `lsst.geom.Box2I`
127 Bounding box for the image.
129 Returns
130 -------
131 overlap : `bool`
132 `True` if the bounding box overlaps the tract; `False` otherwise.
133 """
134 tractPoly = tract.getOuterSkyPolygon()
136 imagePixelCorners = lsst.geom.Box2D(imageBox).getCorners()
137 try:
138 imageSkyCorners = imageWcs.pixelToSky(imagePixelCorners)
139 except lsst.pex.exceptions.LsstCppException as e:
140 # Protecting ourselves from awful Wcs solutions in input images
141 if (not isinstance(e.message, lsst.pex.exceptions.DomainErrorException)
142 and not isinstance(e.message, lsst.pex.exceptions.RuntimeErrorException)):
143 raise
144 return False
146 imagePoly = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in imageSkyCorners])
147 return tractPoly.intersects(imagePoly) # "intersects" also covers "contains" or "is contained by"
150class ForcedPhotCcdConnections(PipelineTaskConnections,
151 dimensions=("instrument", "visit", "detector", "skymap", "tract"),
152 defaultTemplates={"inputCoaddName": "deep",
153 "inputName": "calexp"}):
154 inputSchema = cT.InitInput(
155 doc="Schema for the input measurement catalogs.",
156 name="{inputCoaddName}Coadd_ref_schema",
157 storageClass="SourceCatalog",
158 )
159 outputSchema = cT.InitOutput(
160 doc="Schema for the output forced measurement catalogs.",
161 name="forced_src_schema",
162 storageClass="SourceCatalog",
163 )
164 exposure = cT.Input(
165 doc="Input exposure to perform photometry on.",
166 name="{inputName}",
167 storageClass="ExposureF",
168 dimensions=["instrument", "visit", "detector"],
169 )
170 refCat = cT.Input(
171 doc="Catalog of shapes and positions at which to force photometry.",
172 name="{inputCoaddName}Coadd_ref",
173 storageClass="SourceCatalog",
174 dimensions=["skymap", "tract", "patch"],
175 multiple=True
176 )
177 skyMap = cT.Input(
178 doc="SkyMap dataset that defines the coordinate system of the reference catalog.",
179 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
180 storageClass="SkyMap",
181 dimensions=["skymap"],
182 )
183 measCat = cT.Output(
184 doc="Output forced photometry catalog.",
185 name="forced_src",
186 storageClass="SourceCatalog",
187 dimensions=["instrument", "visit", "detector", "skymap", "tract"],
188 )
191class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig,
192 pipelineConnections=ForcedPhotCcdConnections):
193 """Config class for forced measurement driver task."""
194 references = lsst.pex.config.ConfigurableField(
195 target=MultiBandReferencesTask,
196 doc="subtask to retrieve reference source catalog"
197 )
198 measurement = lsst.pex.config.ConfigurableField(
199 target=ForcedMeasurementTask,
200 doc="subtask to do forced measurement"
201 )
202 coaddName = lsst.pex.config.Field(
203 doc="coadd name: typically one of deep or goodSeeing",
204 dtype=str,
205 default="deep",
206 )
207 doApCorr = lsst.pex.config.Field(
208 dtype=bool,
209 default=True,
210 doc="Run subtask to apply aperture corrections"
211 )
212 applyApCorr = lsst.pex.config.ConfigurableField(
213 target=ApplyApCorrTask,
214 doc="Subtask to apply aperture corrections"
215 )
216 catalogCalculation = lsst.pex.config.ConfigurableField(
217 target=CatalogCalculationTask,
218 doc="Subtask to run catalogCalculation plugins on catalog"
219 )
220 doApplyUberCal = lsst.pex.config.Field(
221 dtype=bool,
222 doc="Apply meas_mosaic ubercal results to input calexps?",
223 default=False,
224 deprecated="Deprecated by DM-23352; use doApplyExternalPhotoCalib and doApplyExternalSkyWcs instead",
225 )
226 doApplyExternalPhotoCalib = lsst.pex.config.Field(
227 dtype=bool,
228 default=False,
229 doc=("Whether to apply external photometric calibration via an "
230 "`lsst.afw.image.PhotoCalib` object. Uses the "
231 "``externalPhotoCalibName`` field to determine which calibration "
232 "to load."),
233 )
234 doApplyExternalSkyWcs = lsst.pex.config.Field(
235 dtype=bool,
236 default=False,
237 doc=("Whether to apply external astrometric calibration via an "
238 "`lsst.afw.geom.SkyWcs` object. Uses ``externalSkyWcsName`` "
239 "field to determine which calibration to load."),
240 )
241 doApplySkyCorr = lsst.pex.config.Field(
242 dtype=bool,
243 default=False,
244 doc="Apply sky correction?",
245 )
246 includePhotoCalibVar = lsst.pex.config.Field(
247 dtype=bool,
248 default=False,
249 doc="Add photometric calibration variance to warp variance plane?",
250 )
251 externalPhotoCalibName = lsst.pex.config.ChoiceField(
252 dtype=str,
253 doc=("Type of external PhotoCalib if ``doApplyExternalPhotoCalib`` is True. "
254 "Unused for Gen3 middleware."),
255 default="jointcal",
256 allowed={
257 "jointcal": "Use jointcal_photoCalib",
258 "fgcm": "Use fgcm_photoCalib",
259 "fgcm_tract": "Use fgcm_tract_photoCalib"
260 },
261 )
262 externalSkyWcsName = lsst.pex.config.ChoiceField(
263 dtype=str,
264 doc="Type of external SkyWcs if ``doApplyExternalSkyWcs`` is True. Unused for Gen3 middleware.",
265 default="jointcal",
266 allowed={
267 "jointcal": "Use jointcal_wcs"
268 },
269 )
271 def setDefaults(self):
272 # Docstring inherited.
273 # Make catalogCalculation a no-op by default as no modelFlux is setup by default in
274 # ForcedMeasurementTask
275 super().setDefaults()
277 self.catalogCalculation.plugins.names = []
280class ForcedPhotCcdTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
281 """A command-line driver for performing forced measurement on CCD images.
283 Parameters
284 ----------
285 butler : `lsst.daf.persistence.butler.Butler`, optional
286 A Butler which will be passed to the references subtask to allow it to
287 load its schema from disk. Optional, but must be specified if
288 ``refSchema`` is not; if both are specified, ``refSchema`` takes
289 precedence.
290 refSchema : `lsst.afw.table.Schema`, optional
291 The schema of the reference catalog, passed to the constructor of the
292 references subtask. Optional, but must be specified if ``butler`` is
293 not; if both are specified, ``refSchema`` takes precedence.
294 **kwds
295 Keyword arguments are passed to the supertask constructor.
297 Notes
298 -----
299 The `runDataRef` method takes a `~lsst.daf.persistence.ButlerDataRef` argument
300 that corresponds to a single CCD. This should contain the data ID keys that
301 correspond to the ``forced_src`` dataset (the output dataset for this
302 task), which are typically all those used to specify the ``calexp`` dataset
303 (``visit``, ``raft``, ``sensor`` for LSST data) as well as a coadd tract.
304 The tract is used to look up the appropriate coadd measurement catalogs to
305 use as references (e.g. ``deepCoadd_src``; see
306 :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` for more
307 information). While the tract must be given as part of the dataRef, the
308 patches are determined automatically from the bounding box and WCS of the
309 calexp to be measured, and the filter used to fetch references is set via
310 the ``filter`` option in the configuration of
311 :lsst-task:`lsst.meas.base.references.BaseReferencesTask`).
312 """
314 ConfigClass = ForcedPhotCcdConfig
315 RunnerClass = pipeBase.ButlerInitializedTaskRunner
316 _DefaultName = "forcedPhotCcd"
317 dataPrefix = ""
319 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds):
320 super().__init__(**kwds)
322 if initInputs is not None:
323 refSchema = initInputs['inputSchema'].schema
325 self.makeSubtask("references", butler=butler, schema=refSchema)
326 if refSchema is None:
327 refSchema = self.references.schema
328 self.makeSubtask("measurement", refSchema=refSchema)
329 # It is necessary to get the schema internal to the forced measurement task until such a time
330 # that the schema is not owned by the measurement task, but is passed in by an external caller
331 if self.config.doApCorr:
332 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
333 self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
334 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
336 def runQuantum(self, butlerQC, inputRefs, outputRefs):
337 inputs = butlerQC.get(inputRefs)
339 tract = butlerQC.quantum.dataId['tract']
340 skyMap = inputs.pop("skyMap")
341 inputs['refWcs'] = skyMap[tract].getWcs()
343 inputs['refCat'] = self.mergeAndFilterReferences(inputs['exposure'], inputs['refCat'],
344 inputs['refWcs'])
346 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId,
347 inputs['exposure'],
348 inputs['refCat'], inputs['refWcs'],
349 "visit_detector")
350 self.attachFootprints(inputs['measCat'], inputs['refCat'], inputs['exposure'], inputs['refWcs'])
351 # TODO: apply external calibrations (DM-17062)
352 outputs = self.run(**inputs)
353 butlerQC.put(outputs, outputRefs)
355 def mergeAndFilterReferences(self, exposure, refCats, refWcs):
356 """Filter reference catalog so that all sources are within the
357 boundaries of the exposure.
359 Parameters
360 ----------
361 exposure : `lsst.afw.image.exposure.Exposure`
362 Exposure to generate the catalog for.
363 refCats : sequence of `lsst.afw.table.SourceCatalog`
364 Catalogs of shapes and positions at which to force photometry.
365 refWcs : `lsst.afw.image.SkyWcs`
366 Reference world coordinate system.
368 Returns
369 -------
370 refSources : `lsst.afw.table.SourceCatalog`
371 Filtered catalog of forced sources to measure.
373 Notes
374 -----
375 Filtering the reference catalog is currently handled by Gen2
376 specific methods. To function for Gen3, this method copies
377 code segments to do the filtering and transformation. The
378 majority of this code is based on the methods of
379 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
381 """
383 # Step 1: Determine bounds of the exposure photometry will
384 # be performed on.
385 expWcs = exposure.getWcs()
386 expRegion = exposure.getBBox(lsst.afw.image.PARENT)
387 expBBox = lsst.geom.Box2D(expRegion)
388 expBoxCorners = expBBox.getCorners()
389 expSkyCorners = [expWcs.pixelToSky(corner).getVector() for
390 corner in expBoxCorners]
391 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners)
393 # Step 2: Filter out reference catalog sources that are
394 # not contained within the exposure boundaries, or whose
395 # parents are not within the exposure boundaries. Note
396 # that within a single input refCat, the parents always
397 # appear before the children.
398 mergedRefCat = lsst.afw.table.SourceCatalog(refCats[0].table)
399 for refCat in refCats:
400 containedIds = {0} # zero as a parent ID means "this is a parent"
401 for record in refCat:
402 if expPolygon.contains(record.getCoord().getVector()) and record.getParent() in containedIds:
403 record.setFootprint(record.getFootprint().transform(refWcs, expWcs, expRegion))
404 mergedRefCat.append(record)
405 containedIds.add(record.getId())
406 mergedRefCat.sort(lsst.afw.table.SourceTable.getParentKey())
407 return mergedRefCat
409 def generateMeasCat(self, exposureDataId, exposure, refCat, refWcs, idPackerName):
410 """Generate a measurement catalog for Gen3.
412 Parameters
413 ----------
414 exposureDataId : `DataId`
415 Butler dataId for this exposure.
416 exposure : `lsst.afw.image.exposure.Exposure`
417 Exposure to generate the catalog for.
418 refCat : `lsst.afw.table.SourceCatalog`
419 Catalog of shapes and positions at which to force photometry.
420 refWcs : `lsst.afw.image.SkyWcs`
421 Reference world coordinate system.
422 idPackerName : `str`
423 Type of ID packer to construct from the registry.
425 Returns
426 -------
427 measCat : `lsst.afw.table.SourceCatalog`
428 Catalog of forced sources to measure.
429 expId : `int`
430 Unique binary id associated with the input exposure
431 """
432 expId, expBits = exposureDataId.pack(idPackerName, returnMaxBits=True)
433 idFactory = lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
435 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
436 idFactory=idFactory)
437 return measCat, expId
439 def runDataRef(self, dataRef, psfCache=None):
440 """Perform forced measurement on a single exposure.
442 Parameters
443 ----------
444 dataRef : `lsst.daf.persistence.ButlerDataRef`
445 Passed to the ``references`` subtask to obtain the reference WCS,
446 the ``getExposure`` method (implemented by derived classes) to
447 read the measurment image, and the ``fetchReferences`` method to
448 get the exposure and load the reference catalog (see
449 :lsst-task`lsst.meas.base.references.CoaddSrcReferencesTask`).
450 Refer to derived class documentation for details of the datasets
451 and data ID keys which are used.
452 psfCache : `int`, optional
453 Size of PSF cache, or `None`. The size of the PSF cache can have
454 a significant effect upon the runtime for complicated PSF models.
456 Notes
457 -----
458 Sources are generated with ``generateMeasCat`` in the ``measurement``
459 subtask. These are passed to ``measurement``'s ``run`` method, which
460 fills the source catalog with the forced measurement results. The
461 sources are then passed to the ``writeOutputs`` method (implemented by
462 derived classes) which writes the outputs.
463 """
464 refWcs = self.references.getWcs(dataRef)
465 exposure = self.getExposure(dataRef)
466 if psfCache is not None:
467 exposure.getPsf().setCacheSize(psfCache)
468 refCat = self.fetchReferences(dataRef, exposure)
470 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
471 idFactory=self.makeIdFactory(dataRef))
472 self.log.info("Performing forced measurement on %s" % (dataRef.dataId,))
473 self.attachFootprints(measCat, refCat, exposure, refWcs)
475 exposureId = self.getExposureId(dataRef)
477 forcedPhotResult = self.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
479 self.writeOutput(dataRef, forcedPhotResult.measCat)
481 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
482 """Perform forced measurement on a single exposure.
484 Parameters
485 ----------
486 measCat : `lsst.afw.table.SourceCatalog`
487 The measurement catalog, based on the sources listed in the
488 reference catalog.
489 exposure : `lsst.afw.image.Exposure`
490 The measurement image upon which to perform forced detection.
491 refCat : `lsst.afw.table.SourceCatalog`
492 The reference catalog of sources to measure.
493 refWcs : `lsst.afw.image.SkyWcs`
494 The WCS for the references.
495 exposureId : `int`
496 Optional unique exposureId used for random seed in measurement
497 task.
499 Returns
500 -------
501 result : `lsst.pipe.base.Struct`
502 Structure with fields:
504 ``measCat``
505 Catalog of forced measurement results
506 (`lsst.afw.table.SourceCatalog`).
507 """
508 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
509 if self.config.doApCorr:
510 self.applyApCorr.run(
511 catalog=measCat,
512 apCorrMap=exposure.getInfo().getApCorrMap()
513 )
514 self.catalogCalculation.run(measCat)
516 return pipeBase.Struct(measCat=measCat)
518 def makeIdFactory(self, dataRef):
519 """Create an object that generates globally unique source IDs.
521 Source IDs are created based on a per-CCD ID and the ID of the CCD
522 itself.
524 Parameters
525 ----------
526 dataRef : `lsst.daf.persistence.ButlerDataRef`
527 Butler data reference. The ``ccdExposureId_bits`` and
528 ``ccdExposureId`` datasets are accessed. The data ID must have the
529 keys that correspond to ``ccdExposureId``, which are generally the
530 same as those that correspond to ``calexp`` (``visit``, ``raft``,
531 ``sensor`` for LSST data).
532 """
533 expBits = dataRef.get("ccdExposureId_bits")
534 expId = int(dataRef.get("ccdExposureId"))
535 return lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
537 def getExposureId(self, dataRef):
538 return int(dataRef.get("ccdExposureId", immediate=True))
540 def fetchReferences(self, dataRef, exposure):
541 """Get sources that overlap the exposure.
543 Parameters
544 ----------
545 dataRef : `lsst.daf.persistence.ButlerDataRef`
546 Butler data reference corresponding to the image to be measured;
547 should have ``tract``, ``patch``, and ``filter`` keys.
548 exposure : `lsst.afw.image.Exposure`
549 The image to be measured (used only to obtain a WCS and bounding
550 box).
552 Returns
553 -------
554 referencs : `lsst.afw.table.SourceCatalog`
555 Catalog of sources that overlap the exposure
557 Notes
558 -----
559 The returned catalog is sorted by ID and guarantees that all included
560 children have their parent included and that all Footprints are valid.
562 All work is delegated to the references subtask; see
563 :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask`
564 for information about the default behavior.
565 """
566 references = lsst.afw.table.SourceCatalog(self.references.schema)
567 badParents = set()
568 unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs())
569 for record in unfiltered:
570 if record.getFootprint() is None or record.getFootprint().getArea() == 0:
571 if record.getParent() != 0:
572 self.log.warn("Skipping reference %s (child of %s) with bad Footprint",
573 record.getId(), record.getParent())
574 else:
575 self.log.warn("Skipping reference parent %s with bad Footprint", record.getId())
576 badParents.add(record.getId())
577 elif record.getParent() not in badParents:
578 references.append(record)
579 # catalog must be sorted by parent ID for lsst.afw.table.getChildren to work
580 references.sort(lsst.afw.table.SourceTable.getParentKey())
581 return references
583 def attachFootprints(self, sources, refCat, exposure, refWcs):
584 r"""Attach footprints to blank sources prior to measurements.
586 Notes
587 -----
588 `~lsst.afw.detection.Footprint`\ s for forced photometry must be in the
589 pixel coordinate system of the image being measured, while the actual
590 detections may start out in a different coordinate system.
592 Subclasses of this class must implement this method to define how
593 those `~lsst.afw.detection.Footprint`\ s should be generated.
595 This default implementation transforms the
596 `~lsst.afw.detection.Footprint`\ s from the reference catalog from the
597 reference WCS to the exposure's WcS, which downgrades
598 `lsst.afw.detection.heavyFootprint.HeavyFootprint`\ s into regular
599 `~lsst.afw.detection.Footprint`\ s, destroying deblend information.
600 """
601 return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs)
603 def getExposure(self, dataRef):
604 """Read input exposure for measurement.
606 Parameters
607 ----------
608 dataRef : `lsst.daf.persistence.ButlerDataRef`
609 Butler data reference.
610 """
611 exposure = dataRef.get(self.dataPrefix + "calexp", immediate=True)
613 if self.config.doApplyExternalPhotoCalib:
614 source = f"{self.config.externalPhotoCalibName}_photoCalib"
615 self.log.info("Applying external photoCalib from %s", source)
616 photoCalib = dataRef.get(source)
617 exposure.setPhotoCalib(photoCalib) # No need for calibrateImage; having the photoCalib suffices
619 if self.config.doApplyExternalSkyWcs:
620 source = f"{self.config.externalSkyWcsName}_wcs"
621 self.log.info("Applying external skyWcs from %s", source)
622 skyWcs = dataRef.get(source)
623 exposure.setWcs(skyWcs)
625 if self.config.doApplySkyCorr:
626 self.log.info("Apply sky correction")
627 skyCorr = dataRef.get("skyCorr")
628 exposure.maskedImage -= skyCorr.getImage()
630 return exposure
632 def writeOutput(self, dataRef, sources):
633 """Write forced source table
635 Parameters
636 ----------
637 dataRef : `lsst.daf.persistence.ButlerDataRef`
638 Butler data reference. The forced_src dataset (with
639 self.dataPrefix prepended) is all that will be modified.
640 sources : `lsst.afw.table.SourceCatalog`
641 Catalog of sources to save.
642 """
643 dataRef.put(sources, self.dataPrefix + "forced_src", flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS)
645 def getSchemaCatalogs(self):
646 """The schema catalogs that will be used by this task.
648 Returns
649 -------
650 schemaCatalogs : `dict`
651 Dictionary mapping dataset type to schema catalog.
653 Notes
654 -----
655 There is only one schema for each type of forced measurement. The
656 dataset type for this measurement is defined in the mapper.
657 """
658 catalog = lsst.afw.table.SourceCatalog(self.measurement.schema)
659 catalog.getTable().setMetadata(self.measurement.algMetadata)
660 datasetType = self.dataPrefix + "forced_src"
661 return {datasetType: catalog}
663 def _getConfigName(self):
664 # Documented in superclass.
665 return self.dataPrefix + "forcedPhotCcd_config"
667 def _getMetadataName(self):
668 # Documented in superclass
669 return self.dataPrefix + "forcedPhotCcd_metadata"
671 @classmethod
672 def _makeArgumentParser(cls):
673 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
674 parser.add_id_argument("--id", "forced_src", help="data ID with raw CCD keys [+ tract optionally], "
675 "e.g. --id visit=12345 ccd=1,2 [tract=0]",
676 ContainerClass=PerTractCcdDataIdContainer)
677 return parser