LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
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 collections
22
23import numpy as np
24
25import lsst.afw.image as afwImage
26import lsst.geom as geom
27import lsst.afw.geom as afwGeom
28import lsst.afw.table as afwTable
29import lsst.afw.math as afwMath
30import lsst.pex.config as pexConfig
31import lsst.pipe.base as pipeBase
32from lsst.skymap import BaseSkyMap
33from lsst.ip.diffim.dcrModel import DcrModel
34from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig
35from lsst.utils.timer import timeMethod
36
37__all__ = ["GetTemplateTask", "GetTemplateConfig",
38 "GetDcrTemplateTask", "GetDcrTemplateConfig"]
39
40
41class GetTemplateConnections(pipeBase.PipelineTaskConnections,
42 dimensions=("instrument", "visit", "detector", "skymap"),
43 defaultTemplates={"coaddName": "goodSeeing",
44 "warpTypeSuffix": "",
45 "fakesType": ""}):
46 bbox = pipeBase.connectionTypes.Input(
47 doc="Bounding box of exposure to determine the geometry of the output template.",
48 name="{fakesType}calexp.bbox",
49 storageClass="Box2I",
50 dimensions=("instrument", "visit", "detector"),
51 )
52 wcs = pipeBase.connectionTypes.Input(
53 doc="WCS of the exposure that we will construct the template for.",
54 name="{fakesType}calexp.wcs",
55 storageClass="Wcs",
56 dimensions=("instrument", "visit", "detector"),
57 )
58 skyMap = pipeBase.connectionTypes.Input(
59 doc="Geometry of the tracts and patches that the coadds are defined on.",
60 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
61 dimensions=("skymap", ),
62 storageClass="SkyMap",
63 )
64 coaddExposures = pipeBase.connectionTypes.Input(
65 doc="Coadds that may overlap the desired region, as possible inputs to the template."
66 " Will be restricted to those that directly overlap the projected bounding box.",
67 dimensions=("tract", "patch", "skymap", "band"),
68 storageClass="ExposureF",
69 name="{fakesType}{coaddName}Coadd{warpTypeSuffix}",
70 multiple=True,
71 deferLoad=True
72 )
73
74 template = pipeBase.connectionTypes.Output(
75 doc="Warped template, pixel matched to the bounding box and WCS.",
76 dimensions=("instrument", "visit", "detector"),
77 storageClass="ExposureF",
78 name="{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
79 )
80
81
82class GetTemplateConfig(pipeBase.PipelineTaskConfig,
83 pipelineConnections=GetTemplateConnections):
84 templateBorderSize = pexConfig.Field(
85 dtype=int,
86 default=20,
87 doc="Number of pixels to grow the requested template image to account for warping"
88 )
89 warp = pexConfig.ConfigField(
90 dtype=afwMath.Warper.ConfigClass,
91 doc="warper configuration",
92 )
93 coaddPsf = pexConfig.ConfigField(
94 doc="Configuration for CoaddPsf",
95 dtype=CoaddPsfConfig,
96 )
97
98 def setDefaults(self):
99 self.warp.warpingKernelName = 'lanczos5'
100 self.coaddPsf.warpingKernelName = 'lanczos5'
101
102
103class GetTemplateTask(pipeBase.PipelineTask):
104 ConfigClass = GetTemplateConfig
105 _DefaultName = "getTemplate"
106
107 def __init__(self, *args, **kwargs):
108 super().__init__(*args, **kwargs)
109 self.warper = afwMath.Warper.fromConfig(self.config.warp)
110 self.schema = afwTable.ExposureTable.makeMinimalSchema()
111 self.schema.addField('tract', type=np.int32, doc='Which tract this exposure came from.')
112 self.schema.addField('patch', type=np.int32, doc='Which patch in the tract this exposure came from.')
113 self.schema.addField('weight', type=float,
114 doc='Weight for each exposure, used to make the CoaddPsf; should always be 1.')
115
116 def runQuantum(self, butlerQC, inputRefs, outputRefs):
117 inputs = butlerQC.get(inputRefs)
118 results = self.getOverlappingExposures(inputs)
119 del inputs["skyMap"] # Only needed for the above.
120 inputs["coaddExposures"] = results.coaddExposures
121 inputs["dataIds"] = results.dataIds
122 inputs["physical_filter"] = butlerQC.quantum.dataId["physical_filter"]
123 outputs = self.run(**inputs)
124 butlerQC.put(outputs, outputRefs)
125
126 def getOverlappingExposures(self, inputs):
127 """Return a data structure containing the coadds that overlap the
128 specified bbox projected onto the sky, and a corresponding data
129 structure of their dataIds.
130 These are the appropriate inputs to this task's `run` method.
131
132 The spatial index in the butler registry has generous padding and often
133 supplies patches near, but not directly overlapping the desired region.
134 This method filters the inputs so that `run` does not have to read in
135 all possibly-matching coadd exposures.
136
137 Parameters
138 ----------
139 inputs : `dict` of task Inputs, containing:
140 - coaddExposures : `list` \
141 [`lsst.daf.butler.DeferredDatasetHandle` of \
142 `lsst.afw.image.Exposure`]
143 Data references to exposures that might overlap the desired
144 region.
145 - bbox : `lsst.geom.Box2I`
146 Template bounding box of the pixel geometry onto which the
147 coaddExposures will be resampled.
148 - skyMap : `lsst.skymap.SkyMap`
149 Geometry of the tracts and patches the coadds are defined on.
150 - wcs : `lsst.afw.geom.SkyWcs`
151 Template WCS onto which the coadds will be resampled.
152
153 Returns
154 -------
155 result : `lsst.pipe.base.Struct`
156 A struct with attributes:
157
158 ``coaddExposures``
159 Dict of coadd exposures that overlap the projected bbox,
160 indexed on tract id
161 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
162 ``dataIds``
163 Dict of data IDs of the coadd exposures that overlap the
164 projected bbox, indexed on tract id
165 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
166
167 Raises
168 ------
169 NoWorkFound
170 Raised if no patches overlap the input detector bbox, or the input
171 WCS is None.
172 """
173 if (wcs := inputs['wcs']) is None:
174 raise pipeBase.NoWorkFound("Exposure has no WCS; cannot create a template.")
175
176 # Exposure's validPolygon would be more accurate
177 detectorPolygon = geom.Box2D(inputs['bbox'])
178 overlappingArea = 0
179 coaddExposures = collections.defaultdict(list)
180 dataIds = collections.defaultdict(list)
181
182 skymap = inputs['skyMap']
183 for coaddRef in inputs['coaddExposures']:
184 dataId = coaddRef.dataId
185 patchWcs = skymap[dataId['tract']].getWcs()
186 patchBBox = skymap[dataId['tract']][dataId['patch']].getOuterBBox()
187 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
188 patchPolygon = afwGeom.Polygon(wcs.skyToPixel(patchCorners))
189 if patchPolygon.intersection(detectorPolygon):
190 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
191 self.log.info("Using template input tract=%s, patch=%s", dataId['tract'], dataId['patch'])
192 coaddExposures[dataId['tract']].append(coaddRef.get())
193 dataIds[dataId['tract']].append(dataId)
194
195 if not overlappingArea:
196 raise pipeBase.NoWorkFound('No patches overlap detector')
197
198 return pipeBase.Struct(coaddExposures=coaddExposures,
199 dataIds=dataIds)
200
201 @timeMethod
202 def run(self, coaddExposures, bbox, wcs, dataIds, physical_filter):
203 """Warp coadds from multiple tracts and patches to form a template to
204 subtract from a science image.
205
206 Tract and patch overlap regions are combined by a variance-weighted
207 average, and the variance planes are combined with the same weights,
208 not added in quadrature; the overlap regions are not statistically
209 independent, because they're derived from the same original data.
210 The PSF on the template is created by combining the CoaddPsf on each
211 template image into a meta-CoaddPsf.
212
213 Parameters
214 ----------
215 coaddExposures : `dict` [`int`, `list` [`lsst.afw.image.Exposure`]]
216 Coadds to be mosaicked, indexed on tract id.
217 bbox : `lsst.geom.Box2I`
218 Template Bounding box of the detector geometry onto which to
219 resample the ``coaddExposures``. Modified in-place to include the
220 template border.
221 wcs : `lsst.afw.geom.SkyWcs`
222 Template WCS onto which to resample the ``coaddExposures``.
223 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
224 Record of the tract and patch of each coaddExposure, indexed on
225 tract id.
226 physical_filter : `str`
227 Physical filter of the science image.
228
229 Returns
230 -------
231 result : `lsst.pipe.base.Struct`
232 A struct with attributes:
233
234 ``template``
235 A template coadd exposure assembled out of patches
236 (`lsst.afw.image.ExposureF`).
237
238 Raises
239 ------
240 NoWorkFound
241 If no coadds are found with sufficient un-masked pixels.
242 """
243 band, photoCalib = self._checkInputs(dataIds, coaddExposures)
244
245 bbox.grow(self.config.templateBorderSize)
246
247 warped = {}
248 catalogs = []
249 for tract in coaddExposures:
250 maskedImages, catalog, totalBox = self._makeExposureCatalog(coaddExposures[tract],
251 dataIds[tract])
252 # Combine images from individual patches together.
253 unwarped = self._merge(maskedImages, totalBox, catalog[0].wcs)
254 potentialInput = self.warper.warpExposure(wcs, unwarped, destBBox=bbox)
255 if not np.any(np.isfinite(potentialInput.image.array)):
256 self.log.info("No overlap from coadds in tract %s; not including in output.", tract)
257 continue
258 catalogs.append(catalog)
259 warped[tract] = potentialInput
260 warped[tract].setWcs(wcs)
261
262 if len(warped) == 0:
263 raise pipeBase.NoWorkFound("No patches found to overlap science exposure.")
264 template = self._merge([x.maskedImage for x in warped.values()], bbox, wcs)
265
266 # Make a single catalog containing all the inputs that were accepted.
267 catalog = afwTable.ExposureCatalog(self.schema)
268 catalog.reserve(sum([len(c) for c in catalogs]))
269 for c in catalogs:
270 catalog.extend(c)
271
272 template.setPsf(self._makePsf(template, catalog, wcs))
273 template.setFilter(afwImage.FilterLabel(band, physical_filter))
274 template.setPhotoCalib(photoCalib)
275 return pipeBase.Struct(template=template)
276
277 @staticmethod
278 def _checkInputs(dataIds, coaddExposures):
279 """Check that the all the dataIds are from the same band and that
280 the exposures all have the same photometric calibration.
281
282 Parameters
283 ----------
284 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
285 Record of the tract and patch of each coaddExposure.
286 coaddExposures : `dict` [`int`, `list` [`lsst.afw.image.Exposure`]]
287 Coadds to be mosaicked.
288
289 Returns
290 -------
291 band : `str`
292 Filter band of all the input exposures.
293 photoCalib : `lsst.afw.image.PhotoCalib`
294 Photometric calibration of all of the input exposures.
295
296 Raises
297 ------
298 RuntimeError
299 Raised if the bands or calibrations of the input exposures are not
300 all the same.
301 """
302 bands = set(dataId["band"] for tract in dataIds for dataId in dataIds[tract])
303 if len(bands) > 1:
304 raise RuntimeError(f"GetTemplateTask called with multiple bands: {bands}")
305 band = bands.pop()
306 photoCalibs = [exposure.photoCalib for exposures in coaddExposures.values() for exposure in exposures]
307 if not all([photoCalibs[0] == x for x in photoCalibs]):
308 msg = f"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
309 raise RuntimeError(msg)
310 photoCalib = photoCalibs[0]
311 return band, photoCalib
312
313 def _makeExposureCatalog(self, exposures, dataIds):
314 """Make an exposure catalog for one tract.
315
316 Parameters
317 ----------
318 exposures : `list` [`lsst.afw.image.Exposuref`]
319 Exposures to include in the catalog.
320 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
321 Data ids of each of the included exposures; must have "tract" and
322 "patch" entries.
323
324 Returns
325 -------
326 images : `list` [`lsst.afw.image.MaskedImage`]
327 MaskedImages of each of the input exposures, for warping.
328 catalog : `lsst.afw.table.ExposureCatalog`
329 Catalog of metadata for each exposure
330 totalBox : `lsst.geom.Box2I`
331 The union of the bounding boxes of all the input exposures.
332 """
333 catalog = afwTable.ExposureCatalog(self.schema)
334 catalog.reserve(len(exposures))
335 images = [exposure.maskedImage for exposure in exposures]
336 totalBox = geom.Box2I()
337 for coadd, dataId in zip(exposures, dataIds):
338 totalBox = totalBox.expandedTo(coadd.getBBox())
339 record = catalog.addNew()
340 record.setPsf(coadd.psf)
341 record.setWcs(coadd.wcs)
342 record.setPhotoCalib(coadd.photoCalib)
343 record.setBBox(coadd.getBBox())
344 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(coadd.getBBox()).getCorners()))
345 record.set("tract", dataId["tract"])
346 record.set("patch", dataId["patch"])
347 # Weight is used by CoaddPsf, but the PSFs from overlapping patches
348 # should be very similar, so this value mostly shouldn't matter.
349 record.set("weight", 1)
350
351 return images, catalog, totalBox
352
353 def _merge(self, maskedImages, bbox, wcs):
354 """Merge the images that came from one tract into one larger image,
355 ignoring NaN pixels and non-finite variance pixels from individual
356 exposures.
357
358 Parameters
359 ----------
360 maskedImages : `list` [`lsst.afw.image.MaskedImage`]
361 Images to be merged into one larger bounding box.
362 bbox : `lsst.geom.Box2I`
363 Bounding box defining the image to merge into.
364 wcs : `lsst.afw.geom.SkyWcs`
365 WCS of all of the input images to set on the output image.
366
367 Returns
368 -------
369 merged : `lsst.afw.image.MaskedImage`
370 Merged image with all of the inputs at their respective bbox
371 positions.
372 """
373 merged = afwImage.ExposureF(bbox, wcs)
374 weights = afwImage.ImageF(bbox)
375 for maskedImage in maskedImages:
376 weight = afwImage.ImageF(maskedImage.variance.array**(-0.5))
377 bad = np.isnan(maskedImage.image.array) | ~np.isfinite(maskedImage.variance.array)
378 # Note that modifying the patch MaskedImage in place is fine;
379 # we're throwing it away at the end anyway.
380 maskedImage.image.array[bad] = 0.0
381 maskedImage.variance.array[bad] = 0.0
382 # Reset mask, too, since these pixels don't contribute to sum.
383 maskedImage.mask.array[bad] = 0
384 # Cannot use `merged.maskedImage *= weight` because that operator
385 # multiplies the variance by the weight twice; in this case
386 # `weight` are the exact values we want to scale by.
387 maskedImage.image *= weight
388 maskedImage.variance *= weight
389 merged.maskedImage[maskedImage.getBBox()] += maskedImage
390 # Clear the NaNs to ensure that areas missing from this input are
391 # masked with NO_DATA after the loop.
392 weight.array[np.isnan(weight.array)] = 0
393 weights[maskedImage.getBBox()] += weight
394 # Cannot use `merged.maskedImage /= weights` because that operator
395 # divides the variance by the weight twice; in this case `weights` are
396 # the exact values we want to scale by.
397 merged.image /= weights
398 merged.variance /= weights
399 merged.mask.array |= merged.mask.getPlaneBitMask("NO_DATA") * (weights.array == 0)
400
401 return merged
402
403 def _makePsf(self, template, catalog, wcs):
404 """Return a PSF containing the PSF at each of the input regions.
405
406 Note that although this includes all the exposures from the catalog,
407 the PSF knows which part of the template the inputs came from, so when
408 evaluated at a given position it will not include inputs that never
409 went in to those pixels.
410
411 Parameters
412 ----------
413 template : `lsst.afw.image.Exposure`
414 Generated template the PSF is for.
415 catalog : `lsst.afw.table.ExposureCatalog`
416 Catalog of exposures that went into the template that contains all
417 of the input PSFs.
418 wcs : `lsst.afw.geom.SkyWcs`
419 WCS of the template, to warp the PSFs to.
420
421 Returns
422 -------
423 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
424 The meta-psf constructed from all of the input catalogs.
425 """
426 # CoaddPsf centroid not only must overlap image, but must overlap the
427 # part of image with data. Use centroid of region with data.
428 boolmask = template.mask.array & template.mask.getPlaneBitMask('NO_DATA') == 0
429 maskx = afwImage.makeMaskFromArray(boolmask.astype(afwImage.MaskPixel))
430 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
431
432 ctrl = self.config.coaddPsf.makeControl()
433 coaddPsf = CoaddPsf(catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
434 return coaddPsf
435
436
438 dimensions=("instrument", "visit", "detector", "skymap"),
439 defaultTemplates={"coaddName": "dcr",
440 "warpTypeSuffix": "",
441 "fakesType": ""}):
442 visitInfo = pipeBase.connectionTypes.Input(
443 doc="VisitInfo of calexp used to determine observing conditions.",
444 name="{fakesType}calexp.visitInfo",
445 storageClass="VisitInfo",
446 dimensions=("instrument", "visit", "detector"),
447 )
448 dcrCoadds = pipeBase.connectionTypes.Input(
449 doc="Input DCR template to match and subtract from the exposure",
450 name="{fakesType}dcrCoadd{warpTypeSuffix}",
451 storageClass="ExposureF",
452 dimensions=("tract", "patch", "skymap", "band", "subfilter"),
453 multiple=True,
454 deferLoad=True
455 )
456
457 def __init__(self, *, config=None):
458 super().__init__(config=config)
459 self.inputs.remove("coaddExposures")
460
461
462class GetDcrTemplateConfig(GetTemplateConfig,
463 pipelineConnections=GetDcrTemplateConnections):
464 numSubfilters = pexConfig.Field(
465 doc="Number of subfilters in the DcrCoadd.",
466 dtype=int,
467 default=3,
468 )
469 effectiveWavelength = pexConfig.Field(
470 doc="Effective wavelength of the filter.",
471 optional=False,
472 dtype=float,
473 )
474 bandwidth = pexConfig.Field(
475 doc="Bandwidth of the physical filter.",
476 optional=False,
477 dtype=float,
478 )
479
480 def validate(self):
481 if self.effectiveWavelength is None or self.bandwidth is None:
482 raise ValueError("The effective wavelength and bandwidth of the physical filter "
483 "must be set in the getTemplate config for DCR coadds. "
484 "Required until transmission curves are used in DM-13668.")
485
486
487class GetDcrTemplateTask(GetTemplateTask):
488 ConfigClass = GetDcrTemplateConfig
489 _DefaultName = "getDcrTemplate"
490
491 def getOverlappingExposures(self, inputs):
492 """Return lists of coadds and their corresponding dataIds that overlap
493 the detector.
494
495 The spatial index in the registry has generous padding and often
496 supplies patches near, but not directly overlapping the detector.
497 Filters inputs so that we don't have to read in all input coadds.
498
499 Parameters
500 ----------
501 inputs : `dict` of task Inputs, containing:
502 - coaddExposureRefs : `list` \
503 [`lsst.daf.butler.DeferredDatasetHandle` of \
504 `lsst.afw.image.Exposure`]
505 Data references to exposures that might overlap the detector.
506 - bbox : `lsst.geom.Box2I`
507 Template Bounding box of the detector geometry onto which to
508 resample the coaddExposures.
509 - skyMap : `lsst.skymap.SkyMap`
510 Input definition of geometry/bbox and projection/wcs for
511 template exposures.
512 - wcs : `lsst.afw.geom.SkyWcs`
513 Template WCS onto which to resample the coaddExposures.
514 - visitInfo : `lsst.afw.image.VisitInfo`
515 Metadata for the science image.
516
517 Returns
518 -------
519 result : `lsst.pipe.base.Struct`
520 A struct with attibutes:
521
522 ``coaddExposures``
523 Coadd exposures that overlap the detector (`list`
524 [`lsst.afw.image.Exposure`]).
525 ``dataIds``
526 Data IDs of the coadd exposures that overlap the detector
527 (`list` [`lsst.daf.butler.DataCoordinate`]).
528
529 Raises
530 ------
531 NoWorkFound
532 Raised if no patches overlatp the input detector bbox.
533 """
534 # Check that the patches actually overlap the detector
535 # Exposure's validPolygon would be more accurate
536 detectorPolygon = geom.Box2D(inputs["bbox"])
537 overlappingArea = 0
538 coaddExposureRefList = []
539 dataIds = []
540 patchList = dict()
541 for coaddRef in inputs["dcrCoadds"]:
542 dataId = coaddRef.dataId
543 patchWcs = inputs["skyMap"][dataId['tract']].getWcs()
544 patchBBox = inputs["skyMap"][dataId['tract']][dataId['patch']].getOuterBBox()
545 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
546 patchPolygon = afwGeom.Polygon(inputs["wcs"].skyToPixel(patchCorners))
547 if patchPolygon.intersection(detectorPolygon):
548 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
549 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s" %
550 (dataId['tract'], dataId['patch'], dataId["subfilter"]))
551 coaddExposureRefList.append(coaddRef)
552 if dataId['tract'] in patchList:
553 patchList[dataId['tract']].append(dataId['patch'])
554 else:
555 patchList[dataId['tract']] = [dataId['patch'], ]
556 dataIds.append(dataId)
557
558 if not overlappingArea:
559 raise pipeBase.NoWorkFound('No patches overlap detector')
560
561 self.checkPatchList(patchList)
562
563 coaddExposures = self.getDcrModel(patchList, inputs['dcrCoadds'], inputs['visitInfo'])
564 return pipeBase.Struct(coaddExposures=coaddExposures,
565 dataIds=dataIds)
566
567 def checkPatchList(self, patchList):
568 """Check that all of the DcrModel subfilters are present for each
569 patch.
570
571 Parameters
572 ----------
573 patchList : `dict`
574 Dict of the patches containing valid data for each tract.
575
576 Raises
577 ------
578 RuntimeError
579 If the number of exposures found for a patch does not match the
580 number of subfilters.
581 """
582 for tract in patchList:
583 for patch in set(patchList[tract]):
584 if patchList[tract].count(patch) != self.config.numSubfilters:
585 raise RuntimeError("Invalid number of DcrModel subfilters found: %d vs %d expected",
586 patchList[tract].count(patch), self.config.numSubfilters)
587
588 def getDcrModel(self, patchList, coaddRefs, visitInfo):
589 """Build DCR-matched coadds from a list of exposure references.
590
591 Parameters
592 ----------
593 patchList : `dict`
594 Dict of the patches containing valid data for each tract.
595 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
596 Data references to `~lsst.afw.image.Exposure` representing
597 DcrModels that overlap the detector.
598 visitInfo : `lsst.afw.image.VisitInfo`
599 Metadata for the science image.
600
601 Returns
602 -------
603 coaddExposures : `list` [`lsst.afw.image.Exposure`]
604 Coadd exposures that overlap the detector.
605 """
606 coaddExposures = []
607 for tract in patchList:
608 for patch in set(patchList[tract]):
609 coaddRefList = [coaddRef for coaddRef in coaddRefs
610 if _selectDataRef(coaddRef, tract, patch)]
611
612 dcrModel = DcrModel.fromQuantum(coaddRefList,
613 self.config.effectiveWavelength,
614 self.config.bandwidth,
615 self.config.numSubfilters)
616 coaddExposures.append(dcrModel.buildMatchedExposure(visitInfo=visitInfo))
617 return coaddExposures
618
619
620def _selectDataRef(coaddRef, tract, patch):
621 condition = (coaddRef.dataId['tract'] == tract) & (coaddRef.dataId['patch'] == patch)
622 return condition
Cartesian polygons.
Definition Polygon.h:59
A group of labels for a filter in an exposure or coadd.
Definition FilterLabel.h:58
Custom catalog class for ExposureRecord/Table.
Definition Exposure.h:310
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)
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter)
checkPatchList(self, patchList)
_merge(self, maskedImages, bbox, wcs)
_makeExposureCatalog(self, exposures, dataIds)
getDcrModel(self, patchList, coaddRefs, visitInfo)
_makePsf(self, template, catalog, wcs)