LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
dcrAssembleCoadd.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
2#
3# LSST Data Management System
4# This product includes software developed by the
5# LSST Project (http://www.lsst.org/).
6# See COPYRIGHT file at the top of the source tree.
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <https://www.lsstcorp.org/LegalNotices/>.
21#
22
23from math import ceil
24import numpy as np
25from scipy import ndimage
26import lsst.geom as geom
27import lsst.afw.image as afwImage
28import lsst.afw.table as afwTable
29import lsst.coadd.utils as coaddUtils
30from lsst.daf.butler import DeferredDatasetHandle
31from lsst.ip.diffim.dcrModel import applyDcr, calculateDcr, DcrModel
32import lsst.meas.algorithms as measAlg
33from lsst.meas.base import SingleFrameMeasurementTask
34import lsst.pex.config as pexConfig
35import lsst.pipe.base as pipeBase
36import lsst.utils as utils
37from lsst.utils.timer import timeMethod
38from .assembleCoadd import (AssembleCoaddConnections,
39 AssembleCoaddTask,
40 CompareWarpAssembleCoaddConfig,
41 CompareWarpAssembleCoaddTask)
42from .coaddBase import makeSkyInfo
43from .measurePsf import MeasurePsfTask
44
45__all__ = ["DcrAssembleCoaddConnections", "DcrAssembleCoaddTask", "DcrAssembleCoaddConfig"]
46
47
49 dimensions=("tract", "patch", "band", "skymap"),
50 defaultTemplates={"inputWarpName": "deep",
51 "inputCoaddName": "deep",
52 "outputCoaddName": "dcr",
53 "warpType": "direct",
54 "warpTypeSuffix": "",
55 "fakesType": ""}):
56 inputWarps = pipeBase.connectionTypes.Input(
57 doc=("Input list of warps to be assembled i.e. stacked."
58 "Note that this will often be different than the inputCoaddName."
59 "WarpType (e.g. direct, psfMatched) is controlled by the warpType config parameter"),
60 name="{inputWarpName}Coadd_{warpType}Warp",
61 storageClass="ExposureF",
62 dimensions=("tract", "patch", "skymap", "visit", "instrument"),
63 deferLoad=True,
64 multiple=True
65 )
66 templateExposure = pipeBase.connectionTypes.Input(
67 doc="Input coadded exposure, produced by previous call to AssembleCoadd",
68 name="{fakesType}{inputCoaddName}Coadd{warpTypeSuffix}",
69 storageClass="ExposureF",
70 dimensions=("tract", "patch", "skymap", "band"),
71 )
72 dcrCoadds = pipeBase.connectionTypes.Output(
73 doc="Output coadded exposure, produced by stacking input warps",
74 name="{fakesType}{outputCoaddName}Coadd{warpTypeSuffix}",
75 storageClass="ExposureF",
76 dimensions=("tract", "patch", "skymap", "band", "subfilter"),
77 multiple=True,
78 )
79 dcrNImages = pipeBase.connectionTypes.Output(
80 doc="Output image of number of input images per pixel",
81 name="{outputCoaddName}Coadd_nImage",
82 storageClass="ImageU",
83 dimensions=("tract", "patch", "skymap", "band", "subfilter"),
84 multiple=True,
85 )
86
87 def __init__(self, *, config=None):
88 super().__init__(config=config)
89 if not config.doWrite:
90 self.outputs.remove("dcrCoadds")
91 if not config.doNImage:
92 self.outputs.remove("dcrNImages")
93 # Remove outputs inherited from ``AssembleCoaddConnections`` that are not used
94 self.outputs.remove("coaddExposure")
95 self.outputs.remove("nImage")
96
97
98class DcrAssembleCoaddConfig(CompareWarpAssembleCoaddConfig,
99 pipelineConnections=DcrAssembleCoaddConnections):
100 dcrNumSubfilters = pexConfig.Field(
101 dtype=int,
102 doc="Number of sub-filters to forward model chromatic effects to fit the supplied exposures.",
103 default=3,
104 )
105 maxNumIter = pexConfig.Field(
106 dtype=int,
107 optional=True,
108 doc="Maximum number of iterations of forward modeling.",
109 default=None,
110 )
111 minNumIter = pexConfig.Field(
112 dtype=int,
113 optional=True,
114 doc="Minimum number of iterations of forward modeling.",
115 default=None,
116 )
117 convergenceThreshold = pexConfig.Field(
118 dtype=float,
119 doc="Target relative change in convergence between iterations of forward modeling.",
120 default=0.001,
121 )
122 useConvergence = pexConfig.Field(
123 dtype=bool,
124 doc="Use convergence test as a forward modeling end condition?"
125 "If not set, skips calculating convergence and runs for ``maxNumIter`` iterations",
126 default=True,
127 )
128 baseGain = pexConfig.Field(
129 dtype=float,
130 optional=True,
131 doc="Relative weight to give the new solution vs. the last solution when updating the model."
132 "A value of 1.0 gives equal weight to both solutions."
133 "Small values imply slower convergence of the solution, but can "
134 "help prevent overshooting and failures in the fit."
135 "If ``baseGain`` is None, a conservative gain "
136 "will be calculated from the number of subfilters. ",
137 default=None,
138 )
139 useProgressiveGain = pexConfig.Field(
140 dtype=bool,
141 doc="Use a gain that slowly increases above ``baseGain`` to accelerate convergence? "
142 "When calculating the next gain, we use up to 5 previous gains and convergence values."
143 "Can be set to False to force the model to change at the rate of ``baseGain``. ",
144 default=True,
145 )
146 doAirmassWeight = pexConfig.Field(
147 dtype=bool,
148 doc="Weight exposures by airmass? Useful if there are relatively few high-airmass observations.",
149 default=False,
150 )
151 modelWeightsWidth = pexConfig.Field(
152 dtype=float,
153 doc="Width of the region around detected sources to include in the DcrModel.",
154 default=3,
155 )
156 useModelWeights = pexConfig.Field(
157 dtype=bool,
158 doc="Width of the region around detected sources to include in the DcrModel.",
159 default=True,
160 )
161 splitSubfilters = pexConfig.Field(
162 dtype=bool,
163 doc="Calculate DCR for two evenly-spaced wavelengths in each subfilter."
164 "Instead of at the midpoint",
165 default=True,
166 )
167 splitThreshold = pexConfig.Field(
168 dtype=float,
169 doc="Minimum DCR difference within a subfilter to use ``splitSubfilters``, in pixels."
170 "Set to 0 to always split the subfilters.",
171 default=0.1,
172 )
173 regularizeModelIterations = pexConfig.Field(
174 dtype=float,
175 doc="Maximum relative change of the model allowed between iterations."
176 "Set to zero to disable.",
177 default=2.,
178 )
179 regularizeModelFrequency = pexConfig.Field(
180 dtype=float,
181 doc="Maximum relative change of the model allowed between subfilters."
182 "Set to zero to disable.",
183 default=4.,
184 )
185 convergenceMaskPlanes = pexConfig.ListField(
186 dtype=str,
187 default=["DETECTED"],
188 doc="Mask planes to use to calculate convergence."
189 )
190 regularizationWidth = pexConfig.Field(
191 dtype=int,
192 default=2,
193 doc="Minimum radius of a region to include in regularization, in pixels."
194 )
195 imageInterpOrder = pexConfig.Field(
196 dtype=int,
197 doc="The order of the spline interpolation used to shift the image plane.",
198 default=1,
199 )
200 accelerateModel = pexConfig.Field(
201 dtype=float,
202 doc="Factor to amplify the differences between model planes by to speed convergence.",
203 default=3,
204 )
205 doCalculatePsf = pexConfig.Field(
206 dtype=bool,
207 doc="Set to detect stars and recalculate the PSF from the final coadd."
208 "Otherwise the PSF is estimated from a selection of the best input exposures",
209 default=False,
210 )
211 detectPsfSources = pexConfig.ConfigurableField(
212 target=measAlg.SourceDetectionTask,
213 doc="Task to detect sources for PSF measurement, if ``doCalculatePsf`` is set.",
214 )
215 measurePsfSources = pexConfig.ConfigurableField(
216 target=SingleFrameMeasurementTask,
217 doc="Task to measure sources for PSF measurement, if ``doCalculatePsf`` is set."
218 )
219 measurePsf = pexConfig.ConfigurableField(
220 target=MeasurePsfTask,
221 doc="Task to measure the PSF of the coadd, if ``doCalculatePsf`` is set.",
222 )
223 effectiveWavelength = pexConfig.Field(
224 doc="Effective wavelength of the filter, in nm."
225 "Required if transmission curves aren't used."
226 "Support for using transmission curves is to be added in DM-13668.",
227 dtype=float,
228 )
229 bandwidth = pexConfig.Field(
230 doc="Bandwidth of the physical filter, in nm."
231 "Required if transmission curves aren't used."
232 "Support for using transmission curves is to be added in DM-13668.",
233 dtype=float,
234 )
235
236 def setDefaults(self):
237 CompareWarpAssembleCoaddConfig.setDefaults(self)
238 self.assembleStaticSkyModel.retarget(CompareWarpAssembleCoaddTask)
239 self.doNImage = True
240 self.assembleStaticSkyModel.warpType = self.warpType
241 # The deepCoadd and nImage files will be overwritten by this Task, so don't write them the first time
242 self.assembleStaticSkyModel.doNImage = False
243 self.assembleStaticSkyModel.doWrite = False
244 self.detectPsfSources.returnOriginalFootprints = False
245 self.detectPsfSources.thresholdPolarity = "positive"
246 # Only use bright sources for PSF measurement
247 self.detectPsfSources.thresholdValue = 50
248 self.detectPsfSources.nSigmaToGrow = 2
249 # A valid star for PSF measurement should at least fill 5x5 pixels
250 self.detectPsfSources.minPixels = 25
251 # Use the variance plane to calculate signal to noise
252 self.detectPsfSources.thresholdType = "pixel_stdev"
253 # The signal to noise limit is good enough, while the flux limit is set
254 # in dimensionless units and may not be appropriate for all data sets.
255 self.measurePsf.starSelector["objectSize"].doFluxLimit = False
256 # Ensure psf candidate size is as large as piff psf size.
257 if (self.doCalculatePsf and self.measurePsf.psfDeterminer.name == "piff"
258 and self.psfDeterminer["piff"].kernelSize > self.makePsfCandidates.kernelSize):
259 self.makePsfCandidates.kernelSize = self.psfDeterminer["piff"].kernelSize
260
261
262class DcrAssembleCoaddTask(CompareWarpAssembleCoaddTask):
263 """Assemble DCR coadded images from a set of warps.
264
265 Attributes
266 ----------
267 bufferSize : `int`
268 The number of pixels to grow each subregion by to allow for DCR.
269
270 Notes
271 -----
272 As with AssembleCoaddTask, we want to assemble a coadded image from a set of
273 Warps (also called coadded temporary exposures), including the effects of
274 Differential Chromatic Refraction (DCR).
275 For full details of the mathematics and algorithm, please see
276 DMTN-037: DCR-matched template generation (https://dmtn-037.lsst.io).
277
278 This Task produces a DCR-corrected deepCoadd, as well as a dcrCoadd for
279 each subfilter used in the iterative calculation.
280 It begins by dividing the bandpass-defining filter into N equal bandwidth
281 "subfilters", and divides the flux in each pixel from an initial coadd
282 equally into each as a "dcrModel". Because the airmass and parallactic
283 angle of each individual exposure is known, we can calculate the shift
284 relative to the center of the band in each subfilter due to DCR. For each
285 exposure we apply this shift as a linear transformation to the dcrModels
286 and stack the results to produce a DCR-matched exposure. The matched
287 exposures are subtracted from the input exposures to produce a set of
288 residual images, and these residuals are reverse shifted for each
289 exposures' subfilters and stacked. The shifted and stacked residuals are
290 added to the dcrModels to produce a new estimate of the flux in each pixel
291 within each subfilter. The dcrModels are solved for iteratively, which
292 continues until the solution from a new iteration improves by less than
293 a set percentage, or a maximum number of iterations is reached.
294 Two forms of regularization are employed to reduce unphysical results.
295 First, the new solution is averaged with the solution from the previous
296 iteration, which mitigates oscillating solutions where the model
297 overshoots with alternating very high and low values.
298 Second, a common degeneracy when the data have a limited range of airmass or
299 parallactic angle values is for one subfilter to be fit with very low or
300 negative values, while another subfilter is fit with very high values. This
301 typically appears in the form of holes next to sources in one subfilter,
302 and corresponding extended wings in another. Because each subfilter has
303 a narrow bandwidth we assume that physical sources that are above the noise
304 level will not vary in flux by more than a factor of `frequencyClampFactor`
305 between subfilters, and pixels that have flux deviations larger than that
306 factor will have the excess flux distributed evenly among all subfilters.
307 If `splitSubfilters` is set, then each subfilter will be further sub-
308 divided during the forward modeling step (only). This approximates using
309 a higher number of subfilters that may be necessary for high airmass
310 observations, but does not increase the number of free parameters in the
311 fit. This is needed when there are high airmass observations which would
312 otherwise have significant DCR even within a subfilter. Because calculating
313 the shifted images takes most of the time, splitting the subfilters is
314 turned off by way of the `splitThreshold` option for low-airmass
315 observations that do not suffer from DCR within a subfilter.
316 """
317
318 ConfigClass = DcrAssembleCoaddConfig
319 _DefaultName = "dcrAssembleCoadd"
320
321 def __init__(self, *args, **kwargs):
322 super().__init__(*args, **kwargs)
323 if self.config.doCalculatePsf:
324 self.schema = afwTable.SourceTable.makeMinimalSchema()
325 self.makeSubtask("detectPsfSources", schema=self.schema)
326 self.makeSubtask("measurePsfSources", schema=self.schema)
327 self.makeSubtask("measurePsf", schema=self.schema)
328
329 @utils.inheritDoc(pipeBase.PipelineTask)
330 def runQuantum(self, butlerQC, inputRefs, outputRefs):
331 # Docstring to be formatted with info from PipelineTask.runQuantum
332 """
333 Notes
334 -----
335 Assemble a coadd from a set of Warps.
336
337 PipelineTask (Gen3) entry point to Coadd a set of Warps.
338 Analogous to `runDataRef`, it prepares all the data products to be
339 passed to `run`, and processes the results before returning a struct
340 of results to be written out. AssembleCoadd cannot fit all Warps in memory.
341 Therefore, its inputs are accessed subregion by subregion
342 by the Gen3 `DeferredDatasetHandle` that is analagous to the Gen2
343 `lsst.daf.persistence.ButlerDataRef`. Any updates to this method should
344 correspond to an update in `runDataRef` while both entry points
345 are used.
346 """
347 inputData = butlerQC.get(inputRefs)
348
349 # Construct skyInfo expected by run
350 # Do not remove skyMap from inputData in case makeSupplementaryDataGen3 needs it
351 skyMap = inputData["skyMap"]
352 outputDataId = butlerQC.quantum.dataId
353
354 inputData['skyInfo'] = makeSkyInfo(skyMap,
355 tractId=outputDataId['tract'],
356 patchId=outputDataId['patch'])
357
358 # Construct list of input Deferred Datasets
359 # These quack a bit like like Gen2 DataRefs
360 warpRefList = inputData['inputWarps']
361 # Perform same middle steps as `runDataRef` does
362 inputs = self.prepareInputs(warpRefList)
363 self.log.info("Found %d %s", len(inputs.tempExpRefList),
364 self.getTempExpDatasetName(self.warpType))
365 if len(inputs.tempExpRefList) == 0:
366 self.log.warning("No coadd temporary exposures found")
367 return
368
369 supplementaryData = self.makeSupplementaryDataGen3(butlerQC, inputRefs, outputRefs)
370 retStruct = self.run(inputData['skyInfo'], inputs.tempExpRefList, inputs.imageScalerList,
371 inputs.weightList, supplementaryData=supplementaryData)
372
373 inputData.setdefault('brightObjectMask', None)
374 for subfilter in range(self.config.dcrNumSubfilters):
375 # Use the PSF of the stacked dcrModel, and do not recalculate the PSF for each subfilter
376 retStruct.dcrCoadds[subfilter].setPsf(retStruct.coaddExposure.getPsf())
377 self.processResults(retStruct.dcrCoadds[subfilter], inputData['brightObjectMask'], outputDataId)
378
379 if self.config.doWrite:
380 butlerQC.put(retStruct, outputRefs)
381 return retStruct
382
383 @timeMethod
384 def runDataRef(self, dataRef, selectDataList=None, warpRefList=None):
385 """Assemble a coadd from a set of warps.
386
387 Coadd a set of Warps. Compute weights to be applied to each Warp and
388 find scalings to match the photometric zeropoint to a reference Warp.
389 Assemble the Warps using run method.
390 Forward model chromatic effects across multiple subfilters,
391 and subtract from the input Warps to build sets of residuals.
392 Use the residuals to construct a new ``DcrModel`` for each subfilter,
393 and iterate until the model converges.
394 Interpolate over NaNs and optionally write the coadd to disk.
395 Return the coadded exposure.
396
397 Parameters
398 ----------
400 Data reference defining the patch for coaddition and the
401 reference Warp
402 selectDataList : `list` of `lsst.daf.persistence.ButlerDataRef`
403 List of data references to warps. Data to be coadded will be
404 selected from this list based on overlap with the patch defined by
405 the data reference.
406
407 Returns
408 -------
409 results : `lsst.pipe.base.Struct`
410 The Struct contains the following fields:
411
412 - ``coaddExposure``: coadded exposure (`lsst.afw.image.Exposure`)
413 - ``nImage``: exposure count image (`lsst.afw.image.ImageU`)
414 - ``dcrCoadds``: `list` of coadded exposures for each subfilter
415 - ``dcrNImages``: `list` of exposure count images for each subfilter
416 """
417 if (selectDataList is None and warpRefList is None) or (selectDataList and warpRefList):
418 raise RuntimeError("runDataRef must be supplied either a selectDataList or warpRefList")
419
420 skyInfo = self.getSkyInfo(dataRef)
421 if warpRefList is None:
422 calExpRefList = self.selectExposures(dataRef, skyInfo, selectDataList=selectDataList)
423 if len(calExpRefList) == 0:
424 self.log.warning("No exposures to coadd")
425 return
426 self.log.info("Coadding %d exposures", len(calExpRefList))
427
428 warpRefList = self.getTempExpRefList(dataRef, calExpRefList)
429
430 inputData = self.prepareInputs(warpRefList)
431 self.log.info("Found %d %s", len(inputData.tempExpRefList),
432 self.getTempExpDatasetName(self.warpType))
433 if len(inputData.tempExpRefList) == 0:
434 self.log.warning("No coadd temporary exposures found")
435 return
436
437 supplementaryData = self.makeSupplementaryData(dataRef, warpRefList=inputData.tempExpRefList)
438
439 results = self.run(skyInfo, inputData.tempExpRefList, inputData.imageScalerList,
440 inputData.weightList, supplementaryData=supplementaryData)
441 if results is None:
442 self.log.warning("Could not construct DcrModel for patch %s: no data to coadd.",
443 skyInfo.patchInfo.getIndex())
444 return
445
446 if self.config.doCalculatePsf:
447 self.measureCoaddPsf(results.coaddExposure)
448 brightObjects = self.readBrightObjectMasks(dataRef) if self.config.doMaskBrightObjects else None
449 for subfilter in range(self.config.dcrNumSubfilters):
450 # Use the PSF of the stacked dcrModel, and do not recalculate the PSF for each subfilter
451 results.dcrCoadds[subfilter].setPsf(results.coaddExposure.getPsf())
452 self.processResults(results.dcrCoadds[subfilter],
453 brightObjectMasks=brightObjects, dataId=dataRef.dataId)
454 if self.config.doWrite:
455 self.log.info("Persisting dcrCoadd")
456 dataRef.put(results.dcrCoadds[subfilter], "dcrCoadd", subfilter=subfilter,
457 numSubfilters=self.config.dcrNumSubfilters)
458 if self.config.doNImage and results.dcrNImages is not None:
459 dataRef.put(results.dcrNImages[subfilter], "dcrCoadd_nImage", subfilter=subfilter,
460 numSubfilters=self.config.dcrNumSubfilters)
461
462 return results
463
464 @utils.inheritDoc(AssembleCoaddTask)
465 def makeSupplementaryDataGen3(self, butlerQC, inputRefs, outputRefs):
466 """Load the previously-generated template coadd.
467
468 This can be removed entirely once we no longer support the Gen 2 butler.
469
470 Returns
471 -------
472 templateCoadd : `lsst.pipe.base.Struct`
473 Result struct with components:
474
475 - ``templateCoadd``: coadded exposure (`lsst.afw.image.ExposureF`)
476 """
477 templateCoadd = butlerQC.get(inputRefs.templateExposure)
478
479 return pipeBase.Struct(templateCoadd=templateCoadd)
480
481 def measureCoaddPsf(self, coaddExposure):
482 """Detect sources on the coadd exposure and measure the final PSF.
483
484 Parameters
485 ----------
486 coaddExposure : `lsst.afw.image.Exposure`
487 The final coadded exposure.
488 """
489 table = afwTable.SourceTable.make(self.schema)
490 detResults = self.detectPsfSources.run(table, coaddExposure, clearMask=False)
491 coaddSources = detResults.sources
492 self.measurePsfSources.run(
493 measCat=coaddSources,
494 exposure=coaddExposure
495 )
496 # Measure the PSF on the stacked subfilter coadds if possible.
497 # We should already have a decent estimate of the coadd PSF, however,
498 # so in case of any errors simply log them as a warning and use the
499 # default PSF.
500 try:
501 psfResults = self.measurePsf.run(coaddExposure, coaddSources)
502 except Exception as e:
503 self.log.warning("Unable to calculate PSF, using default coadd PSF: %s", e)
504 else:
505 coaddExposure.setPsf(psfResults.psf)
506
507 def prepareDcrInputs(self, templateCoadd, warpRefList, weightList):
508 """Prepare the DCR coadd by iterating through the visitInfo of the input warps.
509
510 Sets the property ``bufferSize``.
511
512 Parameters
513 ----------
514 templateCoadd : `lsst.afw.image.ExposureF`
515 The initial coadd exposure before accounting for DCR.
516 warpRefList : `list` of `lsst.daf.butler.DeferredDatasetHandle` or
518 The data references to the input warped exposures.
519 weightList : `list` of `float`
520 The weight to give each input exposure in the coadd
521 Will be modified in place if ``doAirmassWeight`` is set.
522
523 Returns
524 -------
525 dcrModels : `lsst.pipe.tasks.DcrModel`
526 Best fit model of the true sky after correcting chromatic effects.
527
528 Raises
529 ------
530 NotImplementedError
531 If ``lambdaMin`` is missing from the Mapper class of the obs package being used.
532 """
533 sigma2fwhm = 2.*np.sqrt(2.*np.log(2.))
534 filterLabel = templateCoadd.getFilter()
535 tempExpName = self.getTempExpDatasetName(self.warpType)
536 dcrShifts = []
537 airmassDict = {}
538 angleDict = {}
539 psfSizeDict = {}
540 for visitNum, warpExpRef in enumerate(warpRefList):
541 if isinstance(warpExpRef, DeferredDatasetHandle):
542 # Gen 3 API
543 visitInfo = warpExpRef.get(component="visitInfo")
544 psf = warpExpRef.get(component="psf")
545 else:
546 # Gen 2 API. Delete this when Gen 2 retired
547 visitInfo = warpExpRef.get(tempExpName + "_visitInfo")
548 psf = warpExpRef.get(tempExpName).getPsf()
549 visit = warpExpRef.dataId["visit"]
550 # Just need a rough estimate; average positions are fine
551 psfAvgPos = psf.getAveragePosition()
552 psfSize = psf.computeShape(psfAvgPos).getDeterminantRadius()*sigma2fwhm
553 airmass = visitInfo.getBoresightAirmass()
554 parallacticAngle = visitInfo.getBoresightParAngle().asDegrees()
555 airmassDict[visit] = airmass
556 angleDict[visit] = parallacticAngle
557 psfSizeDict[visit] = psfSize
558 if self.config.doAirmassWeight:
559 weightList[visitNum] *= airmass
560 dcrShifts.append(np.max(np.abs(calculateDcr(visitInfo, templateCoadd.getWcs(),
561 self.config.effectiveWavelength,
562 self.config.bandwidth,
563 self.config.dcrNumSubfilters))))
564 self.log.info("Selected airmasses:\n%s", airmassDict)
565 self.log.info("Selected parallactic angles:\n%s", angleDict)
566 self.log.info("Selected PSF sizes:\n%s", psfSizeDict)
567 self.bufferSize = int(np.ceil(np.max(dcrShifts)) + 1)
568 try:
569 psf = self.selectCoaddPsf(templateCoadd, warpRefList)
570 except Exception as e:
571 self.log.warning("Unable to calculate restricted PSF, using default coadd PSF: %s", e)
572 else:
573 psf = templateCoadd.getPsf()
574 dcrModels = DcrModel.fromImage(templateCoadd.maskedImage,
575 self.config.dcrNumSubfilters,
576 effectiveWavelength=self.config.effectiveWavelength,
577 bandwidth=self.config.bandwidth,
578 wcs=templateCoadd.getWcs(),
579 filterLabel=filterLabel,
580 psf=psf)
581 return dcrModels
582
583 @timeMethod
584 def run(self, skyInfo, warpRefList, imageScalerList, weightList,
585 supplementaryData=None):
586 """Assemble the coadd.
587
588 Requires additional inputs Struct ``supplementaryData`` to contain a
589 ``templateCoadd`` that serves as the model of the static sky.
590
591 Find artifacts and apply them to the warps' masks creating a list of
592 alternative masks with a new "CLIPPED" plane and updated "NO_DATA" plane
593 Then pass these alternative masks to the base class's assemble method.
594
595 Divide the ``templateCoadd`` evenly between each subfilter of a
596 ``DcrModel`` as the starting best estimate of the true wavelength-
597 dependent sky. Forward model the ``DcrModel`` using the known
598 chromatic effects in each subfilter and calculate a convergence metric
599 based on how well the modeled template matches the input warps. If
600 the convergence has not yet reached the desired threshold, then shift
601 and stack the residual images to build a new ``DcrModel``. Apply
602 conditioning to prevent oscillating solutions between iterations or
603 between subfilters.
604
605 Once the ``DcrModel`` reaches convergence or the maximum number of
606 iterations has been reached, fill the metadata for each subfilter
607 image and make them proper ``coaddExposure``s.
608
609 Parameters
610 ----------
611 skyInfo : `lsst.pipe.base.Struct`
612 Patch geometry information, from getSkyInfo
613 warpRefList : `list` of `lsst.daf.butler.DeferredDatasetHandle` or
615 The data references to the input warped exposures.
616 imageScalerList : `list` of `lsst.pipe.task.ImageScaler`
617 The image scalars correct for the zero point of the exposures.
618 weightList : `list` of `float`
619 The weight to give each input exposure in the coadd
620 supplementaryData : `lsst.pipe.base.Struct`
621 Result struct returned by ``makeSupplementaryData`` with components:
622
623 - ``templateCoadd``: coadded exposure (`lsst.afw.image.Exposure`)
624
625 Returns
626 -------
627 result : `lsst.pipe.base.Struct`
628 Result struct with components:
629
630 - ``coaddExposure``: coadded exposure (`lsst.afw.image.Exposure`)
631 - ``nImage``: exposure count image (`lsst.afw.image.ImageU`)
632 - ``dcrCoadds``: `list` of coadded exposures for each subfilter
633 - ``dcrNImages``: `list` of exposure count images for each subfilter
634 """
635 minNumIter = self.config.minNumIter or self.config.dcrNumSubfilters
636 maxNumIter = self.config.maxNumIter or self.config.dcrNumSubfilters*3
637 templateCoadd = supplementaryData.templateCoadd
638 baseMask = templateCoadd.mask.clone()
639 # The variance plane is for each subfilter
640 # and should be proportionately lower than the full-band image
641 baseVariance = templateCoadd.variance.clone()
642 baseVariance /= self.config.dcrNumSubfilters
643 spanSetMaskList = self.findArtifacts(templateCoadd, warpRefList, imageScalerList)
644 # Note that the mask gets cleared in ``findArtifacts``, but we want to preserve the mask.
645 templateCoadd.setMask(baseMask)
646 badMaskPlanes = self.config.badMaskPlanes[:]
647 # Note that is important that we do not add "CLIPPED" to ``badMaskPlanes``
648 # This is because pixels in observations that are significantly affect by DCR
649 # are likely to have many pixels that are both "DETECTED" and "CLIPPED",
650 # but those are necessary to constrain the DCR model.
651 badPixelMask = templateCoadd.mask.getPlaneBitMask(badMaskPlanes)
652
653 stats = self.prepareStats(mask=badPixelMask)
654 dcrModels = self.prepareDcrInputs(templateCoadd, warpRefList, weightList)
655 if self.config.doNImage:
656 dcrNImages, dcrWeights = self.calculateNImage(dcrModels, skyInfo.bbox, warpRefList,
657 spanSetMaskList, stats.ctrl)
658 nImage = afwImage.ImageU(skyInfo.bbox)
659 # Note that this nImage will be a factor of dcrNumSubfilters higher than
660 # the nImage returned by assembleCoadd for most pixels. This is because each
661 # subfilter may have a different nImage, and fractional values are not allowed.
662 for dcrNImage in dcrNImages:
663 nImage += dcrNImage
664 else:
665 dcrNImages = None
666
667 subregionSize = geom.Extent2I(*self.config.subregionSize)
668 nSubregions = (ceil(skyInfo.bbox.getHeight()/subregionSize[1])
669 * ceil(skyInfo.bbox.getWidth()/subregionSize[0]))
670 subIter = 0
671 for subBBox in self._subBBoxIter(skyInfo.bbox, subregionSize):
672 modelIter = 0
673 subIter += 1
674 self.log.info("Computing coadd over patch %s subregion %s of %s: %s",
675 skyInfo.patchInfo.getIndex(), subIter, nSubregions, subBBox)
676 dcrBBox = geom.Box2I(subBBox)
677 dcrBBox.grow(self.bufferSize)
678 dcrBBox.clip(dcrModels.bbox)
679 modelWeights = self.calculateModelWeights(dcrModels, dcrBBox)
680 subExposures = self.loadSubExposures(dcrBBox, stats.ctrl, warpRefList,
681 imageScalerList, spanSetMaskList)
682 convergenceMetric = self.calculateConvergence(dcrModels, subExposures, subBBox,
683 warpRefList, weightList, stats.ctrl)
684 self.log.info("Initial convergence : %s", convergenceMetric)
685 convergenceList = [convergenceMetric]
686 gainList = []
687 convergenceCheck = 1.
688 refImage = templateCoadd.image
689 while (convergenceCheck > self.config.convergenceThreshold or modelIter <= minNumIter):
690 gain = self.calculateGain(convergenceList, gainList)
691 self.dcrAssembleSubregion(dcrModels, subExposures, subBBox, dcrBBox, warpRefList,
692 stats.ctrl, convergenceMetric, gain,
693 modelWeights, refImage, dcrWeights)
694 if self.config.useConvergence:
695 convergenceMetric = self.calculateConvergence(dcrModels, subExposures, subBBox,
696 warpRefList, weightList, stats.ctrl)
697 if convergenceMetric == 0:
698 self.log.warning("Coadd patch %s subregion %s had convergence metric of 0.0 which is "
699 "most likely due to there being no valid data in the region.",
700 skyInfo.patchInfo.getIndex(), subIter)
701 break
702 convergenceCheck = (convergenceList[-1] - convergenceMetric)/convergenceMetric
703 if (convergenceCheck < 0) & (modelIter > minNumIter):
704 self.log.warning("Coadd patch %s subregion %s diverged before reaching maximum "
705 "iterations or desired convergence improvement of %s."
706 " Divergence: %s",
707 skyInfo.patchInfo.getIndex(), subIter,
708 self.config.convergenceThreshold, convergenceCheck)
709 break
710 convergenceList.append(convergenceMetric)
711 if modelIter > maxNumIter:
712 if self.config.useConvergence:
713 self.log.warning("Coadd patch %s subregion %s reached maximum iterations "
714 "before reaching desired convergence improvement of %s."
715 " Final convergence improvement: %s",
716 skyInfo.patchInfo.getIndex(), subIter,
717 self.config.convergenceThreshold, convergenceCheck)
718 break
719
720 if self.config.useConvergence:
721 self.log.info("Iteration %s with convergence metric %s, %.4f%% improvement (gain: %.2f)",
722 modelIter, convergenceMetric, 100.*convergenceCheck, gain)
723 modelIter += 1
724 else:
725 if self.config.useConvergence:
726 self.log.info("Coadd patch %s subregion %s finished with "
727 "convergence metric %s after %s iterations",
728 skyInfo.patchInfo.getIndex(), subIter, convergenceMetric, modelIter)
729 else:
730 self.log.info("Coadd patch %s subregion %s finished after %s iterations",
731 skyInfo.patchInfo.getIndex(), subIter, modelIter)
732 if self.config.useConvergence and convergenceMetric > 0:
733 self.log.info("Final convergence improvement was %.4f%% overall",
734 100*(convergenceList[0] - convergenceMetric)/convergenceMetric)
735
736 dcrCoadds = self.fillCoadd(dcrModels, skyInfo, warpRefList, weightList,
737 calibration=self.scaleZeroPoint.getPhotoCalib(),
738 coaddInputs=templateCoadd.getInfo().getCoaddInputs(),
739 mask=baseMask,
740 variance=baseVariance)
741 coaddExposure = self.stackCoadd(dcrCoadds)
742 return pipeBase.Struct(coaddExposure=coaddExposure, nImage=nImage,
743 dcrCoadds=dcrCoadds, dcrNImages=dcrNImages)
744
745 def calculateNImage(self, dcrModels, bbox, warpRefList, spanSetMaskList, statsCtrl):
746 """Calculate the number of exposures contributing to each subfilter.
747
748 Parameters
749 ----------
750 dcrModels : `lsst.pipe.tasks.DcrModel`
751 Best fit model of the true sky after correcting chromatic effects.
752 bbox : `lsst.geom.box.Box2I`
753 Bounding box of the patch to coadd.
754 warpRefList : `list` of `lsst.daf.butler.DeferredDatasetHandle` or
756 The data references to the input warped exposures.
757 spanSetMaskList : `list` of `dict` containing spanSet lists, or None
758 Each element of the `dict` contains the new mask plane name
759 (e.g. "CLIPPED and/or "NO_DATA") as the key,
760 and the list of SpanSets to apply to the mask.
762 Statistics control object for coadd
763
764 Returns
765 -------
766 dcrNImages : `list` of `lsst.afw.image.ImageU`
767 List of exposure count images for each subfilter
768 dcrWeights : `list` of `lsst.afw.image.ImageF`
769 Per-pixel weights for each subfilter.
770 Equal to 1/(number of unmasked images contributing to each pixel).
771 """
772 dcrNImages = [afwImage.ImageU(bbox) for subfilter in range(self.config.dcrNumSubfilters)]
773 dcrWeights = [afwImage.ImageF(bbox) for subfilter in range(self.config.dcrNumSubfilters)]
774 tempExpName = self.getTempExpDatasetName(self.warpType)
775 for warpExpRef, altMaskSpans in zip(warpRefList, spanSetMaskList):
776 if isinstance(warpExpRef, DeferredDatasetHandle):
777 # Gen 3 API
778 exposure = warpExpRef.get(parameters={'bbox': bbox})
779 else:
780 # Gen 2 API. Delete this when Gen 2 retired
781 exposure = warpExpRef.get(tempExpName + "_sub", bbox=bbox)
782 visitInfo = exposure.getInfo().getVisitInfo()
783 wcs = exposure.getInfo().getWcs()
784 mask = exposure.mask
785 if altMaskSpans is not None:
786 self.applyAltMaskPlanes(mask, altMaskSpans)
787 weightImage = np.zeros_like(exposure.image.array)
788 weightImage[(mask.array & statsCtrl.getAndMask()) == 0] = 1.
789 # The weights must be shifted in exactly the same way as the residuals,
790 # because they will be used as the denominator in the weighted average of residuals.
791 weightsGenerator = self.dcrResiduals(weightImage, visitInfo, wcs,
792 dcrModels.effectiveWavelength, dcrModels.bandwidth)
793 for shiftedWeights, dcrNImage, dcrWeight in zip(weightsGenerator, dcrNImages, dcrWeights):
794 dcrNImage.array += np.rint(shiftedWeights).astype(dcrNImage.array.dtype)
795 dcrWeight.array += shiftedWeights
796 # Exclude any pixels that don't have at least one exposure contributing in all subfilters
797 weightsThreshold = 1.
798 goodPix = dcrWeights[0].array > weightsThreshold
799 for weights in dcrWeights[1:]:
800 goodPix = (weights.array > weightsThreshold) & goodPix
801 for subfilter in range(self.config.dcrNumSubfilters):
802 dcrWeights[subfilter].array[goodPix] = 1./dcrWeights[subfilter].array[goodPix]
803 dcrWeights[subfilter].array[~goodPix] = 0.
804 dcrNImages[subfilter].array[~goodPix] = 0
805 return (dcrNImages, dcrWeights)
806
807 def dcrAssembleSubregion(self, dcrModels, subExposures, bbox, dcrBBox, warpRefList,
808 statsCtrl, convergenceMetric,
809 gain, modelWeights, refImage, dcrWeights):
810 """Assemble the DCR coadd for a sub-region.
811
812 Build a DCR-matched template for each input exposure, then shift the
813 residuals according to the DCR in each subfilter.
814 Stack the shifted residuals and apply them as a correction to the
815 solution from the previous iteration.
816 Restrict the new model solutions from varying by more than a factor of
817 `modelClampFactor` from the last solution, and additionally restrict the
818 individual subfilter models from varying by more than a factor of
819 `frequencyClampFactor` from their average.
820 Finally, mitigate potentially oscillating solutions by averaging the new
821 solution with the solution from the previous iteration, weighted by
822 their convergence metric.
823
824 Parameters
825 ----------
826 dcrModels : `lsst.pipe.tasks.DcrModel`
827 Best fit model of the true sky after correcting chromatic effects.
828 subExposures : `dict` of `lsst.afw.image.ExposureF`
829 The pre-loaded exposures for the current subregion.
830 bbox : `lsst.geom.box.Box2I`
831 Bounding box of the subregion to coadd.
832 dcrBBox : `lsst.geom.box.Box2I`
833 Sub-region of the coadd which includes a buffer to allow for DCR.
834 warpRefList : `list` of `lsst.daf.butler.DeferredDatasetHandle` or
836 The data references to the input warped exposures.
838 Statistics control object for coadd
839 convergenceMetric : `float`
840 Quality of fit metric for the matched templates of the input images.
841 gain : `float`, optional
842 Relative weight to give the new solution when updating the model.
843 modelWeights : `numpy.ndarray` or `float`
844 A 2D array of weight values that tapers smoothly to zero away from detected sources.
845 Set to a placeholder value of 1.0 if ``self.config.useModelWeights`` is False.
846 refImage : `lsst.afw.image.Image`
847 A reference image used to supply the default pixel values.
848 dcrWeights : `list` of `lsst.afw.image.Image`
849 Per-pixel weights for each subfilter.
850 Equal to 1/(number of unmasked images contributing to each pixel).
851 """
852 residualGeneratorList = []
853
854 for warpExpRef in warpRefList:
855 visit = warpExpRef.dataId["visit"]
856 exposure = subExposures[visit]
857 visitInfo = exposure.getInfo().getVisitInfo()
858 wcs = exposure.getInfo().getWcs()
859 templateImage = dcrModels.buildMatchedTemplate(exposure=exposure,
860 bbox=exposure.getBBox(),
861 order=self.config.imageInterpOrder,
862 splitSubfilters=self.config.splitSubfilters,
863 splitThreshold=self.config.splitThreshold,
864 amplifyModel=self.config.accelerateModel)
865 residual = exposure.image.array - templateImage.array
866 # Note that the variance plane here is used to store weights, not the actual variance
867 residual *= exposure.variance.array
868 # The residuals are stored as a list of generators.
869 # This allows the residual for a given subfilter and exposure to be created
870 # on the fly, instead of needing to store them all in memory.
871 residualGeneratorList.append(self.dcrResiduals(residual, visitInfo, wcs,
872 dcrModels.effectiveWavelength,
873 dcrModels.bandwidth))
874
875 dcrSubModelOut = self.newModelFromResidual(dcrModels, residualGeneratorList, dcrBBox, statsCtrl,
876 gain=gain,
877 modelWeights=modelWeights,
878 refImage=refImage,
879 dcrWeights=dcrWeights)
880 dcrModels.assign(dcrSubModelOut, bbox)
881
882 def dcrResiduals(self, residual, visitInfo, wcs, effectiveWavelength, bandwidth):
883 """Prepare a residual image for stacking in each subfilter by applying the reverse DCR shifts.
884
885 Parameters
886 ----------
887 residual : `numpy.ndarray`
888 The residual masked image for one exposure,
889 after subtracting the matched template
890 visitInfo : `lsst.afw.image.VisitInfo`
891 Metadata for the exposure.
893 Coordinate system definition (wcs) for the exposure.
894
895 Yields
896 ------
897 residualImage : `numpy.ndarray`
898 The residual image for the next subfilter, shifted for DCR.
899 """
900 if self.config.imageInterpOrder > 1:
901 # Pre-calculate the spline-filtered residual image, so that step can be
902 # skipped in the shift calculation in `applyDcr`.
903 filteredResidual = ndimage.spline_filter(residual, order=self.config.imageInterpOrder)
904 else:
905 # No need to prefilter if order=1 (it will also raise an error)
906 filteredResidual = residual
907 # Note that `splitSubfilters` is always turned off in the reverse direction.
908 # This option introduces additional blurring if applied to the residuals.
909 dcrShift = calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, self.config.dcrNumSubfilters,
910 splitSubfilters=False)
911 for dcr in dcrShift:
912 yield applyDcr(filteredResidual, dcr, useInverse=True, splitSubfilters=False,
913 doPrefilter=False, order=self.config.imageInterpOrder)
914
915 def newModelFromResidual(self, dcrModels, residualGeneratorList, dcrBBox, statsCtrl,
916 gain, modelWeights, refImage, dcrWeights):
917 """Calculate a new DcrModel from a set of image residuals.
918
919 Parameters
920 ----------
921 dcrModels : `lsst.pipe.tasks.DcrModel`
922 Current model of the true sky after correcting chromatic effects.
923 residualGeneratorList : `generator` of `numpy.ndarray`
924 The residual image for the next subfilter, shifted for DCR.
925 dcrBBox : `lsst.geom.box.Box2I`
926 Sub-region of the coadd which includes a buffer to allow for DCR.
928 Statistics control object for coadd
929 gain : `float`
930 Relative weight to give the new solution when updating the model.
931 modelWeights : `numpy.ndarray` or `float`
932 A 2D array of weight values that tapers smoothly to zero away from detected sources.
933 Set to a placeholder value of 1.0 if ``self.config.useModelWeights`` is False.
934 refImage : `lsst.afw.image.Image`
935 A reference image used to supply the default pixel values.
936 dcrWeights : `list` of `lsst.afw.image.Image`
937 Per-pixel weights for each subfilter.
938 Equal to 1/(number of unmasked images contributing to each pixel).
939
940 Returns
941 -------
942 dcrModel : `lsst.pipe.tasks.DcrModel`
943 New model of the true sky after correcting chromatic effects.
944 """
945 newModelImages = []
946 for subfilter, model in enumerate(dcrModels):
947 residualsList = [next(residualGenerator) for residualGenerator in residualGeneratorList]
948 residual = np.sum(residualsList, axis=0)
949 residual *= dcrWeights[subfilter][dcrBBox].array
950 # `MaskedImage`s only support in-place addition, so rename for readability
951 newModel = model[dcrBBox].clone()
952 newModel.array += residual
953 # Catch any invalid values
954 badPixels = ~np.isfinite(newModel.array)
955 newModel.array[badPixels] = model[dcrBBox].array[badPixels]
956 if self.config.regularizeModelIterations > 0:
957 dcrModels.regularizeModelIter(subfilter, newModel, dcrBBox,
958 self.config.regularizeModelIterations,
959 self.config.regularizationWidth)
960 newModelImages.append(newModel)
961 if self.config.regularizeModelFrequency > 0:
962 dcrModels.regularizeModelFreq(newModelImages, dcrBBox, statsCtrl,
963 self.config.regularizeModelFrequency,
964 self.config.regularizationWidth)
965 dcrModels.conditionDcrModel(newModelImages, dcrBBox, gain=gain)
966 self.applyModelWeights(newModelImages, refImage[dcrBBox], modelWeights)
967 return DcrModel(newModelImages, dcrModels.filter, dcrModels.effectiveWavelength,
968 dcrModels.bandwidth, dcrModels.psf,
969 dcrModels.mask, dcrModels.variance)
970
971 def calculateConvergence(self, dcrModels, subExposures, bbox, warpRefList, weightList, statsCtrl):
972 """Calculate a quality of fit metric for the matched templates.
973
974 Parameters
975 ----------
976 dcrModels : `lsst.pipe.tasks.DcrModel`
977 Best fit model of the true sky after correcting chromatic effects.
978 subExposures : `dict` of `lsst.afw.image.ExposureF`
979 The pre-loaded exposures for the current subregion.
980 bbox : `lsst.geom.box.Box2I`
981 Sub-region to coadd
982 warpRefList : `list` of `lsst.daf.butler.DeferredDatasetHandle` or
984 The data references to the input warped exposures.
985 weightList : `list` of `float`
986 The weight to give each input exposure in the coadd
988 Statistics control object for coadd
989
990 Returns
991 -------
992 convergenceMetric : `float`
993 Quality of fit metric for all input exposures, within the sub-region
994 """
995 significanceImage = np.abs(dcrModels.getReferenceImage(bbox))
996 nSigma = 3.
997 significanceImage += nSigma*dcrModels.calculateNoiseCutoff(dcrModels[1], statsCtrl,
998 bufferSize=self.bufferSize)
999 if np.max(significanceImage) == 0:
1000 significanceImage += 1.
1001 weight = 0
1002 metric = 0.
1003 metricList = {}
1004 for warpExpRef, expWeight in zip(warpRefList, weightList):
1005 visit = warpExpRef.dataId["visit"]
1006 exposure = subExposures[visit][bbox]
1007 singleMetric = self.calculateSingleConvergence(dcrModels, exposure, significanceImage, statsCtrl)
1008 metric += singleMetric
1009 metricList[visit] = singleMetric
1010 weight += 1.
1011 self.log.info("Individual metrics:\n%s", metricList)
1012 return 1.0 if weight == 0.0 else metric/weight
1013
1014 def calculateSingleConvergence(self, dcrModels, exposure, significanceImage, statsCtrl):
1015 """Calculate a quality of fit metric for a single matched template.
1016
1017 Parameters
1018 ----------
1019 dcrModels : `lsst.pipe.tasks.DcrModel`
1020 Best fit model of the true sky after correcting chromatic effects.
1021 exposure : `lsst.afw.image.ExposureF`
1022 The input warped exposure to evaluate.
1023 significanceImage : `numpy.ndarray`
1024 Array of weights for each pixel corresponding to its significance
1025 for the convergence calculation.
1027 Statistics control object for coadd
1028
1029 Returns
1030 -------
1031 convergenceMetric : `float`
1032 Quality of fit metric for one exposure, within the sub-region.
1033 """
1034 convergeMask = exposure.mask.getPlaneBitMask(self.config.convergenceMaskPlanes)
1035 templateImage = dcrModels.buildMatchedTemplate(exposure=exposure,
1036 bbox=exposure.getBBox(),
1037 order=self.config.imageInterpOrder,
1038 splitSubfilters=self.config.splitSubfilters,
1039 splitThreshold=self.config.splitThreshold,
1040 amplifyModel=self.config.accelerateModel)
1041 diffVals = np.abs(exposure.image.array - templateImage.array)*significanceImage
1042 refVals = np.abs(exposure.image.array + templateImage.array)*significanceImage/2.
1043
1044 finitePixels = np.isfinite(diffVals)
1045 goodMaskPixels = (exposure.mask.array & statsCtrl.getAndMask()) == 0
1046 convergeMaskPixels = exposure.mask.array & convergeMask > 0
1047 usePixels = finitePixels & goodMaskPixels & convergeMaskPixels
1048 if np.sum(usePixels) == 0:
1049 metric = 0.
1050 else:
1051 diffUse = diffVals[usePixels]
1052 refUse = refVals[usePixels]
1053 metric = np.sum(diffUse/np.median(diffUse))/np.sum(refUse/np.median(diffUse))
1054 return metric
1055
1056 def stackCoadd(self, dcrCoadds):
1057 """Add a list of sub-band coadds together.
1058
1059 Parameters
1060 ----------
1061 dcrCoadds : `list` of `lsst.afw.image.ExposureF`
1062 A list of coadd exposures, each exposure containing
1063 the model for one subfilter.
1064
1065 Returns
1066 -------
1067 coaddExposure : `lsst.afw.image.ExposureF`
1068 A single coadd exposure that is the sum of the sub-bands.
1069 """
1070 coaddExposure = dcrCoadds[0].clone()
1071 for coadd in dcrCoadds[1:]:
1072 coaddExposure.maskedImage += coadd.maskedImage
1073 return coaddExposure
1074
1075 def fillCoadd(self, dcrModels, skyInfo, warpRefList, weightList, calibration=None, coaddInputs=None,
1076 mask=None, variance=None):
1077 """Create a list of coadd exposures from a list of masked images.
1078
1079 Parameters
1080 ----------
1081 dcrModels : `lsst.pipe.tasks.DcrModel`
1082 Best fit model of the true sky after correcting chromatic effects.
1083 skyInfo : `lsst.pipe.base.Struct`
1084 Patch geometry information, from getSkyInfo
1085 warpRefList : `list` of `lsst.daf.butler.DeferredDatasetHandle` or
1087 The data references to the input warped exposures.
1088 weightList : `list` of `float`
1089 The weight to give each input exposure in the coadd
1090 calibration : `lsst.afw.Image.PhotoCalib`, optional
1091 Scale factor to set the photometric calibration of an exposure.
1092 coaddInputs : `lsst.afw.Image.CoaddInputs`, optional
1093 A record of the observations that are included in the coadd.
1094 mask : `lsst.afw.image.Mask`, optional
1095 Optional mask to override the values in the final coadd.
1096 variance : `lsst.afw.image.Image`, optional
1097 Optional variance plane to override the values in the final coadd.
1098
1099 Returns
1100 -------
1101 dcrCoadds : `list` of `lsst.afw.image.ExposureF`
1102 A list of coadd exposures, each exposure containing
1103 the model for one subfilter.
1104 """
1105 dcrCoadds = []
1106 refModel = dcrModels.getReferenceImage()
1107 for model in dcrModels:
1108 if self.config.accelerateModel > 1:
1109 model.array = (model.array - refModel)*self.config.accelerateModel + refModel
1110 coaddExposure = afwImage.ExposureF(skyInfo.bbox, skyInfo.wcs)
1111 if calibration is not None:
1112 coaddExposure.setPhotoCalib(calibration)
1113 if coaddInputs is not None:
1114 coaddExposure.getInfo().setCoaddInputs(coaddInputs)
1115 # Set the metadata for the coadd, including PSF and aperture corrections.
1116 self.assembleMetadata(coaddExposure, warpRefList, weightList)
1117 # Overwrite the PSF
1118 coaddExposure.setPsf(dcrModels.psf)
1119 coaddUtils.setCoaddEdgeBits(dcrModels.mask[skyInfo.bbox], dcrModels.variance[skyInfo.bbox])
1120 maskedImage = afwImage.MaskedImageF(dcrModels.bbox)
1121 maskedImage.image = model
1122 maskedImage.mask = dcrModels.mask
1123 maskedImage.variance = dcrModels.variance
1124 coaddExposure.setMaskedImage(maskedImage[skyInfo.bbox])
1125 coaddExposure.setPhotoCalib(self.scaleZeroPoint.getPhotoCalib())
1126 if mask is not None:
1127 coaddExposure.setMask(mask)
1128 if variance is not None:
1129 coaddExposure.setVariance(variance)
1130 dcrCoadds.append(coaddExposure)
1131 return dcrCoadds
1132
1133 def calculateGain(self, convergenceList, gainList):
1134 """Calculate the gain to use for the current iteration.
1135
1136 After calculating a new DcrModel, each value is averaged with the
1137 value in the corresponding pixel from the previous iteration. This
1138 reduces oscillating solutions that iterative techniques are plagued by,
1139 and speeds convergence. By far the biggest changes to the model
1140 happen in the first couple iterations, so we can also use a more
1141 aggressive gain later when the model is changing slowly.
1142
1143 Parameters
1144 ----------
1145 convergenceList : `list` of `float`
1146 The quality of fit metric from each previous iteration.
1147 gainList : `list` of `float`
1148 The gains used in each previous iteration: appended with the new
1149 gain value.
1150 Gains are numbers between ``self.config.baseGain`` and 1.
1151
1152 Returns
1153 -------
1154 gain : `float`
1155 Relative weight to give the new solution when updating the model.
1156 A value of 1.0 gives equal weight to both solutions.
1157
1158 Raises
1159 ------
1160 ValueError
1161 If ``len(convergenceList) != len(gainList)+1``.
1162 """
1163 nIter = len(convergenceList)
1164 if nIter != len(gainList) + 1:
1165 raise ValueError("convergenceList (%d) must be one element longer than gainList (%d)."
1166 % (len(convergenceList), len(gainList)))
1167
1168 if self.config.baseGain is None:
1169 # If ``baseGain`` is not set, calculate it from the number of DCR subfilters
1170 # The more subfilters being modeled, the lower the gain should be.
1171 baseGain = 1./(self.config.dcrNumSubfilters - 1)
1172 else:
1173 baseGain = self.config.baseGain
1174
1175 if self.config.useProgressiveGain and nIter > 2:
1176 # To calculate the best gain to use, compare the past gains that have been used
1177 # with the resulting convergences to estimate the best gain to use.
1178 # Algorithmically, this is a Kalman filter.
1179 # If forward modeling proceeds perfectly, the convergence metric should
1180 # asymptotically approach a final value.
1181 # We can estimate that value from the measured changes in convergence
1182 # weighted by the gains used in each previous iteration.
1183 estFinalConv = [((1 + gainList[i])*convergenceList[i + 1] - convergenceList[i])/gainList[i]
1184 for i in range(nIter - 1)]
1185 # The convergence metric is strictly positive, so if the estimated final convergence is
1186 # less than zero, force it to zero.
1187 estFinalConv = np.array(estFinalConv)
1188 estFinalConv[estFinalConv < 0] = 0
1189 # Because the estimate may slowly change over time, only use the most recent measurements.
1190 estFinalConv = np.median(estFinalConv[max(nIter - 5, 0):])
1191 lastGain = gainList[-1]
1192 lastConv = convergenceList[-2]
1193 newConv = convergenceList[-1]
1194 # The predicted convergence is the value we would get if the new model calculated
1195 # in the previous iteration was perfect. Recall that the updated model that is
1196 # actually used is the gain-weighted average of the new and old model,
1197 # so the convergence would be similarly weighted.
1198 predictedConv = (estFinalConv*lastGain + lastConv)/(1. + lastGain)
1199 # If the measured and predicted convergence are very close, that indicates
1200 # that our forward model is accurate and we can use a more aggressive gain
1201 # If the measured convergence is significantly worse (or better!) than predicted,
1202 # that indicates that the model is not converging as expected and
1203 # we should use a more conservative gain.
1204 delta = (predictedConv - newConv)/((lastConv - estFinalConv)/(1 + lastGain))
1205 newGain = 1 - abs(delta)
1206 # Average the gains to prevent oscillating solutions.
1207 newGain = (newGain + lastGain)/2.
1208 gain = max(baseGain, newGain)
1209 else:
1210 gain = baseGain
1211 gainList.append(gain)
1212 return gain
1213
1214 def calculateModelWeights(self, dcrModels, dcrBBox):
1215 """Build an array that smoothly tapers to 0 away from detected sources.
1216
1217 Parameters
1218 ----------
1219 dcrModels : `lsst.pipe.tasks.DcrModel`
1220 Best fit model of the true sky after correcting chromatic effects.
1221 dcrBBox : `lsst.geom.box.Box2I`
1222 Sub-region of the coadd which includes a buffer to allow for DCR.
1223
1224 Returns
1225 -------
1226 weights : `numpy.ndarray` or `float`
1227 A 2D array of weight values that tapers smoothly to zero away from detected sources.
1228 Set to a placeholder value of 1.0 if ``self.config.useModelWeights`` is False.
1229
1230 Raises
1231 ------
1232 ValueError
1233 If ``useModelWeights`` is set and ``modelWeightsWidth`` is negative.
1234 """
1235 if not self.config.useModelWeights:
1236 return 1.0
1237 if self.config.modelWeightsWidth < 0:
1238 raise ValueError("modelWeightsWidth must not be negative if useModelWeights is set")
1239 convergeMask = dcrModels.mask.getPlaneBitMask(self.config.convergenceMaskPlanes)
1240 convergeMaskPixels = dcrModels.mask[dcrBBox].array & convergeMask > 0
1241 weights = np.zeros_like(dcrModels[0][dcrBBox].array)
1242 weights[convergeMaskPixels] = 1.
1243 weights = ndimage.gaussian_filter(weights, self.config.modelWeightsWidth)
1244 weights /= np.max(weights)
1245 return weights
1246
1247 def applyModelWeights(self, modelImages, refImage, modelWeights):
1248 """Smoothly replace model pixel values with those from a
1249 reference at locations away from detected sources.
1250
1251 Parameters
1252 ----------
1253 modelImages : `list` of `lsst.afw.image.Image`
1254 The new DCR model images from the current iteration.
1255 The values will be modified in place.
1256 refImage : `lsst.afw.image.MaskedImage`
1257 A reference image used to supply the default pixel values.
1258 modelWeights : `numpy.ndarray` or `float`
1259 A 2D array of weight values that tapers smoothly to zero away from detected sources.
1260 Set to a placeholder value of 1.0 if ``self.config.useModelWeights`` is False.
1261 """
1262 if self.config.useModelWeights:
1263 for model in modelImages:
1264 model.array *= modelWeights
1265 model.array += refImage.array*(1. - modelWeights)/self.config.dcrNumSubfilters
1266
1267 def loadSubExposures(self, bbox, statsCtrl, warpRefList, imageScalerList, spanSetMaskList):
1268 """Pre-load sub-regions of a list of exposures.
1269
1270 Parameters
1271 ----------
1272 bbox : `lsst.geom.box.Box2I`
1273 Sub-region to coadd
1275 Statistics control object for coadd
1276 warpRefList : `list` of `lsst.daf.butler.DeferredDatasetHandle` or
1278 The data references to the input warped exposures.
1279 imageScalerList : `list` of `lsst.pipe.task.ImageScaler`
1280 The image scalars correct for the zero point of the exposures.
1281 spanSetMaskList : `list` of `dict` containing spanSet lists, or None
1282 Each element is dict with keys = mask plane name to add the spans to
1283
1284 Returns
1285 -------
1286 subExposures : `dict`
1287 The `dict` keys are the visit IDs,
1288 and the values are `lsst.afw.image.ExposureF`
1289 The pre-loaded exposures for the current subregion.
1290 The variance plane contains weights, and not the variance
1291 """
1292 tempExpName = self.getTempExpDatasetName(self.warpType)
1293 zipIterables = zip(warpRefList, imageScalerList, spanSetMaskList)
1294 subExposures = {}
1295 for warpExpRef, imageScaler, altMaskSpans in zipIterables:
1296 if isinstance(warpExpRef, DeferredDatasetHandle):
1297 exposure = warpExpRef.get(parameters={'bbox': bbox})
1298 else:
1299 exposure = warpExpRef.get(tempExpName + "_sub", bbox=bbox)
1300 visit = warpExpRef.dataId["visit"]
1301 if altMaskSpans is not None:
1302 self.applyAltMaskPlanes(exposure.mask, altMaskSpans)
1303 imageScaler.scaleMaskedImage(exposure.maskedImage)
1304 # Note that the variance plane here is used to store weights, not the actual variance
1305 exposure.variance.array[:, :] = 0.
1306 # Set the weight of unmasked pixels to 1.
1307 exposure.variance.array[(exposure.mask.array & statsCtrl.getAndMask()) == 0] = 1.
1308 # Set the image value of masked pixels to zero.
1309 # This eliminates needing the mask plane when stacking images in ``newModelFromResidual``
1310 exposure.image.array[(exposure.mask.array & statsCtrl.getAndMask()) > 0] = 0.
1311 subExposures[visit] = exposure
1312 return subExposures
1313
1314 def selectCoaddPsf(self, templateCoadd, warpRefList):
1315 """Compute the PSF of the coadd from the exposures with the best seeing.
1316
1317 Parameters
1318 ----------
1319 templateCoadd : `lsst.afw.image.ExposureF`
1320 The initial coadd exposure before accounting for DCR.
1321 warpRefList : `list` of `lsst.daf.butler.DeferredDatasetHandle` or
1323 The data references to the input warped exposures.
1324
1325 Returns
1326 -------
1328 The average PSF of the input exposures with the best seeing.
1329 """
1330 sigma2fwhm = 2.*np.sqrt(2.*np.log(2.))
1331 tempExpName = self.getTempExpDatasetName(self.warpType)
1332 # Note: ``ccds`` is a `lsst.afw.table.ExposureCatalog` with one entry per ccd and per visit
1333 # If there are multiple ccds, it will have that many times more elements than ``warpExpRef``
1334 ccds = templateCoadd.getInfo().getCoaddInputs().ccds
1335 templatePsf = templateCoadd.getPsf()
1336 # Just need a rough estimate; average positions are fine
1337 templateAvgPos = templatePsf.getAveragePosition()
1338 psfRefSize = templatePsf.computeShape(templateAvgPos).getDeterminantRadius()*sigma2fwhm
1339 psfSizes = np.zeros(len(ccds))
1340 ccdVisits = np.array(ccds["visit"])
1341 for warpExpRef in warpRefList:
1342 if isinstance(warpExpRef, DeferredDatasetHandle):
1343 # Gen 3 API
1344 psf = warpExpRef.get(component="psf")
1345 else:
1346 # Gen 2 API. Delete this when Gen 2 retired
1347 psf = warpExpRef.get(tempExpName).getPsf()
1348 visit = warpExpRef.dataId["visit"]
1349 psfAvgPos = psf.getAveragePosition()
1350 psfSize = psf.computeShape(psfAvgPos).getDeterminantRadius()*sigma2fwhm
1351 psfSizes[ccdVisits == visit] = psfSize
1352 # Note that the input PSFs include DCR, which should be absent from the DcrCoadd
1353 # The selected PSFs are those that have a FWHM less than or equal to the smaller
1354 # of the mean or median FWHM of the input exposures.
1355 sizeThreshold = min(np.median(psfSizes), psfRefSize)
1356 goodPsfs = psfSizes <= sizeThreshold
1357 psf = measAlg.CoaddPsf(ccds[goodPsfs], templateCoadd.getWcs(),
1358 self.config.coaddPsf.makeControl())
1359 return psf
int min
int max
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Definition: SkyWcs.h:117
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
A class to represent a 2-dimensional array of pixels.
Definition: Image.h:51
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:77
A class to manipulate images, masks, and variance as a single object.
Definition: MaskedImage.h:73
Information about a single exposure of an imaging camera.
Definition: VisitInfo.h:68
Pass parameters to a Statistics object.
Definition: Statistics.h:92
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
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
void setCoaddEdgeBits(lsst::afw::image::Mask< lsst::afw::image::MaskPixel > &coaddMask, lsst::afw::image::Image< WeightPixelT > const &weightMap)
set edge bits of coadd mask based on weight map
Extent< int, N > ceil(Extent< double, N > const &input) noexcept
Return the component-wise ceil (round towards more positive).
Definition: Extent.cc:118
def applyDcr(image, dcr, useInverse=False, splitSubfilters=False, splitThreshold=0., doPrefilter=True, order=3)
Definition: dcrModel.py:667
def calculateDcr(visitInfo, wcs, effectiveWavelength, bandwidth, dcrNumSubfilters, splitSubfilters=False)
Definition: dcrModel.py:732
def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs)
Definition: getTemplate.py:596
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def makeSupplementaryDataGen3(self, butlerQC, inputRefs, outputRefs)
def makeSkyInfo(skyMap, tractId, patchId)
Definition: coaddBase.py:293
def loadSubExposures(self, bbox, statsCtrl, warpRefList, imageScalerList, spanSetMaskList)
def fillCoadd(self, dcrModels, skyInfo, warpRefList, weightList, calibration=None, coaddInputs=None, mask=None, variance=None)
def applyModelWeights(self, modelImages, refImage, modelWeights)
def calculateSingleConvergence(self, dcrModels, exposure, significanceImage, statsCtrl)
def calculateConvergence(self, dcrModels, subExposures, bbox, warpRefList, weightList, statsCtrl)
def dcrAssembleSubregion(self, dcrModels, subExposures, bbox, dcrBBox, warpRefList, statsCtrl, convergenceMetric, gain, modelWeights, refImage, dcrWeights)
def calculateGain(self, convergenceList, gainList)
def calculateModelWeights(self, dcrModels, dcrBBox)
def newModelFromResidual(self, dcrModels, residualGeneratorList, dcrBBox, statsCtrl, gain, modelWeights, refImage, dcrWeights)
def selectCoaddPsf(self, templateCoadd, warpRefList)
def dcrResiduals(self, residual, visitInfo, wcs, effectiveWavelength, bandwidth)
Angle abs(Angle const &a)
Definition: Angle.h:106