LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
getTemplate.py
Go to the documentation of this file.
1# This file is part of ip_diffim.
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/>.
21import numpy as np
22
23import lsst.afw.image as afwImage
24import lsst.geom as geom
25import lsst.afw.geom as afwGeom
26import lsst.afw.table as afwTable
27import lsst.afw.math as afwMath
28import lsst.pex.config as pexConfig
29import lsst.pipe.base as pipeBase
30from lsst.skymap import BaseSkyMap
31from lsst.daf.butler import DeferredDatasetHandle
32from lsst.ip.diffim.dcrModel import DcrModel
33from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig
34
35__all__ = ["GetCoaddAsTemplateTask", "GetCoaddAsTemplateConfig",
36 "GetTemplateTask", "GetTemplateConfig",
37 "GetDcrTemplateTask", "GetDcrTemplateConfig",
38 "GetMultiTractCoaddTemplateTask", "GetMultiTractCoaddTemplateConfig"]
39
40
41class GetCoaddAsTemplateConfig(pexConfig.Config):
42 templateBorderSize = pexConfig.Field(
43 dtype=int,
44 default=20,
45 doc="Number of pixels to grow the requested template image to account for warping"
46 )
47 coaddName = pexConfig.Field(
48 doc="coadd name: typically one of 'deep', 'goodSeeing', or 'dcr'",
49 dtype=str,
50 default="deep",
51 )
52 warpType = pexConfig.Field(
53 doc="Warp type of the coadd template: one of 'direct' or 'psfMatched'",
54 dtype=str,
55 default="direct",
56 )
57
58
59class GetCoaddAsTemplateTask(pipeBase.Task):
60 """Subtask to retrieve coadd for use as an image difference template.
61
62 This is the default getTemplate Task to be run as a subtask by
63 ``pipe.tasks.ImageDifferenceTask``.
64
65 Notes
66 -----
67 From the given skymap, the closest tract is selected; multiple tracts are
68 not supported. The assembled template inherits the WCS of the selected
69 skymap tract and the resolution of the template exposures. Overlapping box
70 regions of the input template patches are pixel by pixel copied into the
71 assembled template image. There is no warping or pixel resampling.
72
73 Pixels with no overlap of any available input patches are set to ``nan``
74 value and ``NO_DATA`` flagged.
75 """
76
77 ConfigClass = GetCoaddAsTemplateConfig
78 _DefaultName = "GetCoaddAsTemplateTask"
79
80 def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs):
81 """Gen3 task entry point. Retrieve and mosaic a template coadd exposure
82 that overlaps the science exposure.
83
84 Parameters
85 ----------
86 exposure : `lsst.afw.image.Exposure`
87 The science exposure to define the sky region of the template
88 coadd.
89 butlerQC : `lsst.pipe.base.ButlerQuantumContext`
90 Butler like object that supports getting data by DatasetRef.
91 skyMapRef : `lsst.daf.butler.DatasetRef`
92 Reference to SkyMap object that corresponds to the template coadd.
93 coaddExposureRefs : iterable of `lsst.daf.butler.DeferredDatasetRef`
94 Iterable of references to the available template coadd patches.
95
96 Returns
97 -------
98 result : `lsst.pipe.base.Struct`
99 A struct with attibutes:
100
101 ``exposure``
102 Template coadd exposure assembled out of patches
103 (`lsst.afw.image.ExposureF`).
104 ``sources``
105 Always `None` for this subtask.
106
107 """
108 self.log.warning("GetCoaddAsTemplateTask is deprecated. Use GetTemplateTask instead.")
109 skyMap = butlerQC.get(skyMapRef)
110 coaddExposureRefs = butlerQC.get(coaddExposureRefs)
111 tracts = [ref.dataId['tract'] for ref in coaddExposureRefs]
112 if tracts.count(tracts[0]) == len(tracts):
113 tractInfo = skyMap[tracts[0]]
114 else:
115 raise RuntimeError("Templates constructed from multiple Tracts not supported by this task. "
116 "Use GetTemplateTask instead.")
117
118 detectorWcs = exposure.getWcs()
119 if detectorWcs is None:
120 templateExposure = None
121 pixGood = 0
122 self.log.info("Exposure has no WCS, so cannot create associated template.")
123 else:
124 detectorBBox = exposure.getBBox()
125 detectorCorners = detectorWcs.pixelToSky(geom.Box2D(detectorBBox).getCorners())
126 validPolygon = exposure.getInfo().getValidPolygon()
127 detectorPolygon = validPolygon if validPolygon else geom.Box2D(detectorBBox)
128
129 availableCoaddRefs = dict()
130 overlappingArea = 0
131 for coaddRef in coaddExposureRefs:
132 dataId = coaddRef.dataId
133 patchWcs = skyMap[dataId['tract']].getWcs()
134 patchBBox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox()
135 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
136 patchPolygon = afwGeom.Polygon(detectorWcs.skyToPixel(patchCorners))
137 if patchPolygon.intersection(detectorPolygon):
138 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
139 if self.config.coaddName == 'dcr':
140 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s",
141 dataId['tract'], dataId['patch'], dataId['subfilter'])
142 if dataId['patch'] in availableCoaddRefs:
143 availableCoaddRefs[dataId['patch']].append(coaddRef)
144 else:
145 availableCoaddRefs[dataId['patch']] = [coaddRef, ]
146 else:
147 self.log.info("Using template input tract=%s, patch=%s",
148 dataId['tract'], dataId['patch'])
149 availableCoaddRefs[dataId['patch']] = coaddRef
150
151 if overlappingArea == 0:
152 templateExposure = None
153 pixGood = 0
154 self.log.warning("No overlapping template patches found")
155 else:
156 patchList = [tractInfo[patch] for patch in availableCoaddRefs.keys()]
157 templateExposure = self.run(tractInfo, patchList, detectorCorners, availableCoaddRefs,
158 visitInfo=exposure.getInfo().getVisitInfo())
159 # Count the number of pixels with the NO_DATA mask bit set.
160 # Counting NaN pixels is insufficient because pixels without
161 # data are often intepolated over.
162 pixNoData = np.count_nonzero(templateExposure.mask.array
163 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
164 pixGood = templateExposure.getBBox().getArea() - pixNoData
165 self.log.info("template has %d good pixels (%.1f%%)", pixGood,
166 100*pixGood/templateExposure.getBBox().getArea())
167 return pipeBase.Struct(exposure=templateExposure, sources=None, area=pixGood)
168
169 def getOverlapPatchList(self, exposure, skyMap):
170 """Select the relevant tract and its patches that overlap with the
171 science exposure.
172
173 Parameters
174 ----------
175 exposure : `lsst.afw.image.Exposure`
176 The science exposure to define the sky region of the template
177 coadd.
178
179 skyMap : `lsst.skymap.BaseSkyMap`
180 SkyMap object that corresponds to the template coadd.
181
182 Returns
183 -------
184 result : `tuple` of
185 - ``tractInfo`` : `lsst.skymap.TractInfo`
186 The selected tract.
187 - ``patchList`` : `list` [`lsst.skymap.PatchInfo`]
188 List of all overlap patches of the selected tract.
189 - ``skyCorners`` : `list` [`lsst.geom.SpherePoint`]
190 Corners of the exposure in the sky in the order given by
191 `lsst.geom.Box2D.getCorners`.
192 """
193 expWcs = exposure.getWcs()
194 expBoxD = geom.Box2D(exposure.getBBox())
195 expBoxD.grow(self.config.templateBorderSize)
196 ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter())
197 tractInfo = skyMap.findTract(ctrSkyPos)
198 self.log.info("Using skyMap tract %s", tractInfo.getId())
199 skyCorners = [expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners()]
200 patchList = tractInfo.findPatchList(skyCorners)
201
202 if not patchList:
203 raise RuntimeError("No suitable tract found")
204
205 self.log.info("Assembling %d coadd patches", len(patchList))
206 self.log.info("exposure dimensions=%s", exposure.getDimensions())
207
208 return (tractInfo, patchList, skyCorners)
209
210 def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs,
211 sensorRef=None, visitInfo=None):
212 """Determination of exposure dimensions and copying of pixels from
213 overlapping patch regions.
214
215 Parameters
216 ----------
217 skyMap : `lsst.skymap.BaseSkyMap`
218 SkyMap object that corresponds to the template coadd.
219 tractInfo : `lsst.skymap.TractInfo`
220 The selected tract.
221 patchList : iterable of `lsst.skymap.patchInfo.PatchInfo`
222 Patches to consider for making the template exposure.
223 skyCorners : `list` [`lsst.geom.SpherePoint`]
224 Sky corner coordinates to be covered by the template exposure.
225 availableCoaddRefs : `dict` [`int`]
226 Dictionary of spatially relevant retrieved coadd patches,
227 indexed by their sequential patch number. Values are
228 `lsst.daf.butler.DeferredDatasetHandle` and ``.get()`` is called.
229 sensorRef : `None`
230 Must always be `None`. Gen2 parameters are no longer used.
231 visitInfo : `lsst.afw.image.VisitInfo`
232 VisitInfo to make dcr model.
233
234 Returns
235 -------
236 templateExposure : `lsst.afw.image.ExposureF`
237 The created template exposure.
238 """
239 if sensorRef is not None:
240 raise ValueError("sensorRef parameter is a Gen2 parameter that is no longer usable."
241 " Please move to Gen3 middleware.")
242 coaddWcs = tractInfo.getWcs()
243
244 # compute coadd bbox
245 coaddBBox = geom.Box2D()
246 for skyPos in skyCorners:
247 coaddBBox.include(coaddWcs.skyToPixel(skyPos))
248 coaddBBox = geom.Box2I(coaddBBox)
249 self.log.info("coadd dimensions=%s", coaddBBox.getDimensions())
250
251 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
252 coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
253 nPatchesFound = 0
254 coaddFilterLabel = None
255 coaddPsf = None
256 coaddPhotoCalib = None
257 for patchInfo in patchList:
258 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo)
259 patchSubBBox = patchInfo.getOuterBBox()
260 patchSubBBox.clip(coaddBBox)
261 if patchNumber not in availableCoaddRefs:
262 self.log.warning("skip patch=%d; patch does not exist for this coadd", patchNumber)
263 continue
264 if patchSubBBox.isEmpty():
265 if isinstance(availableCoaddRefs[patchNumber], DeferredDatasetHandle):
266 tract = availableCoaddRefs[patchNumber].dataId['tract']
267 else:
268 tract = availableCoaddRefs[patchNumber]['tract']
269 self.log.info("skip tract=%d patch=%d; no overlapping pixels", tract, patchNumber)
270 continue
271
272 if self.config.coaddName == 'dcr':
273 patchInnerBBox = patchInfo.getInnerBBox()
274 patchInnerBBox.clip(coaddBBox)
275 if np.min(patchInnerBBox.getDimensions()) <= 2*self.config.templateBorderSize:
276 self.log.info("skip tract=%(tract)s, patch=%(patch)s; too few pixels.",
277 availableCoaddRefs[patchNumber])
278 continue
279 self.log.info("Constructing DCR-matched template for patch %s",
280 availableCoaddRefs[patchNumber])
281
282 dcrModel = DcrModel.fromQuantum(availableCoaddRefs[patchNumber],
283 self.config.effectiveWavelength,
284 self.config.bandwidth)
285 # The edge pixels of the DcrCoadd may contain artifacts due to
286 # missing data. Each patch has significant overlap, and the
287 # contaminated edge pixels in a new patch will overwrite good
288 # pixels in the overlap region from previous patches.
289 # Shrink the BBox to remove the contaminated pixels,
290 # but make sure it is only the overlap region that is reduced.
291 dcrBBox = geom.Box2I(patchSubBBox)
292 dcrBBox.grow(-self.config.templateBorderSize)
293 dcrBBox.include(patchInnerBBox)
294 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox,
295 visitInfo=visitInfo)
296 else:
297 coaddPatch = availableCoaddRefs[patchNumber].get()
298
299 nPatchesFound += 1
300
301 # Gen2 get() seems to clip based on bbox kwarg but we removed bbox
302 # calculation from caller code. Gen3 also does not do this.
303 overlapBox = coaddPatch.getBBox()
304 overlapBox.clip(coaddBBox)
305 coaddExposure.maskedImage.assign(coaddPatch.maskedImage[overlapBox], overlapBox)
306
307 if coaddFilterLabel is None:
308 coaddFilterLabel = coaddPatch.getFilter()
309
310 # Retrieve the PSF for this coadd tract, if not already retrieved.
311 if coaddPsf is None and coaddPatch.hasPsf():
312 coaddPsf = coaddPatch.getPsf()
313
314 # Retrieve the calibration for this coadd tract, if not already
315 # retrieved>
316 if coaddPhotoCalib is None:
317 coaddPhotoCalib = coaddPatch.getPhotoCalib()
318
319 if coaddPhotoCalib is None:
320 raise RuntimeError("No coadd PhotoCalib found!")
321 if nPatchesFound == 0:
322 raise RuntimeError("No patches found!")
323 if coaddPsf is None:
324 raise RuntimeError("No coadd Psf found!")
325
326 coaddExposure.setPhotoCalib(coaddPhotoCalib)
327 coaddExposure.setPsf(coaddPsf)
328 coaddExposure.setFilter(coaddFilterLabel)
329 return coaddExposure
330
332 """Return coadd name for given task config
333
334 Returns
335 -------
336 CoaddDatasetName : `string`
337
338 TODO: This nearly duplicates a method in CoaddBaseTask (DM-11985)
339 """
340 warpType = self.config.warpType
341 suffix = "" if warpType == "direct" else warpType[0].upper() + warpType[1:]
342 return self.config.coaddName + "Coadd" + suffix
343
344
345class GetTemplateConnections(pipeBase.PipelineTaskConnections,
346 dimensions=("instrument", "visit", "detector", "skymap"),
347 defaultTemplates={"coaddName": "goodSeeing",
348 "warpTypeSuffix": "",
349 "fakesType": ""}):
350 bbox = pipeBase.connectionTypes.Input(
351 doc="BBoxes of calexp used determine geometry of output template",
352 name="{fakesType}calexp.bbox",
353 storageClass="Box2I",
354 dimensions=("instrument", "visit", "detector"),
355 )
356 wcs = pipeBase.connectionTypes.Input(
357 doc="WCS of the calexp that we want to fetch the template for",
358 name="{fakesType}calexp.wcs",
359 storageClass="Wcs",
360 dimensions=("instrument", "visit", "detector"),
361 )
362 skyMap = pipeBase.connectionTypes.Input(
363 doc="Input definition of geometry/bbox and projection/wcs for template exposures",
364 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
365 dimensions=("skymap", ),
366 storageClass="SkyMap",
367 )
368 # TODO DM-31292: Add option to use global external wcs from jointcal
369 # Needed for DRP HSC
370 coaddExposures = pipeBase.connectionTypes.Input(
371 doc="Input template to match and subtract from the exposure",
372 dimensions=("tract", "patch", "skymap", "band"),
373 storageClass="ExposureF",
374 name="{fakesType}{coaddName}Coadd{warpTypeSuffix}",
375 multiple=True,
376 deferLoad=True
377 )
378 template = pipeBase.connectionTypes.Output(
379 doc="Warped template used to create `subtractedExposure`.",
380 dimensions=("instrument", "visit", "detector"),
381 storageClass="ExposureF",
382 name="{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
383 )
384
385
386class GetTemplateConfig(pipeBase.PipelineTaskConfig,
387 pipelineConnections=GetTemplateConnections):
388 templateBorderSize = pexConfig.Field(
389 dtype=int,
390 default=20,
391 doc="Number of pixels to grow the requested template image to account for warping"
392 )
393 warp = pexConfig.ConfigField(
394 dtype=afwMath.Warper.ConfigClass,
395 doc="warper configuration",
396 )
397 coaddPsf = pexConfig.ConfigField(
398 doc="Configuration for CoaddPsf",
399 dtype=CoaddPsfConfig,
400 )
401
402 def setDefaults(self):
403 self.warp.warpingKernelName = 'lanczos5'
404 self.coaddPsf.warpingKernelName = 'lanczos5'
405
406
407class GetTemplateTask(pipeBase.PipelineTask):
408 ConfigClass = GetTemplateConfig
409 _DefaultName = "getTemplate"
410
411 def __init__(self, *args, **kwargs):
412 super().__init__(*args, **kwargs)
413 self.warper = afwMath.Warper.fromConfig(self.config.warp)
414
415 def runQuantum(self, butlerQC, inputRefs, outputRefs):
416 # Read in all inputs.
417 inputs = butlerQC.get(inputRefs)
418 results = self.getOverlappingExposures(inputs)
419 inputs["coaddExposures"] = results.coaddExposures
420 inputs["dataIds"] = results.dataIds
421 outputs = self.run(**inputs)
422 butlerQC.put(outputs, outputRefs)
423
424 def getOverlappingExposures(self, inputs):
425 """Return lists of coadds and their corresponding dataIds that overlap
426 the detector.
427
428 The spatial index in the registry has generous padding and often
429 supplies patches near, but not directly overlapping the detector.
430 Filters inputs so that we don't have to read in all input coadds.
431
432 Parameters
433 ----------
434 inputs : `dict` of task Inputs, containing:
435 - coaddExposureRefs : `list`
436 [`lsst.daf.butler.DeferredDatasetHandle` of
438 Data references to exposures that might overlap the detector.
439 - bbox : `lsst.geom.Box2I`
440 Template Bounding box of the detector geometry onto which to
441 resample the coaddExposures.
442 - skyMap : `lsst.skymap.SkyMap`
443 Input definition of geometry/bbox and projection/wcs for
444 template exposures.
445 - wcs : `lsst.afw.geom.SkyWcs`
446 Template WCS onto which to resample the coaddExposures.
447
448 Returns
449 -------
450 result : `lsst.pipe.base.Struct`
451 A struct with attributes:
452
453 ``coaddExposures``
454 List of Coadd exposures that overlap the detector (`list`
456 ``dataIds``
457 List of data IDs of the coadd exposures that overlap the
458 detector (`list` [`lsst.daf.butler.DataCoordinate`]).
459
460 Raises
461 ------
462 NoWorkFound
463 Raised if no patches overlap the input detector bbox.
464 """
465 # Check that the patches actually overlap the detector
466 # Exposure's validPolygon would be more accurate
467 detectorPolygon = geom.Box2D(inputs['bbox'])
468 overlappingArea = 0
469 coaddExposureList = []
470 dataIds = []
471 for coaddRef in inputs['coaddExposures']:
472 dataId = coaddRef.dataId
473 patchWcs = inputs['skyMap'][dataId['tract']].getWcs()
474 patchBBox = inputs['skyMap'][dataId['tract']][dataId['patch']].getOuterBBox()
475 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
476 inputsWcs = inputs['wcs']
477 if inputsWcs is not None:
478 patchPolygon = afwGeom.Polygon(inputsWcs.skyToPixel(patchCorners))
479 if patchPolygon.intersection(detectorPolygon):
480 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
481 self.log.info("Using template input tract=%s, patch=%s" %
482 (dataId['tract'], dataId['patch']))
483 coaddExposureList.append(coaddRef.get())
484 dataIds.append(dataId)
485 else:
486 self.log.info("Exposure has no WCS, so cannot create associated template.")
487
488 if not overlappingArea:
489 raise pipeBase.NoWorkFound('No patches overlap detector')
490
491 return pipeBase.Struct(coaddExposures=coaddExposureList,
492 dataIds=dataIds)
493
494 def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs):
495 """Warp coadds from multiple tracts to form a template for image diff.
496
497 Where the tracts overlap, the resulting template image is averaged.
498 The PSF on the template is created by combining the CoaddPsf on each
499 template image into a meta-CoaddPsf.
500
501 Parameters
502 ----------
503 coaddExposures : `list` [`lsst.afw.image.Exposure`]
504 Coadds to be mosaicked.
505 bbox : `lsst.geom.Box2I`
506 Template Bounding box of the detector geometry onto which to
507 resample the ``coaddExposures``.
509 Template WCS onto which to resample the ``coaddExposures``.
510 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
511 Record of the tract and patch of each coaddExposure.
512 **kwargs
513 Any additional keyword parameters.
514
515 Returns
516 -------
517 result : `lsst.pipe.base.Struct`
518 A struct with attributes:
519
520 ``template``
521 A template coadd exposure assembled out of patches
522 (`lsst.afw.image.ExposureF`).
523 """
524 # Table for CoaddPSF
525 tractsSchema = afwTable.ExposureTable.makeMinimalSchema()
526 tractKey = tractsSchema.addField('tract', type=np.int32, doc='Which tract')
527 patchKey = tractsSchema.addField('patch', type=np.int32, doc='Which patch')
528 weightKey = tractsSchema.addField('weight', type=float, doc='Weight for each tract, should be 1')
529 tractsCatalog = afwTable.ExposureCatalog(tractsSchema)
530
531 finalWcs = wcs
532 bbox.grow(self.config.templateBorderSize)
533 finalBBox = bbox
534
535 nPatchesFound = 0
536 maskedImageList = []
537 weightList = []
538
539 for coaddExposure, dataId in zip(coaddExposures, dataIds):
540
541 # warp to detector WCS
542 warped = self.warper.warpExposure(finalWcs, coaddExposure, maxBBox=finalBBox)
543
544 # Check if warped image is viable
545 if not np.any(np.isfinite(warped.image.array)):
546 self.log.info("No overlap for warped %s. Skipping" % dataId)
547 continue
548
549 exp = afwImage.ExposureF(finalBBox, finalWcs)
550 exp.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
551 exp.maskedImage.assign(warped.maskedImage, warped.getBBox())
552
553 maskedImageList.append(exp.maskedImage)
554 weightList.append(1)
555 record = tractsCatalog.addNew()
556 record.setPsf(coaddExposure.getPsf())
557 record.setWcs(coaddExposure.getWcs())
558 record.setPhotoCalib(coaddExposure.getPhotoCalib())
559 record.setBBox(coaddExposure.getBBox())
560 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(coaddExposure.getBBox()).getCorners()))
561 record.set(tractKey, dataId['tract'])
562 record.set(patchKey, dataId['patch'])
563 record.set(weightKey, 1.)
564 nPatchesFound += 1
565
566 if nPatchesFound == 0:
567 raise pipeBase.NoWorkFound("No patches found to overlap detector")
568
569 # Combine images from individual patches together
570 statsFlags = afwMath.stringToStatisticsProperty('MEAN')
571 statsCtrl = afwMath.StatisticsControl()
572 statsCtrl.setNanSafe(True)
573 statsCtrl.setWeighted(True)
574 statsCtrl.setCalcErrorMosaicMode(True)
575
576 templateExposure = afwImage.ExposureF(finalBBox, finalWcs)
577 templateExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
578 xy0 = templateExposure.getXY0()
579 # Do not mask any values
580 templateExposure.maskedImage = afwMath.statisticsStack(maskedImageList, statsFlags, statsCtrl,
581 weightList, clipped=0, maskMap=[])
582 templateExposure.maskedImage.setXY0(xy0)
583
584 # CoaddPsf centroid not only must overlap image, but must overlap the
585 # part of image with data. Use centroid of region with data.
586 boolmask = templateExposure.mask.array & templateExposure.mask.getPlaneBitMask('NO_DATA') == 0
587 maskx = afwImage.makeMaskFromArray(boolmask.astype(afwImage.MaskPixel))
588 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
589
590 ctrl = self.config.coaddPsf.makeControl()
591 coaddPsf = CoaddPsf(tractsCatalog, finalWcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
592 if coaddPsf is None:
593 raise RuntimeError("CoaddPsf could not be constructed")
594
595 templateExposure.setPsf(coaddPsf)
596 templateExposure.setFilter(coaddExposure.getFilter())
597 templateExposure.setPhotoCalib(coaddExposure.getPhotoCalib())
598 return pipeBase.Struct(template=templateExposure)
599
600
602 dimensions=("instrument", "visit", "detector", "skymap"),
603 defaultTemplates={"coaddName": "dcr",
604 "warpTypeSuffix": "",
605 "fakesType": ""}):
606 visitInfo = pipeBase.connectionTypes.Input(
607 doc="VisitInfo of calexp used to determine observing conditions.",
608 name="{fakesType}calexp.visitInfo",
609 storageClass="VisitInfo",
610 dimensions=("instrument", "visit", "detector"),
611 )
612 dcrCoadds = pipeBase.connectionTypes.Input(
613 doc="Input DCR template to match and subtract from the exposure",
614 name="{fakesType}dcrCoadd{warpTypeSuffix}",
615 storageClass="ExposureF",
616 dimensions=("tract", "patch", "skymap", "band", "subfilter"),
617 multiple=True,
618 deferLoad=True
619 )
620
621 def __init__(self, *, config=None):
622 super().__init__(config=config)
623 self.inputs.remove("coaddExposures")
624
625
626class GetDcrTemplateConfig(GetTemplateConfig,
627 pipelineConnections=GetDcrTemplateConnections):
628 numSubfilters = pexConfig.Field(
629 doc="Number of subfilters in the DcrCoadd.",
630 dtype=int,
631 default=3,
632 )
633 effectiveWavelength = pexConfig.Field(
634 doc="Effective wavelength of the filter.",
635 optional=False,
636 dtype=float,
637 )
638 bandwidth = pexConfig.Field(
639 doc="Bandwidth of the physical filter.",
640 optional=False,
641 dtype=float,
642 )
643
644 def validate(self):
645 if self.effectiveWavelength is None or self.bandwidth is None:
646 raise ValueError("The effective wavelength and bandwidth of the physical filter "
647 "must be set in the getTemplate config for DCR coadds. "
648 "Required until transmission curves are used in DM-13668.")
649
650
651class GetDcrTemplateTask(GetTemplateTask):
652 ConfigClass = GetDcrTemplateConfig
653 _DefaultName = "getDcrTemplate"
654
655 def getOverlappingExposures(self, inputs):
656 """Return lists of coadds and their corresponding dataIds that overlap
657 the detector.
658
659 The spatial index in the registry has generous padding and often
660 supplies patches near, but not directly overlapping the detector.
661 Filters inputs so that we don't have to read in all input coadds.
662
663 Parameters
664 ----------
665 inputs : `dict` of task Inputs, containing:
666 - coaddExposureRefs : `list`
667 [`lsst.daf.butler.DeferredDatasetHandle` of
669 Data references to exposures that might overlap the detector.
670 - bbox : `lsst.geom.Box2I`
671 Template Bounding box of the detector geometry onto which to
672 resample the coaddExposures.
673 - skyMap : `lsst.skymap.SkyMap`
674 Input definition of geometry/bbox and projection/wcs for
675 template exposures.
676 - wcs : `lsst.afw.geom.SkyWcs`
677 Template WCS onto which to resample the coaddExposures.
678 - visitInfo : `lsst.afw.image.VisitInfo`
679 Metadata for the science image.
680
681 Returns
682 -------
683 result : `lsst.pipe.base.Struct`
684 A struct with attibutes:
685
686 ``coaddExposures``
687 Coadd exposures that overlap the detector (`list`
689 ``dataIds``
690 Data IDs of the coadd exposures that overlap the detector
691 (`list` [`lsst.daf.butler.DataCoordinate`]).
692
693 Raises
694 ------
695 NoWorkFound
696 Raised if no patches overlatp the input detector bbox.
697 """
698 # Check that the patches actually overlap the detector
699 # Exposure's validPolygon would be more accurate
700 detectorPolygon = geom.Box2D(inputs["bbox"])
701 overlappingArea = 0
702 coaddExposureRefList = []
703 dataIds = []
704 patchList = dict()
705 for coaddRef in inputs["dcrCoadds"]:
706 dataId = coaddRef.dataId
707 patchWcs = inputs["skyMap"][dataId['tract']].getWcs()
708 patchBBox = inputs["skyMap"][dataId['tract']][dataId['patch']].getOuterBBox()
709 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
710 patchPolygon = afwGeom.Polygon(inputs["wcs"].skyToPixel(patchCorners))
711 if patchPolygon.intersection(detectorPolygon):
712 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
713 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s" %
714 (dataId['tract'], dataId['patch'], dataId["subfilter"]))
715 coaddExposureRefList.append(coaddRef)
716 if dataId['tract'] in patchList:
717 patchList[dataId['tract']].append(dataId['patch'])
718 else:
719 patchList[dataId['tract']] = [dataId['patch'], ]
720 dataIds.append(dataId)
721
722 if not overlappingArea:
723 raise pipeBase.NoWorkFound('No patches overlap detector')
724
725 self.checkPatchList(patchList)
726
727 coaddExposures = self.getDcrModel(patchList, inputs['dcrCoadds'], inputs['visitInfo'])
728 return pipeBase.Struct(coaddExposures=coaddExposures,
729 dataIds=dataIds)
730
731 def checkPatchList(self, patchList):
732 """Check that all of the DcrModel subfilters are present for each
733 patch.
734
735 Parameters
736 ----------
737 patchList : `dict`
738 Dict of the patches containing valid data for each tract.
739
740 Raises
741 ------
742 RuntimeError
743 If the number of exposures found for a patch does not match the
744 number of subfilters.
745 """
746 for tract in patchList:
747 for patch in set(patchList[tract]):
748 if patchList[tract].count(patch) != self.config.numSubfilters:
749 raise RuntimeError("Invalid number of DcrModel subfilters found: %d vs %d expected",
750 patchList[tract].count(patch), self.config.numSubfilters)
751
752 def getDcrModel(self, patchList, coaddRefs, visitInfo):
753 """Build DCR-matched coadds from a list of exposure references.
754
755 Parameters
756 ----------
757 patchList : `dict`
758 Dict of the patches containing valid data for each tract.
759 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
760 Data references to `~lsst.afw.image.Exposure` representing
761 DcrModels that overlap the detector.
762 visitInfo : `lsst.afw.image.VisitInfo`
763 Metadata for the science image.
764
765 Returns
766 -------
767 coaddExposureList : `list` [`lsst.afw.image.Exposure`]
768 Coadd exposures that overlap the detector.
769 """
770 coaddExposureList = []
771 for tract in patchList:
772 for patch in set(patchList[tract]):
773 coaddRefList = [coaddRef for coaddRef in coaddRefs
774 if _selectDataRef(coaddRef, tract, patch)]
775
776 dcrModel = DcrModel.fromQuantum(coaddRefList,
777 self.config.effectiveWavelength,
778 self.config.bandwidth,
779 self.config.numSubfilters)
780 coaddExposureList.append(dcrModel.buildMatchedExposure(visitInfo=visitInfo))
781 return coaddExposureList
782
783
784def _selectDataRef(coaddRef, tract, patch):
785 condition = (coaddRef.dataId['tract'] == tract) & (coaddRef.dataId['patch'] == patch)
786 return condition
787
788
789class GetMultiTractCoaddTemplateConfig(GetTemplateConfig):
790 pass
791
792
793class GetMultiTractCoaddTemplateTask(GetTemplateTask):
794 ConfigClass = GetMultiTractCoaddTemplateConfig
795 _DefaultName = "getMultiTractCoaddTemplate"
796
797 def __init__(self, *args, **kwargs):
798 super().__init__(*args, **kwargs)
799 self.log.warning("GetMultiTractCoaddTemplateTask is deprecated. Use GetTemplateTask instead.")
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Definition: SkyWcs.h:117
Cartesian polygons.
Definition: Polygon.h:59
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
Information about a single exposure of an imaging camera.
Definition: VisitInfo.h:68
Pass parameters to a Statistics object.
Definition: Statistics.h:83
Custom catalog class for ExposureRecord/Table.
Definition: Exposure.h:311
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
An integer coordinate rectangle.
Definition: Box.h:55
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs, sensorRef=None, visitInfo=None)
Definition: getTemplate.py:211
def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs)
Definition: getTemplate.py:80
CoaddPsf is the Psf derived to be used for non-PSF-matched Coadd images.
Definition: CoaddPsf.h:58
daf::base::PropertySet * set
Definition: fits.cc:927
std::shared_ptr< lsst::afw::image::Image< PixelT > > statisticsStack(std::vector< std::shared_ptr< lsst::afw::image::Image< PixelT > > > &images, Property flags, StatisticsControl const &sctrl=StatisticsControl(), std::vector< lsst::afw::image::VariancePixel > const &wvector=std::vector< lsst::afw::image::VariancePixel >(0))
A function to compute some statistics of a stack of Images.
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
Definition: Statistics.cc:762
def checkPatchList(self, patchList)
Definition: getTemplate.py:731
def getDcrModel(self, patchList, coaddRefs, visitInfo)
Definition: getTemplate.py:752