Skip to content

Commit 30ebfa0

Browse files
leeskelvinaemerywatkins
authored andcommitted
Refactor by LSK, round 2
1 parent 219481c commit 30ebfa0

File tree

1 file changed

+98
-109
lines changed

1 file changed

+98
-109
lines changed

python/lsst/pipe/tasks/matchBackgrounds.py

Lines changed: 98 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import lsstDebug
2525
import numpy as np
26-
from lsst.afw.image import LOCAL, PARENT, ImageF, Mask, MaskedImageF
26+
from lsst.afw.image import LOCAL, PARENT, ExposureF, ImageF, Mask, MaskedImageF
2727
from lsst.afw.math import (
2828
MEAN,
2929
MEANCLIP,
@@ -35,6 +35,7 @@
3535
ApproximateControl,
3636
BackgroundControl,
3737
BackgroundList,
38+
BackgroundMI,
3839
StatisticsControl,
3940
makeBackground,
4041
makeStatistics,
@@ -84,11 +85,11 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
8485
doc="Visit ID of the reference warp. If None, the best warp is chosen from the list of warps.",
8586
optional=True,
8687
)
87-
bestRefWeightCoverage = RangeField(
88+
bestRefWeightChi2 = RangeField(
8889
dtype=float,
89-
doc="Coverage weight (Number of pixels overlapping the patch) when calculating the best reference "
90-
"exposure. Higher weights prefer exposures with high coverage. Ignored when a ref visit supplied.",
91-
default=0.4,
90+
doc="Mean background goodness of fit statistic weight when calculating the best reference exposure. "
91+
"Higher weights prefer exposures with flatter backgrounds. Ignored when ref visit supplied.",
92+
default=0.2,
9293
min=0.0,
9394
max=1.0,
9495
)
@@ -100,10 +101,18 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
100101
min=0.0,
101102
max=1.0,
102103
)
103-
bestRefWeightLevel = RangeField(
104+
bestRefWeightGlobalCoverage = RangeField(
104105
dtype=float,
105-
doc="Mean background level weight when calculating the best reference exposure. "
106-
"Higher weights prefer exposures with low mean background levels. Ignored when ref visit supplied.",
106+
doc="Global coverage weight (total number of valid pixels) when calculating the best reference "
107+
"exposure. Higher weights prefer exposures with high coverage. Ignored when a ref visit supplied.",
108+
default=0.2,
109+
min=0.0,
110+
max=1.0,
111+
)
112+
bestRefWeightEdgeCoverage = RangeField(
113+
dtype=float,
114+
doc="Edge coverage weight (number of valid edge pixels) when calculating the best reference "
115+
"exposure. Higher weights prefer exposures with high coverage. Ignored when a ref visit supplied.",
107116
default=0.2,
108117
min=0.0,
109118
max=1.0,
@@ -143,7 +152,7 @@ class MatchBackgroundsConfig(PipelineTaskConfig, pipelineConnections=MatchBackgr
143152
)
144153
binSize = Field[int](
145154
doc="Bin size for gridding the difference image and fitting a spatial model.",
146-
default=256,
155+
default=1024,
147156
)
148157
interpStyle = ChoiceField(
149158
dtype=str,
@@ -213,11 +222,16 @@ class MatchBackgroundsTask(PipelineTask):
213222

214223
def __init__(self, *args, **kwargs):
215224
super().__init__(**kwargs)
225+
self.statsFlag = stringToStatisticsProperty(self.config.gridStatistic)
216226
self.statsCtrl = StatisticsControl()
217227
# TODO: Check that setting the mask planes here work - these planes
218228
# can vary from exposure to exposure, I think?
219229
self.statsCtrl.setAndMask(Mask.getPlaneBitMask(self.config.badMaskPlanes))
220230
self.statsCtrl.setNanSafe(True)
231+
self.statsCtrl.setNumSigmaClip(self.config.numSigmaClip)
232+
self.statsCtrl.setNumIter(self.config.numIter)
233+
self.stringToInterpStyle = stringToInterpStyle(self.config.interpStyle)
234+
self.undersampleStyle = stringToUndersampleStyle(self.config.undersampleStyle)
221235

222236
def runQuantum(self, butlerQC, inputRefs, outputRefs):
223237
inputs = butlerQC.get(inputRefs)
@@ -333,154 +347,129 @@ def _defineWarps(self, warps, refWarpVisit=None):
333347
minimizing a cost function that penalizes high variance, high
334348
background level, and low coverage.
335349
336-
To find a reference warp, the following config parameters are used:
337-
- ``bestRefWeightCoverage``
338-
- ``bestRefWeightVariance``
339-
- ``bestRefWeightLevel``
340-
341350
Parameters
342351
----------
343-
warps : `list`[`~lsst.afw.image.Exposure`]
344-
List of warped science exposures.
352+
warps : `list`[`~lsst.daf.butler.DeferredDatasetHandle`]
353+
List of warped exposures (of type `~lsst.afw.image.ExposureF`).
354+
refWarpVisit : `int`, optional
355+
Visit ID of the reference warp.
356+
If None, the best warp is chosen from the list of warps.
345357
346358
Returns
347359
-------
348-
refWarp : `~lsst.afw.image.Exposure`
360+
refWarp : `~lsst.afw.image.ExposureF`
349361
Reference warped exposure.
350-
compWarps : `list`[`~lsst.afw.image.Exposure`]
351-
List of warped science exposures to compare to the reference.
362+
refWarpIndex : `int`
363+
Index of the reference removed from the list of warps.
364+
365+
Notes
366+
-----
367+
This method modifies the input list of warps in place by removing the
368+
reference warp from it.
352369
"""
353-
# User a reference visit, if one has been supplied
370+
# User-defined reference visit, if one has been supplied
354371
if refWarpVisit:
355-
warpVisits = [warp.dataId["visit"] for warp in warps]
372+
warpVisits = [warpDDH.dataId["visit"] for warpDDH in warps]
356373
try:
357-
refWarp = warps.pop(warpVisits.index(refWarpVisit))
374+
refWarpIndex = warpVisits.index(refWarpVisit)
375+
refWarpDDH = warps.pop(refWarpIndex)
358376
self.log.info("Using user-supplied reference visit %d", refWarpVisit)
359-
return refWarp
377+
return refWarpDDH.get(), refWarpIndex
360378
except ValueError:
361379
raise TaskError(f"Reference visit {refWarpVisit} is not found in the list of warps.")
362380

363381
# Extract mean/var/npoints for each warp
364-
warpMeans = []
365-
warpVars = []
366-
warpNPoints = []
382+
warpChi2s = [] # Background goodness of fit
383+
warpVars = [] # Variance
384+
warpNPointsGlobal = [] # Global coverage
385+
warpNPointsEdge = [] # Edge coverage
367386
for warpDDH in warps:
368387
warp = warpDDH.get()
369-
370-
# First check if any image edge is all NaN
371-
# If so, don't use
372-
leftBool = np.all(np.isnan(warp.image.array[:, 0]))
373-
rightBool = np.all(np.isnan(warp.image.array[:, warp.image.getHeight() - 1]))
374-
bottomBool = np.all(np.isnan(warp.image.array[0, :]))
375-
topBool = np.all(np.isnan(warp.image.array[warp.image.getWidth() - 1, :]))
376-
if np.any([leftBool, rightBool, bottomBool, topBool]):
377-
continue
378-
379-
warp.image.array *= warp.getPhotoCalib().instFluxToNanojansky(1) # Convert image plane to nJy
380-
381-
# Select reference based on BG of plane sky-subtracted images
382-
bkgd, __, __, __ = self._setupBackground(warp)
383-
384-
weightByInverseVariance = self.config.approxWeighting
385-
actrl = ApproximateControl(ApproximateControl.CHEBYSHEV, 1, 1, weightByInverseVariance)
386-
undersampleStyle = stringToUndersampleStyle(self.config.undersampleStyle)
387-
approx = bkgd.getApproximate(actrl, undersampleStyle)
388-
bgSubImage = ImageF(warp.image.array - approx.getImage().array)
389-
390-
warpStats = makeStatistics(bgSubImage, warp.mask, MEAN | VARIANCE | NPOINT, self.statsCtrl)
391-
warpMean, _ = warpStats.getResult(MEAN)
388+
instFluxToNanojansky = warp.getPhotoCalib().instFluxToNanojansky(1)
389+
warp.image *= instFluxToNanojansky # Images in nJy to facilitate difference imaging
390+
warp.variance *= instFluxToNanojansky
391+
warpBg, _ = self._makeBackground(warp)
392+
393+
# Return an approximation to the background
394+
approxCtrl = ApproximateControl(ApproximateControl.CHEBYSHEV, 1, 1, self.config.approxWeighting)
395+
warpApprox = warpBg.getApproximate(approxCtrl, self.undersampleStyle)
396+
warpBgSub = ImageF(warp.image.array - warpApprox.getImage().array)
397+
398+
warpStats = makeStatistics(warpBgSub, warp.mask, VARIANCE | NPOINT, self.statsCtrl)
399+
# TODO: need to modify this to account for the background mask
400+
warpChi2 = np.nansum(warpBgSub.array**2 / warp.variance.array)
392401
warpVar, _ = warpStats.getResult(VARIANCE)
393-
warpNPoint, _ = warpStats.getResult(NPOINT)
394-
warpMeans.append(warpMean)
402+
warpNPointGlobal, _ = warpStats.getResult(NPOINT)
403+
warpNPointEdge = (
404+
np.sum(~np.isnan(warp.image.array[:, 0])) # Left edge
405+
+ np.sum(~np.isnan(warp.image.array[:, -1])) # Right edge
406+
+ np.sum(~np.isnan(warp.image.array[0, :])) # Bottom edge
407+
+ np.sum(~np.isnan(warp.image.array[-1, :])) # Top edge
408+
)
409+
warpChi2s.append(warpChi2)
395410
warpVars.append(warpVar)
396-
warpNPoints.append(int(warpNPoint))
397-
398-
if len(warpNPoints) == 0:
399-
raise TaskError("No suitable reference visit found in list of warps.")
411+
warpNPointsGlobal.append(int(warpNPointGlobal))
412+
warpNPointsEdge.append(warpNPointEdge)
400413

401414
# Normalize mean/var/npoints to range from 0 to 1
402-
warpMeansFrac = np.array(warpMeans) / np.nanmax(warpMeans)
415+
warpChi2sFrac = np.array(warpChi2s) / np.nanmax(warpChi2s)
403416
warpVarsFrac = np.array(warpVars) / np.nanmax(warpVars)
404-
warpNPointsFrac = np.nanmin(warpNPoints) / np.array(warpNPoints)
417+
warpNPointsGlobalFrac = np.nanmin(warpNPointsGlobal) / np.array(warpNPointsGlobal)
418+
warpNPointsEdgeFrac = np.nanmin(warpNPointsEdge) / np.array(warpNPointsEdge)
405419

406420
# Calculate cost function values
407-
costFunctionVals = self.config.bestRefWeightLevel * warpMeansFrac
421+
costFunctionVals = self.config.bestRefWeightChi2 * warpChi2sFrac
408422
costFunctionVals += self.config.bestRefWeightVariance * warpVarsFrac
409-
costFunctionVals += self.config.bestRefWeightCoverage * warpNPointsFrac
423+
costFunctionVals += self.config.bestRefWeightGlobalCoverage * warpNPointsGlobalFrac
424+
costFunctionVals += self.config.bestRefWeightEdgeCoverage * warpNPointsEdgeFrac
410425

411426
ind = np.nanargmin(costFunctionVals)
412427
refWarp = warps.pop(ind)
413428
self.log.info("Using best reference visit %d", refWarp.dataId["visit"])
414429
return refWarp, ind
415430

416-
def _fluxScale(self, exposure):
417-
"""Scales image to nJy flux using photometric calibration.
431+
def _makeBackground(self, warp: ExposureF) -> tuple[BackgroundMI, BackgroundControl]:
432+
"""Generate a simple binned background masked image for warped data.
418433
419434
Parameters
420435
----------
421-
exposure: `lsst.afw.image._exposure.ExposureF`
422-
Exposure to scale.
436+
warp: `~lsst.afw.image.ExposureF`
437+
Warped exposure for which to estimate background.
423438
424439
Returns
425440
-------
426-
maskedImage: `lsst.afw.image._maskedImage.MaskedImageF`
427-
Flux-scaled masked exposure.
441+
warpBgMI: `~lsst.afw.math.BackgroundMI`
442+
Background-subtracted masked image.
443+
bgCtrl: `~lsst.afw.math.BackgroundControl`
444+
Background control object.
428445
"""
429-
maskedImage = exposure.getMaskedImage()
430-
fluxZp = exposure.getPhotoCalib().instFluxToNanojansky(1)
431-
exposure.image.array *= fluxZp
446+
nx = warp.getWidth() // self.config.binSize
447+
ny = warp.getHeight() // self.config.binSize
432448

433-
return maskedImage
449+
bgCtrl = BackgroundControl(nx, ny, self.statsCtrl, self.statsFlag)
450+
bgCtrl.setUndersampleStyle(self.config.undersampleStyle)
451+
warpBgMI = makeBackground(warp.getMaskedImage(), bgCtrl)
434452

435-
def _setupBackground(self, warp):
436-
"""Set up and return a background model container and associated images
437-
and controllers.
453+
return warpBgMI, bgCtrl
438454

439-
Uses the following config parameters:
440-
- ``gridStatistic``
441-
- ``numSigmaClip``
442-
- ``numIter``
443-
- ``binSize``
444-
- ``undersampleStyle``
455+
def _fluxScale(self, exposure):
456+
"""Scales image to nJy flux using photometric calibration.
445457
446458
Parameters
447459
----------
448-
warp: `lsst.afw.image._exposure.ExposureF`
449-
Warped exposure or difference image for which to estimate
450-
background.
460+
exposure: `lsst.afw.image._exposure.ExposureF`
461+
Exposure to scale.
451462
452463
Returns
453464
-------
454-
bkgd: `lsst.afw.math.BackgroundMI`
455-
Background model container.
456-
bctrl: `lsst.afw.math.BackgroundControl`
457-
Background model control
458-
warpMI: `lsst.afw.image._maskedImage.MaskedImageF`
459-
Masked image associated with warp
460-
statsFlag: `lsst.afw.math.Property`
461-
Flag for grid statistic property (self.config.gridStatistic)
465+
maskedImage: `lsst.afw.image._maskedImage.MaskedImageF`
466+
Flux-scaled masked exposure.
462467
"""
463-
statsFlag = stringToStatisticsProperty(self.config.gridStatistic)
464-
self.statsCtrl.setNumSigmaClip(self.config.numSigmaClip)
465-
self.statsCtrl.setNumIter(self.config.numIter)
466-
467-
warpMI = warp.getMaskedImage()
468-
469-
width = warpMI.getWidth()
470-
height = warpMI.getHeight()
471-
nx = width // self.config.binSize
472-
if width % self.config.binSize != 0:
473-
nx += 1
474-
ny = height // self.config.binSize
475-
if height % self.config.binSize != 0:
476-
ny += 1
477-
478-
bctrl = BackgroundControl(nx, ny, self.statsCtrl, statsFlag)
479-
bctrl.setUndersampleStyle(self.config.undersampleStyle)
480-
481-
bkgd = makeBackground(warpMI, bctrl)
468+
maskedImage = exposure.getMaskedImage()
469+
fluxZp = exposure.getPhotoCalib().instFluxToNanojansky(1)
470+
exposure.image.array *= fluxZp
482471

483-
return bkgd, bctrl, warpMI, statsFlag
472+
return maskedImage
484473

485474
@timeMethod
486475
def matchBackgrounds(self, refExposure, sciExposure):

0 commit comments

Comments
 (0)