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 # ForcedPhotImage options
195 references = lsst.pex.config.ConfigurableField(
196 target=MultiBandReferencesTask,
197 doc="subtask to retrieve reference source catalog"
198 )
199 measurement = lsst.pex.config.ConfigurableField(
200 target=ForcedMeasurementTask,
201 doc="subtask to do forced measurement"
202 )
203 coaddName = lsst.pex.config.Field(
204 doc="coadd name: typically one of deep or goodSeeing",
205 dtype=str,
206 default="deep",
207 )
208 doApCorr = lsst.pex.config.Field(
209 dtype=bool,
210 default=True,
211 doc="Run subtask to apply aperture corrections"
212 )
213 applyApCorr = lsst.pex.config.ConfigurableField(
214 target=ApplyApCorrTask,
215 doc="Subtask to apply aperture corrections"
216 )
217 catalogCalculation = lsst.pex.config.ConfigurableField(
218 target=CatalogCalculationTask,
219 doc="Subtask to run catalogCalculation plugins on catalog"
220 )
221 doApplyUberCal = lsst.pex.config.Field(
222 dtype=bool,
223 doc="Apply meas_mosaic ubercal results to input calexps?",
224 default=False,
225 deprecated="Deprecated by DM-23352; use doApplyExternalPhotoCalib and doApplyExternalSkyWcs instead",
226 )
227 doApplyExternalPhotoCalib = lsst.pex.config.Field(
228 dtype=bool,
229 default=False,
230 doc=("Whether to apply external photometric calibration via an "
231 "`lsst.afw.image.PhotoCalib` object. Uses the "
232 "``externalPhotoCalibName`` field to determine which calibration "
233 "to load."),
234 )
235 doApplyExternalSkyWcs = lsst.pex.config.Field(
236 dtype=bool,
237 default=False,
238 doc=("Whether to apply external astrometric calibration via an "
239 "`lsst.afw.geom.SkyWcs` object. Uses ``externalSkyWcsName`` "
240 "field to determine which calibration to load."),
241 )
242 doApplySkyCorr = lsst.pex.config.Field(
243 dtype=bool,
244 default=False,
245 doc="Apply sky correction?",
246 )
247 includePhotoCalibVar = lsst.pex.config.Field(
248 dtype=bool,
249 default=False,
250 doc="Add photometric calibration variance to warp variance plane?",
251 )
252 externalPhotoCalibName = lsst.pex.config.ChoiceField(
253 dtype=str,
254 doc=("Type of external PhotoCalib if ``doApplyExternalPhotoCalib`` is True. "
255 "Unused for Gen3 middleware."),
256 default="jointcal",
257 allowed={
258 "jointcal": "Use jointcal_photoCalib",
259 "fgcm": "Use fgcm_photoCalib",
260 "fgcm_tract": "Use fgcm_tract_photoCalib"
261 },
262 )
263 externalSkyWcsName = lsst.pex.config.ChoiceField(
264 dtype=str,
265 doc="Type of external SkyWcs if ``doApplyExternalSkyWcs`` is True. Unused for Gen3 middleware.",
266 default="jointcal",
267 allowed={
268 "jointcal": "Use jointcal_wcs"
269 },
270 )
272 def setDefaults(self):
273 # Docstring inherited.
274 # Make catalogCalculation a no-op by default as no modelFlux is setup by default in
275 # ForcedMeasurementTask
276 super().setDefaults()
278 self.catalogCalculation.plugins.names = []
281class ForcedPhotCcdTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
282 """A command-line driver for performing forced measurement on CCD images.
284 Parameters
285 ----------
286 butler : `lsst.daf.persistence.butler.Butler`, optional
287 A Butler which will be passed to the references subtask to allow it to
288 load its schema from disk. Optional, but must be specified if
289 ``refSchema`` is not; if both are specified, ``refSchema`` takes
290 precedence.
291 refSchema : `lsst.afw.table.Schema`, optional
292 The schema of the reference catalog, passed to the constructor of the
293 references subtask. Optional, but must be specified if ``butler`` is
294 not; if both are specified, ``refSchema`` takes precedence.
295 **kwds
296 Keyword arguments are passed to the supertask constructor.
298 Notes
299 -----
300 The `runDataRef` method takes a `~lsst.daf.persistence.ButlerDataRef` argument
301 that corresponds to a single CCD. This should contain the data ID keys that
302 correspond to the ``forced_src`` dataset (the output dataset for this
303 task), which are typically all those used to specify the ``calexp`` dataset
304 (``visit``, ``raft``, ``sensor`` for LSST data) as well as a coadd tract.
305 The tract is used to look up the appropriate coadd measurement catalogs to
306 use as references (e.g. ``deepCoadd_src``; see
307 :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` for more
308 information). While the tract must be given as part of the dataRef, the
309 patches are determined automatically from the bounding box and WCS of the
310 calexp to be measured, and the filter used to fetch references is set via
311 the ``filter`` option in the configuration of
312 :lsst-task:`lsst.meas.base.references.BaseReferencesTask`).
313 """
315 ConfigClass = ForcedPhotCcdConfig
316 RunnerClass = pipeBase.ButlerInitializedTaskRunner
317 _DefaultName = "forcedPhotCcd"
318 dataPrefix = ""
320 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds):
321 super().__init__(**kwds)
323 if initInputs is not None:
324 refSchema = initInputs['inputSchema'].schema
326 self.makeSubtask("references", butler=butler, schema=refSchema)
327 if refSchema is None:
328 refSchema = self.references.schema
329 self.makeSubtask("measurement", refSchema=refSchema)
330 # It is necessary to get the schema internal to the forced measurement task until such a time
331 # that the schema is not owned by the measurement task, but is passed in by an external caller
332 if self.config.doApCorr:
333 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
334 self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
335 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
337 def runQuantum(self, butlerQC, inputRefs, outputRefs):
338 inputs = butlerQC.get(inputRefs)
340 tract = butlerQC.quantum.dataId['tract']
341 skyMap = inputs.pop("skyMap")
342 inputs['refWcs'] = skyMap[tract].getWcs()
344 inputs['refCat'] = self.mergeAndFilterReferences(inputs['exposure'], inputs['refCat'],
345 inputs['refWcs'])
347 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId,
348 inputs['exposure'],
349 inputs['refCat'], inputs['refWcs'],
350 "visit_detector")
351 self.attachFootprints(inputs['measCat'], inputs['refCat'], inputs['exposure'], inputs['refWcs'])
352 # TODO: apply external calibrations (DM-17062)
353 outputs = self.run(**inputs)
354 butlerQC.put(outputs, outputRefs)
356 def mergeAndFilterReferences(self, exposure, refCats, refWcs):
357 """Filter reference catalog so that all sources are within the
358 boundaries of the exposure.
360 Parameters
361 ----------
362 exposure : `lsst.afw.image.exposure.Exposure`
363 Exposure to generate the catalog for.
364 refCats : sequence of `lsst.afw.table.SourceCatalog`
365 Catalogs of shapes and positions at which to force photometry.
366 refWcs : `lsst.afw.image.SkyWcs`
367 Reference world coordinate system.
369 Returns
370 -------
371 refSources : `lsst.afw.table.SourceCatalog`
372 Filtered catalog of forced sources to measure.
374 Notes
375 -----
376 Filtering the reference catalog is currently handled by Gen2
377 specific methods. To function for Gen3, this method copies
378 code segments to do the filtering and transformation. The
379 majority of this code is based on the methods of
380 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
382 """
384 # Step 1: Determine bounds of the exposure photometry will
385 # be performed on.
386 expWcs = exposure.getWcs()
387 expRegion = exposure.getBBox(lsst.afw.image.PARENT)
388 expBBox = lsst.geom.Box2D(expRegion)
389 expBoxCorners = expBBox.getCorners()
390 expSkyCorners = [expWcs.pixelToSky(corner).getVector() for
391 corner in expBoxCorners]
392 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners)
394 # Step 2: Filter out reference catalog sources that are
395 # not contained within the exposure boundaries, or whose
396 # parents are not within the exposure boundaries. Note
397 # that within a single input refCat, the parents always
398 # appear before the children.
399 mergedRefCat = lsst.afw.table.SourceCatalog(refCats[0].table)
400 for refCat in refCats:
401 containedIds = {0} # zero as a parent ID means "this is a parent"
402 for record in refCat:
403 if expPolygon.contains(record.getCoord().getVector()) and record.getParent() in containedIds:
404 record.setFootprint(record.getFootprint().transform(refWcs, expWcs, expRegion))
405 mergedRefCat.append(record)
406 containedIds.add(record.getId())
407 mergedRefCat.sort(lsst.afw.table.SourceTable.getParentKey())
408 return mergedRefCat
410 def generateMeasCat(self, exposureDataId, exposure, refCat, refWcs, idPackerName):
411 """Generate a measurement catalog for Gen3.
413 Parameters
414 ----------
415 exposureDataId : `DataId`
416 Butler dataId for this exposure.
417 exposure : `lsst.afw.image.exposure.Exposure`
418 Exposure to generate the catalog for.
419 refCat : `lsst.afw.table.SourceCatalog`
420 Catalog of shapes and positions at which to force photometry.
421 refWcs : `lsst.afw.image.SkyWcs`
422 Reference world coordinate system.
423 idPackerName : `str`
424 Type of ID packer to construct from the registry.
426 Returns
427 -------
428 measCat : `lsst.afw.table.SourceCatalog`
429 Catalog of forced sources to measure.
430 expId : `int`
431 Unique binary id associated with the input exposure
432 """
433 expId, expBits = exposureDataId.pack(idPackerName, returnMaxBits=True)
434 idFactory = lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
436 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
437 idFactory=idFactory)
438 return measCat, expId
440 def runDataRef(self, dataRef, psfCache=None):
441 """Perform forced measurement on a single exposure.
443 Parameters
444 ----------
445 dataRef : `lsst.daf.persistence.ButlerDataRef`
446 Passed to the ``references`` subtask to obtain the reference WCS,
447 the ``getExposure`` method (implemented by derived classes) to
448 read the measurment image, and the ``fetchReferences`` method to
449 get the exposure and load the reference catalog (see
450 :lsst-task`lsst.meas.base.references.CoaddSrcReferencesTask`).
451 Refer to derived class documentation for details of the datasets
452 and data ID keys which are used.
453 psfCache : `int`, optional
454 Size of PSF cache, or `None`. The size of the PSF cache can have
455 a significant effect upon the runtime for complicated PSF models.
457 Notes
458 -----
459 Sources are generated with ``generateMeasCat`` in the ``measurement``
460 subtask. These are passed to ``measurement``'s ``run`` method, which
461 fills the source catalog with the forced measurement results. The
462 sources are then passed to the ``writeOutputs`` method (implemented by
463 derived classes) which writes the outputs.
464 """
465 refWcs = self.references.getWcs(dataRef)
466 exposure = self.getExposure(dataRef)
467 if psfCache is not None:
468 exposure.getPsf().setCacheSize(psfCache)
469 refCat = self.fetchReferences(dataRef, exposure)
471 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
472 idFactory=self.makeIdFactory(dataRef))
473 self.log.info("Performing forced measurement on %s" % (dataRef.dataId,))
474 self.attachFootprints(measCat, refCat, exposure, refWcs)
476 exposureId = self.getExposureId(dataRef)
478 forcedPhotResult = self.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
480 self.writeOutput(dataRef, forcedPhotResult.measCat)
482 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
483 """Perform forced measurement on a single exposure.
485 Parameters
486 ----------
487 measCat : `lsst.afw.table.SourceCatalog`
488 The measurement catalog, based on the sources listed in the
489 reference catalog.
490 exposure : `lsst.afw.image.Exposure`
491 The measurement image upon which to perform forced detection.
492 refCat : `lsst.afw.table.SourceCatalog`
493 The reference catalog of sources to measure.
494 refWcs : `lsst.afw.image.SkyWcs`
495 The WCS for the references.
496 exposureId : `int`
497 Optional unique exposureId used for random seed in measurement
498 task.
500 Returns
501 -------
502 result : `lsst.pipe.base.Struct`
503 Structure with fields:
505 ``measCat``
506 Catalog of forced measurement results
507 (`lsst.afw.table.SourceCatalog`).
508 """
509 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
510 if self.config.doApCorr:
511 self.applyApCorr.run(
512 catalog=measCat,
513 apCorrMap=exposure.getInfo().getApCorrMap()
514 )
515 self.catalogCalculation.run(measCat)
517 return pipeBase.Struct(measCat=measCat)
519 def makeIdFactory(self, dataRef):
520 """Create an object that generates globally unique source IDs.
522 Source IDs are created based on a per-CCD ID and the ID of the CCD
523 itself.
525 Parameters
526 ----------
527 dataRef : `lsst.daf.persistence.ButlerDataRef`
528 Butler data reference. The ``ccdExposureId_bits`` and
529 ``ccdExposureId`` datasets are accessed. The data ID must have the
530 keys that correspond to ``ccdExposureId``, which are generally the
531 same as those that correspond to ``calexp`` (``visit``, ``raft``,
532 ``sensor`` for LSST data).
533 """
534 expBits = dataRef.get("ccdExposureId_bits")
535 expId = int(dataRef.get("ccdExposureId"))
536 return lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
538 def getExposureId(self, dataRef):
539 return int(dataRef.get("ccdExposureId", immediate=True))
541 def fetchReferences(self, dataRef, exposure):
542 """Get sources that overlap the exposure.
544 Parameters
545 ----------
546 dataRef : `lsst.daf.persistence.ButlerDataRef`
547 Butler data reference corresponding to the image to be measured;
548 should have ``tract``, ``patch``, and ``filter`` keys.
549 exposure : `lsst.afw.image.Exposure`
550 The image to be measured (used only to obtain a WCS and bounding
551 box).
553 Returns
554 -------
555 referencs : `lsst.afw.table.SourceCatalog`
556 Catalog of sources that overlap the exposure
558 Notes
559 -----
560 The returned catalog is sorted by ID and guarantees that all included
561 children have their parent included and that all Footprints are valid.
563 All work is delegated to the references subtask; see
564 :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask`
565 for information about the default behavior.
566 """
567 references = lsst.afw.table.SourceCatalog(self.references.schema)
568 badParents = set()
569 unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs())
570 for record in unfiltered:
571 if record.getFootprint() is None or record.getFootprint().getArea() == 0:
572 if record.getParent() != 0:
573 self.log.warn("Skipping reference %s (child of %s) with bad Footprint",
574 record.getId(), record.getParent())
575 else:
576 self.log.warn("Skipping reference parent %s with bad Footprint", record.getId())
577 badParents.add(record.getId())
578 elif record.getParent() not in badParents:
579 references.append(record)
580 # catalog must be sorted by parent ID for lsst.afw.table.getChildren to work
581 references.sort(lsst.afw.table.SourceTable.getParentKey())
582 return references
584 def attachFootprints(self, sources, refCat, exposure, refWcs):
585 r"""Attach footprints to blank sources prior to measurements.
587 Notes
588 -----
589 `~lsst.afw.detection.Footprint`\ s for forced photometry must be in the
590 pixel coordinate system of the image being measured, while the actual
591 detections may start out in a different coordinate system.
593 Subclasses of this class must implement this method to define how
594 those `~lsst.afw.detection.Footprint`\ s should be generated.
596 This default implementation transforms the
597 `~lsst.afw.detection.Footprint`\ s from the reference catalog from the
598 reference WCS to the exposure's WcS, which downgrades
599 `lsst.afw.detection.heavyFootprint.HeavyFootprint`\ s into regular
600 `~lsst.afw.detection.Footprint`\ s, destroying deblend information.
601 """
602 return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs)
604 def getExposure(self, dataRef):
605 """Read input exposure for measurement.
607 Parameters
608 ----------
609 dataRef : `lsst.daf.persistence.ButlerDataRef`
610 Butler data reference.
611 """
612 exposure = dataRef.get(self.dataPrefix + "calexp", immediate=True)
614 if self.config.doApplyExternalPhotoCalib:
615 source = f"{self.config.externalPhotoCalibName}_photoCalib"
616 self.log.info("Applying external photoCalib from %s", source)
617 photoCalib = dataRef.get(source)
618 exposure.setPhotoCalib(photoCalib) # No need for calibrateImage; having the photoCalib suffices
620 if self.config.doApplyExternalSkyWcs:
621 source = f"{self.config.externalSkyWcsName}_wcs"
622 self.log.info("Applying external skyWcs from %s", source)
623 skyWcs = dataRef.get(source)
624 exposure.setWcs(skyWcs)
626 if self.config.doApplySkyCorr:
627 self.log.info("Apply sky correction")
628 skyCorr = dataRef.get("skyCorr")
629 exposure.maskedImage -= skyCorr.getImage()
631 return exposure
633 def writeOutput(self, dataRef, sources):
634 """Write forced source table
636 Parameters
637 ----------
638 dataRef : `lsst.daf.persistence.ButlerDataRef`
639 Butler data reference. The forced_src dataset (with
640 self.dataPrefix prepended) is all that will be modified.
641 sources : `lsst.afw.table.SourceCatalog`
642 Catalog of sources to save.
643 """
644 dataRef.put(sources, self.dataPrefix + "forced_src", flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS)
646 def getSchemaCatalogs(self):
647 """The schema catalogs that will be used by this task.
649 Returns
650 -------
651 schemaCatalogs : `dict`
652 Dictionary mapping dataset type to schema catalog.
654 Notes
655 -----
656 There is only one schema for each type of forced measurement. The
657 dataset type for this measurement is defined in the mapper.
658 """
659 catalog = lsst.afw.table.SourceCatalog(self.measurement.schema)
660 catalog.getTable().setMetadata(self.measurement.algMetadata)
661 datasetType = self.dataPrefix + "forced_src"
662 return {datasetType: catalog}
664 def _getConfigName(self):
665 # Documented in superclass.
666 return self.dataPrefix + "forcedPhotCcd_config"
668 def _getMetadataName(self):
669 # Documented in superclass
670 return self.dataPrefix + "forcedPhotCcd_metadata"
672 @classmethod
673 def _makeArgumentParser(cls):
674 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
675 parser.add_id_argument("--id", "forced_src", help="data ID with raw CCD keys [+ tract optionally], "
676 "e.g. --id visit=12345 ccd=1,2 [tract=0]",
677 ContainerClass=PerTractCcdDataIdContainer)
678 return parser