Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+849f694866,g1fd858c14a+7a7b9dd5ed,g2c84ff76c0+5cb23283cf,g30358e5240+f0e04ebe90,g35bb328faa+fcb1d3bbc8,g436fd98eb5+bdc6fcdd04,g4af146b050+742274f7cd,g4d2262a081+9d5bd0394b,g4e0f332c67+cb09b8a5b6,g53246c7159+fcb1d3bbc8,g5a012ec0e7+477f9c599b,g60b5630c4e+bdc6fcdd04,g67b6fd64d1+2218407a0c,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g7b71ed6315+fcb1d3bbc8,g87b7deb4dc+777438113c,g8852436030+ebf28f0d95,g89139ef638+2218407a0c,g9125e01d80+fcb1d3bbc8,g989de1cb63+2218407a0c,g9f33ca652e+42fb53f4c8,g9f7030ddb1+11b9b6f027,ga2b97cdc51+bdc6fcdd04,gab72ac2889+bdc6fcdd04,gabe3b4be73+1e0a283bba,gabf8522325+3210f02652,gb1101e3267+9c79701da9,gb58c049af0+f03b321e39,gb89ab40317+2218407a0c,gcf25f946ba+ebf28f0d95,gd6cbbdb0b4+e8f9c9c900,gd9a9a58781+fcb1d3bbc8,gde0f65d7ad+47bbabaf80,gded526ad44+8c3210761e,ge278dab8ac+3ef3db156b,ge410e46f29+2218407a0c,gf67bdafdda+2218407a0c,v29.0.0.rc3
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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 collections
22
23import numpy as np
24from deprecated.sphinx import deprecated
25
26import lsst.afw.image as afwImage
27import lsst.geom as geom
28import lsst.afw.geom as afwGeom
29import lsst.afw.table as afwTable
30from lsst.afw.math._warper import computeWarpedBBox
31import lsst.afw.math as afwMath
32import lsst.pex.config as pexConfig
33import lsst.pipe.base as pipeBase
34from lsst.skymap import BaseSkyMap
35from lsst.ip.diffim.dcrModel import DcrModel
36from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig
37from lsst.utils.timer import timeMethod
38
39__all__ = ["GetTemplateTask", "GetTemplateConfig",
40 "GetDcrTemplateTask", "GetDcrTemplateConfig"]
41
42
43class GetTemplateConnections(pipeBase.PipelineTaskConnections,
44 dimensions=("instrument", "visit", "detector", "skymap"),
45 defaultTemplates={"coaddName": "goodSeeing",
46 "warpTypeSuffix": "",
47 "fakesType": ""}):
48 bbox = pipeBase.connectionTypes.Input(
49 doc="Bounding box of exposure to determine the geometry of the output template.",
50 name="{fakesType}calexp.bbox",
51 storageClass="Box2I",
52 dimensions=("instrument", "visit", "detector"),
53 )
54 wcs = pipeBase.connectionTypes.Input(
55 doc="WCS of the exposure that we will construct the template for.",
56 name="{fakesType}calexp.wcs",
57 storageClass="Wcs",
58 dimensions=("instrument", "visit", "detector"),
59 )
60 skyMap = pipeBase.connectionTypes.Input(
61 doc="Geometry of the tracts and patches that the coadds are defined on.",
62 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
63 dimensions=("skymap", ),
64 storageClass="SkyMap",
65 )
66 coaddExposures = pipeBase.connectionTypes.Input(
67 doc="Coadds that may overlap the desired region, as possible inputs to the template."
68 " Will be restricted to those that directly overlap the projected bounding box.",
69 dimensions=("tract", "patch", "skymap", "band"),
70 storageClass="ExposureF",
71 name="{fakesType}{coaddName}Coadd{warpTypeSuffix}",
72 multiple=True,
73 deferLoad=True,
74 deferGraphConstraint=True
75 )
76
77 template = pipeBase.connectionTypes.Output(
78 doc="Warped template, pixel matched to the bounding box and WCS.",
79 dimensions=("instrument", "visit", "detector"),
80 storageClass="ExposureF",
81 name="{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
82 )
83
84
85class GetTemplateConfig(pipeBase.PipelineTaskConfig,
86 pipelineConnections=GetTemplateConnections):
87 templateBorderSize = pexConfig.Field(
88 dtype=int,
89 default=20,
90 doc="Number of pixels to grow the requested template image to account for warping"
91 )
92 warp = pexConfig.ConfigField(
93 dtype=afwMath.Warper.ConfigClass,
94 doc="warper configuration",
95 )
96 coaddPsf = pexConfig.ConfigField(
97 doc="Configuration for CoaddPsf",
98 dtype=CoaddPsfConfig,
99 )
100
101 def setDefaults(self):
102 # Use a smaller cache: per SeparableKernel.computeCache, this should
103 # give a warping error of a fraction of a count (these must match).
104 self.warp.cacheSize = 100000
105 self.coaddPsf.cacheSize = self.warp.cacheSize
106 # The WCS for LSST should be smoothly varying, so we can use a longer
107 # interpolation length for WCS evaluations.
108 self.warp.interpLength = 100
109 self.warp.warpingKernelName = 'lanczos3'
110 self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
111
112
113class GetTemplateTask(pipeBase.PipelineTask):
114 ConfigClass = GetTemplateConfig
115 _DefaultName = "getTemplate"
116
117 def __init__(self, *args, **kwargs):
118 super().__init__(*args, **kwargs)
119 self.warper = afwMath.Warper.fromConfig(self.config.warp)
120 self.schema = afwTable.ExposureTable.makeMinimalSchema()
121 self.schema.addField('tract', type=np.int32, doc='Which tract this exposure came from.')
122 self.schema.addField('patch', type=np.int32, doc='Which patch in the tract this exposure came from.')
123 self.schema.addField('weight', type=float,
124 doc='Weight for each exposure, used to make the CoaddPsf; should always be 1.')
125
126 def runQuantum(self, butlerQC, inputRefs, outputRefs):
127 inputs = butlerQC.get(inputRefs)
128 bbox = inputs.pop("bbox")
129 wcs = inputs.pop("wcs")
130 coaddExposures = inputs.pop('coaddExposures')
131 skymap = inputs.pop("skyMap")
132
133 # This should not happen with a properly configured execution context.
134 assert not inputs, "runQuantum got more inputs than expected"
135
136 results = self.getExposures(coaddExposures, bbox, skymap, wcs)
137 physical_filter = butlerQC.quantum.dataId["physical_filter"]
138 outputs = self.run(coaddExposureHandles=results.coaddExposures,
139 bbox=bbox,
140 wcs=wcs,
141 dataIds=results.dataIds,
142 physical_filter=physical_filter)
143 butlerQC.put(outputs, outputRefs)
144
145 @deprecated(reason="Replaced by getExposures, which uses explicit arguments instead of a kwargs dict. "
146 "This method will be removed after v29.",
147 version="v29.0", category=FutureWarning)
148 def getOverlappingExposures(self, inputs):
149 return self.getExposures(inputs["coaddExposures"],
150 inputs["bbox"],
151 inputs["skyMap"],
152 inputs["wcs"])
153
154 def getExposures(self, coaddExposureHandles, bbox, skymap, wcs):
155 """Return a data structure containing the coadds that overlap the
156 specified bbox projected onto the sky, and a corresponding data
157 structure of their dataIds.
158 These are the appropriate inputs to this task's `run` method.
159
160 The spatial index in the butler registry has generous padding and often
161 supplies patches near, but not directly overlapping the desired region.
162 This method filters the inputs so that `run` does not have to read in
163 all possibly-matching coadd exposures.
164
165 Parameters
166 ----------
167 coaddExposureHandles : `iterable` \
168 [`lsst.daf.butler.DeferredDatasetHandle` of \
169 `lsst.afw.image.Exposure`]
170 Dataset handles to exposures that might overlap the desired
171 region.
172 bbox : `lsst.geom.Box2I`
173 Template bounding box of the pixel geometry onto which the
174 coaddExposures will be resampled.
175 skymap : `lsst.skymap.SkyMap`
176 Geometry of the tracts and patches the coadds are defined on.
177 wcs : `lsst.afw.geom.SkyWcs`
178 Template WCS onto which the coadds will be resampled.
179
180 Returns
181 -------
182 result : `lsst.pipe.base.Struct`
183 A struct with attributes:
184
185 ``coaddExposures``
186 Dict of coadd exposures that overlap the projected bbox,
187 indexed on tract id
188 (`dict` [`int`, `list` [`lsst.daf.butler.DeferredDatasetHandle` of
189 `lsst.afw.image.Exposure`] ]).
190 ``dataIds``
191 Dict of data IDs of the coadd exposures that overlap the
192 projected bbox, indexed on tract id
193 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
194
195 Raises
196 ------
197 NoWorkFound
198 Raised if no patches overlap the input detector bbox, or the input
199 WCS is None.
200 """
201 if wcs is None:
202 raise pipeBase.NoWorkFound("WCS is None; cannot find overlapping exposures.")
203
204 # Exposure's validPolygon would be more accurate
205 detectorPolygon = geom.Box2D(bbox)
206 overlappingArea = 0
207 coaddExposures = collections.defaultdict(list)
208 dataIds = collections.defaultdict(list)
209
210 for coaddRef in coaddExposureHandles:
211 dataId = coaddRef.dataId
212 patchWcs = skymap[dataId['tract']].getWcs()
213 patchBBox = skymap[dataId['tract']][dataId['patch']].getOuterBBox()
214 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
215 patchPolygon = afwGeom.Polygon(wcs.skyToPixel(patchCorners))
216 if patchPolygon.intersection(detectorPolygon):
217 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
218 self.log.info("Using template input tract=%s, patch=%s", dataId['tract'], dataId['patch'])
219 coaddExposures[dataId['tract']].append(coaddRef)
220 dataIds[dataId['tract']].append(dataId)
221
222 if not overlappingArea:
223 raise pipeBase.NoWorkFound('No patches overlap detector')
224
225 return pipeBase.Struct(coaddExposures=coaddExposures,
226 dataIds=dataIds)
227
228 @timeMethod
229 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter):
230 """Warp coadds from multiple tracts and patches to form a template to
231 subtract from a science image.
232
233 Tract and patch overlap regions are combined by a variance-weighted
234 average, and the variance planes are combined with the same weights,
235 not added in quadrature; the overlap regions are not statistically
236 independent, because they're derived from the same original data.
237 The PSF on the template is created by combining the CoaddPsf on each
238 template image into a meta-CoaddPsf.
239
240 Parameters
241 ----------
242 coaddExposureHandles : `dict` [`int`, `list` of \
243 [`lsst.daf.butler.DeferredDatasetHandle` of \
244 `lsst.afw.image.Exposure`]]
245 Coadds to be mosaicked, indexed on tract id.
246 bbox : `lsst.geom.Box2I`
247 Template Bounding box of the detector geometry onto which to
248 resample the ``coaddExposureHandles``. Modified in-place to include the
249 template border.
250 wcs : `lsst.afw.geom.SkyWcs`
251 Template WCS onto which to resample the ``coaddExposureHandles``.
252 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
253 Record of the tract and patch of each coaddExposure, indexed on
254 tract id.
255 physical_filter : `str`
256 Physical filter of the science image.
257
258 Returns
259 -------
260 result : `lsst.pipe.base.Struct`
261 A struct with attributes:
262
263 ``template``
264 A template coadd exposure assembled out of patches
265 (`lsst.afw.image.ExposureF`).
266
267 Raises
268 ------
269 NoWorkFound
270 If no coadds are found with sufficient un-masked pixels.
271 """
272 band, photoCalib = self._checkInputs(dataIds, coaddExposureHandles)
273
274 bbox.grow(self.config.templateBorderSize)
275
276 warped = {}
277 catalogs = []
278 for tract in coaddExposureHandles:
279 maskedImages, catalog, totalBox = self._makeExposureCatalog(coaddExposureHandles[tract],
280 dataIds[tract])
281 warpedBox = computeWarpedBBox(catalog[0].wcs, bbox, wcs)
282 warpedBox.grow(5) # to ensure we catch all relevant input pixels
283 # Combine images from individual patches together.
284 unwarped, count, included = self._merge(maskedImages, warpedBox, catalog[0].wcs)
285 # Delete `maskedImages` after combining into one large image to reduce peak memory use
286 del maskedImages
287 if count == 0:
288 self.log.info("No valid pixels from coadd patches in tract %s; not including in output.",
289 tract)
290 continue
291 warpedBox.clip(totalBox)
292 potentialInput = self.warper.warpExposure(wcs, unwarped.subset(warpedBox), destBBox=bbox)
293
294 # Delete the single large `unwarped` image after warping to reduce peak memory use
295 del unwarped
296 if np.all(potentialInput.mask.array & potentialInput.mask.getPlaneBitMask("NO_DATA")):
297 self.log.info("No overlap from coadd patches in tract %s; not including in output.", tract)
298 continue
299
300 # Trim the exposure catalog to just the patches that were used.
301 tempCatalog = afwTable.ExposureCatalog(self.schema)
302 tempCatalog.reserve(len(included))
303 for i in included:
304 tempCatalog.append(catalog[i])
305 catalogs.append(tempCatalog)
306 warped[tract] = potentialInput.maskedImage
307
308 if len(warped) == 0:
309 raise pipeBase.NoWorkFound("No patches found to overlap science exposure.")
310 # At this point, all entries will be valid, so we can ignore included.
311 template, count, _ = self._merge(warped, bbox, wcs)
312 if count == 0:
313 raise pipeBase.NoWorkFound("No valid pixels in warped template.")
314
315 # Make a single catalog containing all the inputs that were accepted.
316 catalog = afwTable.ExposureCatalog(self.schema)
317 catalog.reserve(sum([len(c) for c in catalogs]))
318 for c in catalogs:
319 catalog.extend(c)
320
321 template.setPsf(self._makePsf(template, catalog, wcs))
322 template.setFilter(afwImage.FilterLabel(band, physical_filter))
323 template.setPhotoCalib(photoCalib)
324 return pipeBase.Struct(template=template)
325
326 @staticmethod
327 def _checkInputs(dataIds, coaddExposures):
328 """Check that the all the dataIds are from the same band and that
329 the exposures all have the same photometric calibration.
330
331 Parameters
332 ----------
333 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
334 Record of the tract and patch of each coaddExposure.
335 coaddExposures : `dict` [`int`, `list` of \
336 [`lsst.daf.butler.DeferredDatasetHandle` of \
337 `lsst.afw.image.Exposure`]]
338 Coadds to be mosaicked.
339
340 Returns
341 -------
342 band : `str`
343 Filter band of all the input exposures.
344 photoCalib : `lsst.afw.image.PhotoCalib`
345 Photometric calibration of all of the input exposures.
346
347 Raises
348 ------
349 RuntimeError
350 Raised if the bands or calibrations of the input exposures are not
351 all the same.
352 """
353 bands = set(dataId["band"] for tract in dataIds for dataId in dataIds[tract])
354 if len(bands) > 1:
355 raise RuntimeError(f"GetTemplateTask called with multiple bands: {bands}")
356 band = bands.pop()
357 photoCalibs = [exposure.get(component="photoCalib")
358 for exposures in coaddExposures.values()
359 for exposure in exposures]
360 if not all([photoCalibs[0] == x for x in photoCalibs]):
361 msg = f"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
362 raise RuntimeError(msg)
363 photoCalib = photoCalibs[0]
364 return band, photoCalib
365
366 def _makeExposureCatalog(self, exposureRefs, dataIds):
367 """Make an exposure catalog for one tract.
368
369 Parameters
370 ----------
371 exposureRefs : `list` of [`lsst.daf.butler.DeferredDatasetHandle` of \
372 `lsst.afw.image.Exposure`]
373 Exposures to include in the catalog.
374 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
375 Data ids of each of the included exposures; must have "tract" and
376 "patch" entries.
377
378 Returns
379 -------
380 images : `dict` [`lsst.afw.image.MaskedImage`]
381 MaskedImages of each of the input exposures, for warping.
382 catalog : `lsst.afw.table.ExposureCatalog`
383 Catalog of metadata for each exposure
384 totalBox : `lsst.geom.Box2I`
385 The union of the bounding boxes of all the input exposures.
386 """
387 catalog = afwTable.ExposureCatalog(self.schema)
388 catalog.reserve(len(exposureRefs))
389 exposures = (exposureRef.get() for exposureRef in exposureRefs)
390 images = {}
391 totalBox = geom.Box2I()
392
393 for coadd, dataId in zip(exposures, dataIds):
394 images[dataId] = coadd.maskedImage
395 bbox = coadd.getBBox()
396 totalBox = totalBox.expandedTo(bbox)
397 record = catalog.addNew()
398 record.setPsf(coadd.psf)
399 record.setWcs(coadd.wcs)
400 record.setPhotoCalib(coadd.photoCalib)
401 record.setBBox(bbox)
402 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(bbox).getCorners()))
403 record.set("tract", dataId["tract"])
404 record.set("patch", dataId["patch"])
405 # Weight is used by CoaddPsf, but the PSFs from overlapping patches
406 # should be very similar, so this value mostly shouldn't matter.
407 record.set("weight", 1)
408
409 return images, catalog, totalBox
410
411 def _merge(self, maskedImages, bbox, wcs):
412 """Merge the images that came from one tract into one larger image,
413 ignoring NaN pixels and non-finite variance pixels from individual
414 exposures.
415
416 Parameters
417 ----------
418 maskedImages : `dict` [`lsst.afw.image.MaskedImage` or
419 `lsst.afw.image.Exposure`]
420 Images to be merged into one larger bounding box.
421 bbox : `lsst.geom.Box2I`
422 Bounding box defining the image to merge into.
423 wcs : `lsst.afw.geom.SkyWcs`
424 WCS of all of the input images to set on the output image.
425
426 Returns
427 -------
428 merged : `lsst.afw.image.MaskedImage`
429 Merged image with all of the inputs at their respective bbox
430 positions.
431 count : `int`
432 Count of the number of good pixels (those with positive weights)
433 in the merged image.
434 included : `list` [`int`]
435 List of indexes of patches that were included in the merged
436 result, to be used to trim the exposure catalog.
437 """
438 merged = afwImage.ExposureF(bbox, wcs)
439 weights = afwImage.ImageF(bbox)
440 included = [] # which patches were included in the result
441 for i, (dataId, maskedImage) in enumerate(maskedImages.items()):
442 # Only merge into the trimmed box, to save memory
443 clippedBox = geom.Box2I(maskedImage.getBBox())
444 clippedBox.clip(bbox)
445 if clippedBox.area == 0:
446 self.log.debug("%s does not overlap template region.", dataId)
447 continue # nothing in this image overlaps the output
448 maskedImage = maskedImage.subset(clippedBox)
449 # Catch both zero-value and NaN variance plane pixels
450 good = (maskedImage.variance.array > 0) & (np.isfinite(maskedImage.variance.array))
451 weight = maskedImage.variance.array[good]**(-0.5)
452 bad = np.isnan(maskedImage.image.array) | ~good
453 # Note that modifying the patch MaskedImage in place is fine;
454 # we're throwing it away at the end anyway.
455 maskedImage.image.array[bad] = 0.0
456 maskedImage.variance.array[bad] = 0.0
457 # Reset mask, too, since these pixels don't contribute to sum.
458 maskedImage.mask.array[bad] = 0
459 # Cannot use `merged.maskedImage *= weight` because that operator
460 # multiplies the variance by the weight twice; in this case
461 # `weight` are the exact values we want to scale by.
462 maskedImage.image.array[good] *= weight
463 maskedImage.variance.array[good] *= weight
464 weights[clippedBox].array[good] += weight
465 # Free memory before creating new large arrays
466 del weight
467 merged.maskedImage[clippedBox] += maskedImage
468 included.append(i)
469
470 good = weights.array > 0
471
472 # Cannot use `merged.maskedImage /= weights` because that
473 # operator divides the variance by the weight twice; in this case
474 # `weights` are the exact values we want to scale by.
475 weights = weights.array[good]
476 merged.image.array[good] /= weights
477 merged.variance.array[good] /= weights
478
479 merged.mask.array[~good] |= merged.mask.getPlaneBitMask("NO_DATA")
480
481 return merged, good.sum(), included
482
483 def _makePsf(self, template, catalog, wcs):
484 """Return a PSF containing the PSF at each of the input regions.
485
486 Note that although this includes all the exposures from the catalog,
487 the PSF knows which part of the template the inputs came from, so when
488 evaluated at a given position it will not include inputs that never
489 went in to those pixels.
490
491 Parameters
492 ----------
493 template : `lsst.afw.image.Exposure`
494 Generated template the PSF is for.
495 catalog : `lsst.afw.table.ExposureCatalog`
496 Catalog of exposures that went into the template that contains all
497 of the input PSFs.
498 wcs : `lsst.afw.geom.SkyWcs`
499 WCS of the template, to warp the PSFs to.
500
501 Returns
502 -------
503 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
504 The meta-psf constructed from all of the input catalogs.
505 """
506 # CoaddPsf centroid not only must overlap image, but must overlap the
507 # part of image with data. Use centroid of region with data.
508 boolmask = template.mask.array & template.mask.getPlaneBitMask('NO_DATA') == 0
509 maskx = afwImage.makeMaskFromArray(boolmask.astype(afwImage.MaskPixel))
510 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
511
512 ctrl = self.config.coaddPsf.makeControl()
513 coaddPsf = CoaddPsf(catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
514 return coaddPsf
515
516
518 dimensions=("instrument", "visit", "detector", "skymap"),
519 defaultTemplates={"coaddName": "dcr",
520 "warpTypeSuffix": "",
521 "fakesType": ""}):
522 visitInfo = pipeBase.connectionTypes.Input(
523 doc="VisitInfo of calexp used to determine observing conditions.",
524 name="{fakesType}calexp.visitInfo",
525 storageClass="VisitInfo",
526 dimensions=("instrument", "visit", "detector"),
527 )
528 dcrCoadds = pipeBase.connectionTypes.Input(
529 doc="Input DCR template to match and subtract from the exposure",
530 name="{fakesType}dcrCoadd{warpTypeSuffix}",
531 storageClass="ExposureF",
532 dimensions=("tract", "patch", "skymap", "band", "subfilter"),
533 multiple=True,
534 deferLoad=True
535 )
536
537 def __init__(self, *, config=None):
538 super().__init__(config=config)
539 self.inputs.remove("coaddExposures")
540
541
542class GetDcrTemplateConfig(GetTemplateConfig,
543 pipelineConnections=GetDcrTemplateConnections):
544 numSubfilters = pexConfig.Field(
545 doc="Number of subfilters in the DcrCoadd.",
546 dtype=int,
547 default=3,
548 )
549 effectiveWavelength = pexConfig.Field(
550 doc="Effective wavelength of the filter in nm.",
551 optional=False,
552 dtype=float,
553 )
554 bandwidth = pexConfig.Field(
555 doc="Bandwidth of the physical filter.",
556 optional=False,
557 dtype=float,
558 )
559
560 def validate(self):
561 if self.effectiveWavelength is None or self.bandwidth is None:
562 raise ValueError("The effective wavelength and bandwidth of the physical filter "
563 "must be set in the getTemplate config for DCR coadds. "
564 "Required until transmission curves are used in DM-13668.")
565
566
567class GetDcrTemplateTask(GetTemplateTask):
568 ConfigClass = GetDcrTemplateConfig
569 _DefaultName = "getDcrTemplate"
570
571 def runQuantum(self, butlerQC, inputRefs, outputRefs):
572 inputs = butlerQC.get(inputRefs)
573 bbox = inputs.pop("bbox")
574 wcs = inputs.pop("wcs")
575 dcrCoaddExposureHandles = inputs.pop('dcrCoadds')
576 skymap = inputs.pop("skyMap")
577 visitInfo = inputs.pop("visitInfo")
578
579 # This should not happen with a properly configured execution context.
580 assert not inputs, "runQuantum got more inputs than expected"
581
582 results = self.getExposures(dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo)
583 physical_filter = butlerQC.quantum.dataId["physical_filter"]
584 outputs = self.run(coaddExposures=results.coaddExposures,
585 bbox=bbox,
586 wcs=wcs,
587 dataIds=results.dataIds,
588 physical_filter=physical_filter)
589 butlerQC.put(outputs, outputRefs)
590
591 @deprecated(reason="Replaced by getExposures, which uses explicit arguments instead of a kwargs dict. "
592 "This method will be removed after v29.",
593 version="v29.0", category=FutureWarning)
594 def getOverlappingExposures(self, inputs):
595 return self.getExposures(inputs["dcrCoadds"],
596 inputs["bbox"],
597 inputs["skyMap"],
598 inputs["wcs"],
599 inputs["visitInfo"])
600
601 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
602 """Return lists of coadds and their corresponding dataIds that overlap
603 the detector.
604
605 The spatial index in the registry has generous padding and often
606 supplies patches near, but not directly overlapping the detector.
607 Filters inputs so that we don't have to read in all input coadds.
608
609 Parameters
610 ----------
611 dcrCoaddExposureHandles : `list` \
612 [`lsst.daf.butler.DeferredDatasetHandle` of \
613 `lsst.afw.image.Exposure`]
614 Data references to exposures that might overlap the detector.
615 bbox : `lsst.geom.Box2I`
616 Template Bounding box of the detector geometry onto which to
617 resample the coaddExposures.
618 skymap : `lsst.skymap.SkyMap`
619 Input definition of geometry/bbox and projection/wcs for
620 template exposures.
621 wcs : `lsst.afw.geom.SkyWcs`
622 Template WCS onto which to resample the coaddExposures.
623 visitInfo : `lsst.afw.image.VisitInfo`
624 Metadata for the science image.
625
626 Returns
627 -------
628 result : `lsst.pipe.base.Struct`
629 A struct with attibutes:
630
631 ``coaddExposures``
632 Dict of coadd exposures that overlap the projected bbox,
633 indexed on tract id
634 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
635 ``dataIds``
636 Dict of data IDs of the coadd exposures that overlap the
637 projected bbox, indexed on tract id
638 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
639
640 Raises
641 ------
642 pipeBase.NoWorkFound
643 Raised if no patches overlatp the input detector bbox.
644 """
645 # Check that the patches actually overlap the detector
646 # Exposure's validPolygon would be more accurate
647 if wcs is None:
648 raise pipeBase.NoWorkFound("Exposure has no WCS; cannot create a template.")
649
650 detectorPolygon = geom.Box2D(bbox)
651 overlappingArea = 0
652 dataIds = collections.defaultdict(list)
653 patchList = dict()
654 for coaddRef in dcrCoaddExposureHandles:
655 dataId = coaddRef.dataId
656 patchWcs = skymap[dataId['tract']].getWcs()
657 patchBBox = skymap[dataId['tract']][dataId['patch']].getOuterBBox()
658 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
659 patchPolygon = afwGeom.Polygon(wcs.skyToPixel(patchCorners))
660 if patchPolygon.intersection(detectorPolygon):
661 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
662 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s" %
663 (dataId['tract'], dataId['patch'], dataId["subfilter"]))
664 if dataId['tract'] in patchList:
665 patchList[dataId['tract']].append(dataId['patch'])
666 else:
667 patchList[dataId['tract']] = [dataId['patch'], ]
668 dataIds[dataId['tract']].append(dataId)
669
670 if not overlappingArea:
671 raise pipeBase.NoWorkFound('No patches overlap detector')
672
673 self.checkPatchList(patchList)
674
675 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
676 return pipeBase.Struct(coaddExposures=coaddExposures,
677 dataIds=dataIds)
678
679 def checkPatchList(self, patchList):
680 """Check that all of the DcrModel subfilters are present for each
681 patch.
682
683 Parameters
684 ----------
685 patchList : `dict`
686 Dict of the patches containing valid data for each tract.
687
688 Raises
689 ------
690 RuntimeError
691 If the number of exposures found for a patch does not match the
692 number of subfilters.
693 """
694 for tract in patchList:
695 for patch in set(patchList[tract]):
696 if patchList[tract].count(patch) != self.config.numSubfilters:
697 raise RuntimeError("Invalid number of DcrModel subfilters found: %d vs %d expected",
698 patchList[tract].count(patch), self.config.numSubfilters)
699
700 def getDcrModel(self, patchList, coaddRefs, visitInfo):
701 """Build DCR-matched coadds from a list of exposure references.
702
703 Parameters
704 ----------
705 patchList : `dict`
706 Dict of the patches containing valid data for each tract.
707 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
708 Data references to `~lsst.afw.image.Exposure` representing
709 DcrModels that overlap the detector.
710 visitInfo : `lsst.afw.image.VisitInfo`
711 Metadata for the science image.
712
713 Returns
714 -------
715 coaddExposures : `list` [`lsst.afw.image.Exposure`]
716 Coadd exposures that overlap the detector.
717 """
718 coaddExposures = collections.defaultdict(list)
719 for tract in patchList:
720 for patch in set(patchList[tract]):
721 coaddRefList = [coaddRef for coaddRef in coaddRefs
722 if _selectDataRef(coaddRef, tract, patch)]
723
724 dcrModel = DcrModel.fromQuantum(coaddRefList,
725 self.config.effectiveWavelength,
726 self.config.bandwidth,
727 self.config.numSubfilters)
728 coaddExposures[tract].append(dcrModel.buildMatchedExposure(visitInfo=visitInfo))
729 return coaddExposures
730
731
732def _selectDataRef(coaddRef, tract, patch):
733 condition = (coaddRef.dataId['tract'] == tract) & (coaddRef.dataId['patch'] == patch)
734 return condition
Cartesian polygons.
Definition Polygon.h:59
A group of labels for a filter in an exposure or coadd.
Definition FilterLabel.h:58
A floating-point coordinate rectangle geometry.
Definition Box.h:413
An integer coordinate rectangle.
Definition Box.h:55
CoaddPsf is the Psf derived to be used for non-PSF-matched Coadd images.
Definition CoaddPsf.h:58
_selectDataRef(coaddRef, tract, patch)
checkPatchList(self, patchList)
_merge(self, maskedImages, bbox, wcs)
_makeExposureCatalog(self, exposureRefs, dataIds)
run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter)
getDcrModel(self, patchList, coaddRefs, visitInfo)
_makePsf(self, template, catalog, wcs)