Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-9615: Add dcrAssembleCoadd task #173

Merged
merged 109 commits into from
Jul 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
1943a5e
Begin adding dcrAssembleCoadd task
isullivan Sep 6, 2017
156128e
Move dcrAssembleCoadd file location
isullivan Nov 27, 2017
4ecdcc5
Working skeleton of dcrAssembleCoadd
isullivan Dec 13, 2017
19eacc1
Update with changes to master version of assembleCoaddTask.
isullivan Dec 14, 2017
cd20974
Clean up inheritance and line length in dcrAssembleCoadd.
isullivan Dec 20, 2017
be9109e
Flesh out dcrAssembleCoadd methods.
isullivan Jan 10, 2018
c75f820
Fix bugs in initial dcrAssembleCoadd methods, but convergence metric …
isullivan Jan 12, 2018
402d224
Add log messages at end of dcrAssembleCoadd modeling.
isullivan Jan 17, 2018
4361bf4
Use maskedImages and correct bbox throughout DCR coadd calculation.
isullivan Jan 17, 2018
6b7e68e
Suppress mask planes warnings.
isullivan Jan 23, 2018
8284986
Add config for min and max convergence-weighted gain.
isullivan Jan 23, 2018
ae52956
Call run method of AssembleCoaddTask, then persist DcrCoadds.
isullivan Jan 23, 2018
8509419
Clean up variable names in dcrAssembleCoadd.
isullivan Jan 23, 2018
743b817
Use correct rotation angle calculation.
isullivan Jan 31, 2018
b1c67c8
Clean up DCR shift calculation.
isullivan Jan 31, 2018
778ad02
Remove unnecessary code.
isullivan Jan 31, 2018
44eee4a
Add option for a minimum number of iterations of forward modeling.
isullivan Jan 31, 2018
2c7cab9
Add option to clamp model solutions, and clean up config.
isullivan Feb 7, 2018
141bccd
Support changes from DM-12184
isullivan Feb 7, 2018
29dca62
Clean up Mask usage.
isullivan Feb 7, 2018
2fc9644
Convert to staticmethods where possible.
isullivan Feb 7, 2018
3857cf5
Better log messages for testing.
isullivan Feb 7, 2018
af8a10f
Calculate image weights from quality of fit with matched template.
isullivan Feb 13, 2018
86ea49b
Convert DCR residuals to a generator
isullivan Feb 13, 2018
6e4b08d
Switch X and Y axes for DCR shift.
isullivan Feb 13, 2018
ee22ac2
Use wavelength endpoints instead of midpoint for DCR calculation.
isullivan Feb 13, 2018
e012396
Move `prepareStats` to `AssembleCoaddTask`
isullivan Feb 14, 2018
6da3ca1
Fix flake8 warnings.
isullivan Feb 14, 2018
09e1641
Variables should use camelCase not underscores.
isullivan Feb 21, 2018
08b8838
Update filter effective wavelength and width.
isullivan Feb 27, 2018
45922db
Add treatment of masks.
isullivan Feb 27, 2018
e8800fb
Add option to weight by airmass.
isullivan Feb 27, 2018
09b81eb
Add frequency regularization.
isullivan Feb 27, 2018
77a94ba
Clean up bounding boxes, and variable names.
isullivan Feb 27, 2018
0656f85
Add docstrings in the style of assembleCoadd.py
isullivan Feb 28, 2018
861bdc5
Update with new skyWcs.
isullivan Mar 1, 2018
e273ba3
Begin adding DCR unit tests.
isullivan Mar 3, 2018
39fbb0b
Add DCR shift unit tests.
isullivan Mar 3, 2018
97a8113
unit test fixup
isullivan Mar 4, 2018
102357d
Fix syntax bug that was hidden by local changes
isullivan Mar 4, 2018
3b2b68e
Clean up frequency regularization code and add unit tests.
isullivan Mar 8, 2018
cf35acd
Expand DCR model conditioning test.
isullivan Mar 8, 2018
f37ede7
Clean up and expand function docstrings.
isullivan Mar 8, 2018
fed7310
Support DM-13504 API changes for DCR coadds.
isullivan Mar 20, 2018
03a323c
Fix copyright.
isullivan Mar 20, 2018
e895f9d
Clean up imports and config.
isullivan Mar 20, 2018
371287e
Remove pixelScale attribute.
isullivan Mar 21, 2018
1c9012e
Convert prepareStats to return a struct, not a tuple.
isullivan Mar 21, 2018
97952ab
Convert dcr namedtuple to Extent2D.
isullivan Mar 21, 2018
e39c061
Make _subBBoxIter a static method of AssembleCoaddTask
isullivan Mar 21, 2018
9bbfd8d
Move final coadd interpolation and bright object masking to new method.
isullivan Mar 22, 2018
f15eee1
Convert Coord to new SpherePoint.
isullivan Mar 26, 2018
6b4b972
Convert to NumpyDoc.
isullivan Mar 30, 2018
2cdf5b7
Convert all .getArray() instances to .array
isullivan Mar 31, 2018
39412c5
Improve DCR unit tests.
isullivan Apr 2, 2018
18dfd24
Ensure DCR test checks support for arbitrary XY0 offsets.
isullivan Apr 2, 2018
d7bc339
Generalize DCR mask shift test for both large and small shifts
isullivan Apr 2, 2018
259d1e0
Enable convergence test.
isullivan Apr 4, 2018
c94f2bd
Undo unnecessary staticmethods.
isullivan Apr 4, 2018
ce2ebb9
Move single convergence calculation to its own function.
isullivan Apr 4, 2018
2f8982a
Clean up dcrAssembleCoadd from review
isullivan Apr 6, 2018
916e1cb
Use new lambdaMin and lambdaMax filter properties in dcrAssembleCoadd.
isullivan Apr 6, 2018
d0343e3
Only calculate new variance on the first iteration of dcrAssembleCoadd
isullivan Apr 6, 2018
82176c8
Standardize mask plane usage.
isullivan Apr 20, 2018
9d764a5
Remove extra weights calculation in DcrAssembleCoadd.
isullivan Apr 20, 2018
5f0ac03
Clarify warning and info messages.
isullivan Apr 20, 2018
f24579f
Convergence reference should be template, not exposure.
isullivan Apr 20, 2018
bb7289b
Compute `bufferSize` programmatically.
isullivan Apr 23, 2018
d483519
Include the total number of subfilters when persisting dcrCoadds.
isullivan Apr 27, 2018
c6af894
Add calculation of and return dcrNImages
isullivan Apr 27, 2018
847ca78
Use stack package not scipy for making model images.
isullivan Apr 27, 2018
062919f
Update copyright.
isullivan May 4, 2018
1517d2b
Adjust regularization to handle negative and noise-like pixels.
isullivan May 12, 2018
7a0092d
Use stack image warping instead of scipy.
isullivan May 12, 2018
33daa41
Fix DCR NImage calculations.
isullivan May 12, 2018
3692655
Move duplicated code to remove mask planes to a new method.
isullivan May 15, 2018
29e8a4b
Reuse code for image shifting to shift mask planes.
isullivan May 15, 2018
5799ca7
Simplify DCR tests with method to make test images.
isullivan May 15, 2018
1680bc7
Restrict docstrings to 80 characters
isullivan May 16, 2018
974a2ba
Add initial algorithm overview text.
isullivan May 16, 2018
99bde9b
Remove code to specify `tempExpRefList` in a Jupyter notebook.
isullivan Jun 1, 2018
12fbb6f
Convert deprecated lsst.afw.geom.Angle to lsst.geom data types.
isullivan Jun 6, 2018
7a7cb9a
Fix Numpydoc style errors.
isullivan Jun 8, 2018
06a96f5
Remove python 2 compatibility.
isullivan Jun 8, 2018
0701e73
Rename dcrShiftCalculate to calculateDcr
isullivan Jun 8, 2018
4e79341
Use OR not XOR for masks
isullivan Jun 8, 2018
bf64b27
Use exposure's mask plane definitions, not global.
isullivan Jun 8, 2018
43a3e10
Clean up DCR code from review comments.
isullivan Jun 8, 2018
98b250f
fixup cleanup
isullivan Jun 19, 2018
a658753
Compose Task in test instead of inherit.
isullivan Jun 8, 2018
68054d3
Add unit test of regularization between iterations.
isullivan Jun 14, 2018
8d17ad8
No need to use sub-images when calculating dcrNImages
isullivan Jun 18, 2018
e400934
Minimum working version with the new DcrModel class
isullivan Jun 22, 2018
8136581
Fix unit tests to work with new DcrModel class
isullivan Jun 22, 2018
f5d2c8a
Rename DCR unit test
isullivan Jun 22, 2018
90dc764
Move DcrModel class to its own module.
isullivan Jun 22, 2018
df7b651
Remove access to DcrModel internal parameters
isullivan Jun 22, 2018
4c70764
Remove whitespace
isullivan Jun 22, 2018
1c64350
Clean up the docs.
isullivan Jun 23, 2018
962d7bf
Make DcrModel iterable.
isullivan Jun 25, 2018
76a9dde
Clean up DCR docstrings.
isullivan Jul 11, 2018
d3a0f51
Remove DcrModel.getImage() in favor of __getitem__
isullivan Jul 11, 2018
c32d7ab
afwImage.PARENT is now the default for bounding boxes.
isullivan Jul 11, 2018
08e67b7
Add DcrModel helper methods
isullivan Jul 11, 2018
1728bd8
New DcrModel construction interface
isullivan Jul 11, 2018
2584eaa
Add method to load an existing DcrModel from a repository
isullivan Jul 18, 2018
53bf95b
Rename `calculateRotationAngle`
isullivan Jul 19, 2018
677be0f
Fix variance in `conditionDcrModel`
isullivan Jul 19, 2018
198144e
Clean up DcrModel documentation
isullivan Jul 19, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions bin.src/dcrAssembleCoadd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python

#
# LSST Data Management System
# Copyright 2017-2018 University of Washington.
#
# This product includes software developed by the
# LSST Project (http://www.lsst.org/).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the LSST License Statement and
# the GNU General Public License along with this program. If not,
# see <http://www.lsstcorp.org/LegalNotices/>.
#
from lsst.pipe.tasks.dcrAssembleCoadd import DcrAssembleCoaddTask

DcrAssembleCoaddTask.parseAndRun()
218 changes: 139 additions & 79 deletions python/lsst/pipe/tasks/assembleCoadd.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,17 +360,7 @@ def run(self, dataRef, selectDataList=[]):
retStruct = self.assemble(skyInfo, inputData.tempExpRefList, inputData.imageScalerList,
inputData.weightList, supplementaryData=supplementaryData)

if self.config.doInterp:
self.interpImage.run(retStruct.coaddExposure.getMaskedImage(), planeName="NO_DATA")
# The variance must be positive; work around for DM-3201.
varArray = retStruct.coaddExposure.getMaskedImage().getVariance().getArray()
with numpy.errstate(invalid="ignore"):
varArray[:] = numpy.where(varArray > 0, varArray, numpy.inf)

if self.config.doMaskBrightObjects:
brightObjectMasks = self.readBrightObjectMasks(dataRef)
self.setBrightObjectMasks(retStruct.coaddExposure, dataRef.dataId, brightObjectMasks)

self.processResults(retStruct.coaddExposure, dataRef)
if self.config.doWrite:
self.log.info("Persisting %s" % self.getCoaddDatasetName(self.warpType))
dataRef.put(retStruct.coaddExposure, self.getCoaddDatasetName(self.warpType))
Expand All @@ -379,6 +369,27 @@ def run(self, dataRef, selectDataList=[]):

return retStruct

def processResults(self, coaddExposure, dataRef):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New method needs docstring.

"""Interpolate over missing data and mask bright stars.

Parameters
----------
coaddExposure : `lsst.afw.image.Exposure`
The coadded exposure to process.
dataRef : `lsst.daf.persistence.ButlerDataRef`
Butler data reference for supplementary data.
"""
if self.config.doInterp:
self.interpImage.run(coaddExposure.getMaskedImage(), planeName="NO_DATA")
# The variance must be positive; work around for DM-3201.
varArray = coaddExposure.variance.array
with numpy.errstate(invalid="ignore"):
varArray[:] = numpy.where(varArray > 0, varArray, numpy.inf)

if self.config.doMaskBrightObjects:
brightObjectMasks = self.readBrightObjectMasks(dataRef)
self.setBrightObjectMasks(coaddExposure, dataRef.dataId, brightObjectMasks)

def makeSupplementaryData(self, dataRef, selectDataList):
"""Make additional inputs to assemble() specific to subclasses.

Expand All @@ -388,8 +399,8 @@ def makeSupplementaryData(self, dataRef, selectDataList):

Parameters
----------
dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
Butler dataRef for supplementary data.
dataRef : `lsst.daf.persistence.ButlerDataRef`
Butler data reference for supplementary data.
selectDataList : `list`
List of data references to Warps.
"""
Expand Down Expand Up @@ -488,9 +499,41 @@ def prepareInputs(self, refList):
return pipeBase.Struct(tempExpRefList=tempExpRefList, weightList=weightList,
imageScalerList=imageScalerList)

def prepareStats(self, mask=None):
"""Prepare the statistics for coadding images.

Parameters
----------
mask : `int`, optional
Bit mask value to exclude from coaddition.

Returns
-------
stats : `lsst.pipe.base.Struct`
Statistics structure with the following fields:

- ``statsCtrl``: Statistics control object for coadd
(`lsst.afw.math.StatisticsControl`)
- ``statsFlags``: Statistic for coadd (`lsst.afw.math.Property`)
"""
if mask is None:
mask = self.getBadPixelMask()
statsCtrl = afwMath.StatisticsControl()
statsCtrl.setNumSigmaClip(self.config.sigmaClip)
statsCtrl.setNumIter(self.config.clipIter)
statsCtrl.setAndMask(mask)
statsCtrl.setNanSafe(True)
statsCtrl.setWeighted(True)
statsCtrl.setCalcErrorFromInputVariance(self.config.calcErrorFromInputVariance)
for plane, threshold in self.config.maskPropagationThresholds.items():
bit = afwImage.Mask.getMaskPlane(plane)
statsCtrl.setMaskPropagationThreshold(bit, threshold)
statsFlags = afwMath.stringToStatisticsProperty(self.config.statistic)
return pipeBase.Struct(ctrl=statsCtrl, flags=statsFlags)

def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList,
altMaskList=None, mask=None, supplementaryData=None):
"""Assemble a coadd from input warps
"""Assemble a coadd from input warps.

Assemble the coadd using the provided list of coaddTempExps. Since
the full coadd covers a patch (a large area), the assembly is
Expand Down Expand Up @@ -530,21 +573,7 @@ def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList,
"""
tempExpName = self.getTempExpDatasetName(self.warpType)
self.log.info("Assembling %s %s", len(tempExpRefList), tempExpName)
if mask is None:
mask = self.getBadPixelMask()

statsCtrl = afwMath.StatisticsControl()
statsCtrl.setNumSigmaClip(self.config.sigmaClip)
statsCtrl.setNumIter(self.config.clipIter)
statsCtrl.setAndMask(mask)
statsCtrl.setNanSafe(True)
statsCtrl.setWeighted(True)
statsCtrl.setCalcErrorFromInputVariance(self.config.calcErrorFromInputVariance)
for plane, threshold in self.config.maskPropagationThresholds.items():
bit = afwImage.Mask.getMaskPlane(plane)
statsCtrl.setMaskPropagationThreshold(bit, threshold)

statsFlags = afwMath.stringToStatisticsProperty(self.config.statistic)
stats = self.prepareStats(mask=mask)

if altMaskList is None:
altMaskList = [None]*len(tempExpRefList)
Expand All @@ -561,10 +590,10 @@ def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList,
nImage = afwImage.ImageU(skyInfo.bbox)
else:
nImage = None
for subBBox in _subBBoxIter(skyInfo.bbox, subregionSize):
for subBBox in self._subBBoxIter(skyInfo.bbox, subregionSize):
try:
self.assembleSubregion(coaddExposure, subBBox, tempExpRefList, imageScalerList,
weightList, altMaskList, statsFlags, statsCtrl,
weightList, altMaskList, stats.flags, stats.ctrl,
nImage=nImage)
except Exception as e:
self.log.fatal("Cannot compute coadd %s: %s", subBBox, e)
Expand Down Expand Up @@ -671,17 +700,8 @@ def assembleSubregion(self, coaddExposure, bbox, tempExpRefList, imageScalerList
coaddExposure.mask.addMaskPlane("REJECTED")
coaddExposure.mask.addMaskPlane("CLIPPED")
coaddExposure.mask.addMaskPlane("SENSOR_EDGE")
# If a pixel is rejected due to a mask value other than EDGE, NO_DATA,
# or CLIPPED, set it to REJECTED on the coadd.
# If a pixel is rejected due to EDGE, set the coadd pixel to SENSOR_EDGE.
# if a pixel is rejected due to CLIPPED, set the coadd pixel to CLIPPED.
edge = afwImage.Mask.getPlaneBitMask("EDGE")
noData = afwImage.Mask.getPlaneBitMask("NO_DATA")
maskMap = self.setRejectedMaskMapping(statsCtrl)
clipped = afwImage.Mask.getPlaneBitMask("CLIPPED")
toReject = statsCtrl.getAndMask() & (~noData) & (~edge) & (~clipped)
maskMap = [(toReject, coaddExposure.mask.getPlaneBitMask("REJECTED")),
(edge, coaddExposure.mask.getPlaneBitMask("SENSOR_EDGE")),
(clipped, clipped)]
maskedImageList = []
if nImage is not None:
subNImage = afwImage.ImageU(bbox.getWidth(), bbox.getHeight())
Expand All @@ -698,13 +718,7 @@ def assembleSubregion(self, coaddExposure, bbox, tempExpRefList, imageScalerList
if nImage is not None:
subNImage.getArray()[maskedImage.getMask().getArray() & statsCtrl.getAndMask() == 0] += 1
if self.config.removeMaskPlanes:
mask = maskedImage.getMask()
for maskPlane in self.config.removeMaskPlanes:
try:
mask &= ~mask.getPlaneBitMask(maskPlane)
except Exception as e:
self.log.warn("Unable to remove mask plane %s: %s", maskPlane, e.args[0])

self.removeMaskPlanes(maskedImage)
maskedImageList.append(maskedImage)

with self.timer("stack"):
Expand All @@ -715,6 +729,50 @@ def assembleSubregion(self, coaddExposure, bbox, tempExpRefList, imageScalerList
if nImage is not None:
nImage.assign(subNImage, bbox)

def removeMaskPlanes(self, maskedImage):
"""Unset the mask of an image for mask planes specified in the config.

Parameters
----------
maskedImage : `lsst.afw.image.MaskedImage`
The masked image to be modified.
"""
mask = maskedImage.getMask()
for maskPlane in self.config.removeMaskPlanes:
try:
mask &= ~mask.getPlaneBitMask(maskPlane)
except Exception as e:
self.log.warn("Unable to remove mask plane %s: %s", maskPlane, e.args[0])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why e.args[0]? Surely plain old e should stringify?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plain e does work, but I think the extra verbosity is not helpful:

dcrAssembleCoadd.assembleStaticSkyModel WARN: Unable to remove mask plane NOT_DEBLENDED: 
  File "src/image/Mask.cc", line 685, in static int lsst::afw::image::Mask<int>::getMaskPlane(const std::string &) [MaskPixelT = int]
    Invalid mask plane name: NOT_DEBLENDED {0}
lsst::pex::exceptions::InvalidParameterError: 'Invalid mask plane name: NOT_DEBLENDED'

vs. with e.args[0]:

dcrAssembleCoadd.assembleStaticSkyModel WARN: Unable to remove mask plane NOT_DEBLENDED: Invalid mask plane name: NOT_DEBLENDED

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think plain e gives you what you want for an exception coming from C++.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't actually change these lines from what's on master, so I'd prefer to leave it as it is.


@staticmethod
def setRejectedMaskMapping(statsCtrl):
"""Map certain mask planes of the warps to new planes for the coadd.

If a pixel is rejected due to a mask value other than EDGE, NO_DATA,
or CLIPPED, set it to REJECTED on the coadd.
If a pixel is rejected due to EDGE, set the coadd pixel to SENSOR_EDGE.
If a pixel is rejected due to CLIPPED, set the coadd pixel to CLIPPED.

Parameters
----------
statsCtrl : `lsst.afw.math.StatisticsControl`
Statistics control object for coadd

Returns
-------
maskMap : `list` of `tuple` of `int`
A list of mappings of mask planes of the warped exposures to
mask planes of the coadd.
"""
edge = afwImage.Mask.getPlaneBitMask("EDGE")
noData = afwImage.Mask.getPlaneBitMask("NO_DATA")
clipped = afwImage.Mask.getPlaneBitMask("CLIPPED")
toReject = statsCtrl.getAndMask() & (~noData) & (~edge) & (~clipped)
maskMap = [(toReject, afwImage.Mask.getPlaneBitMask("REJECTED")),
(edge, afwImage.Mask.getPlaneBitMask("SENSOR_EDGE")),
(clipped, clipped)]
return maskMap

def applyAltMaskPlanes(self, mask, altMaskSpans):
"""Apply in place alt mask formatted as SpanSets to a mask.

Expand Down Expand Up @@ -877,37 +935,38 @@ def _makeArgumentParser(cls):
ContainerClass=SelectDataIdContainer)
return parser

@staticmethod
def _subBBoxIter(bbox, subregionSize):
"""Iterate over subregions of a bbox.

def _subBBoxIter(bbox, subregionSize):
"""Iterate over subregions of a bbox.

Parameters
----------
bbox : `lsst.afw.geom.Box2I`
Bounding box over which to iterate.
subregionSize: `lsst.afw.geom.Extent2I`
Size of sub-bboxes.

Yields
------
subBBox : `lsst.afw.geom.Box2I`
Next sub-bounding box of size subregionSize or smaller; each subBBox
is contained within bbox, so it may be smaller than subregionSize at
the edges of bbox, but it will never be empty.
"""
if bbox.isEmpty():
raise RuntimeError("bbox %s is empty" % (bbox,))
if subregionSize[0] < 1 or subregionSize[1] < 1:
raise RuntimeError("subregionSize %s must be nonzero" % (subregionSize,))

for rowShift in range(0, bbox.getHeight(), subregionSize[1]):
for colShift in range(0, bbox.getWidth(), subregionSize[0]):
subBBox = afwGeom.Box2I(bbox.getMin() + afwGeom.Extent2I(colShift, rowShift), subregionSize)
subBBox.clip(bbox)
if subBBox.isEmpty():
raise RuntimeError("Bug: empty bbox! bbox=%s, subregionSize=%s, colShift=%s, rowShift=%s" %
(bbox, subregionSize, colShift, rowShift))
yield subBBox
Parameters
----------
bbox : `lsst.afw.geom.Box2I`
Bounding box over which to iterate.
subregionSize: `lsst.afw.geom.Extent2I`
Size of sub-bboxes.

Yields
------
subBBox : `lsst.afw.geom.Box2I`
Next sub-bounding box of size ``subregionSize`` or smaller; each ``subBBox``
is contained within ``bbox``, so it may be smaller than ``subregionSize`` at
the edges of ``bbox``, but it will never be empty.
"""
if bbox.isEmpty():
raise RuntimeError("bbox %s is empty" % (bbox,))
if subregionSize[0] < 1 or subregionSize[1] < 1:
raise RuntimeError("subregionSize %s must be nonzero" % (subregionSize,))

for rowShift in range(0, bbox.getHeight(), subregionSize[1]):
for colShift in range(0, bbox.getWidth(), subregionSize[0]):
subBBox = afwGeom.Box2I(bbox.getMin() + afwGeom.Extent2I(colShift, rowShift), subregionSize)
subBBox.clip(bbox)
if subBBox.isEmpty():
raise RuntimeError("Bug: empty bbox! bbox=%s, subregionSize=%s, "
"colShift=%s, rowShift=%s" %
(bbox, subregionSize, colShift, rowShift))
yield subBBox


class AssembleCoaddDataIdContainer(pipeBase.DataIdContainer):
Expand Down Expand Up @@ -1753,7 +1812,8 @@ def makeSupplementaryData(self, dataRef, selectDataList):
""" % {"warpName": warpName}
raise RuntimeError(message)

return pipeBase.Struct(templateCoadd=templateCoadd.coaddExposure)
return pipeBase.Struct(templateCoadd=templateCoadd.coaddExposure,
nImage=templateCoadd.nImage)

def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList,
supplementaryData, *args, **kwargs):
Expand Down