22"""Retrieve extended PSF model and subtract bright stars at visit level."""
24__all__ = [
"SubtractBrightStarsConnections",
"SubtractBrightStarsConfig",
"SubtractBrightStarsTask"]
26from functools
import reduce
27from operator
import ior
36 stringToStatisticsProperty,
39from lsst.geom import Box2I, Point2D, Point2I
41from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections, Struct
42from lsst.pipe.base.connectionTypes
import Input, Output
46 PipelineTaskConnections,
47 dimensions=(
"instrument",
"visit",
"detector"),
48 defaultTemplates={
"outputExposureName":
"brightStar_subtracted",
"outputBackgroundName":
"brightStars"},
50 inputExposure = Input(
51 doc=
"Input exposure from which to subtract bright star stamps.",
53 storageClass=
"ExposureF",
59 inputBrightStarStamps = Input(
60 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
61 name=
"brightStarStamps",
62 storageClass=
"BrightStarStamps",
68 inputExtendedPsf = Input(
69 doc=
"Extended PSF model.",
71 storageClass=
"ExtendedPsf",
75 doc=
"Input Sky Correction to be subtracted from the calexp if ``doApplySkyCorr``=True.",
77 storageClass=
"Background",
84 outputExposure = Output(
85 doc=
"Exposure with bright stars subtracted.",
86 name=
"{outputExposureName}_calexp",
87 storageClass=
"ExposureF",
93 outputBackgroundExposure = Output(
94 doc=
"Exposure containing only the modelled bright stars.",
95 name=
"{outputBackgroundName}_calexp_background",
96 storageClass=
"ExposureF",
103 def __init__(self, *, config=None):
104 super().__init__(config=config)
105 if not config.doApplySkyCorr:
106 self.inputs.remove(
"skyCorr")
109class SubtractBrightStarsConfig(PipelineTaskConfig, pipelineConnections=SubtractBrightStarsConnections):
110 """Configuration parameters for SubtractBrightStarsTask"""
112 doWriteSubtractor = Field[bool](
114 doc=
"Should an exposure containing all bright star models be written to disk?",
117 doWriteSubtractedExposure = Field[bool](
119 doc=
"Should an exposure with bright stars subtracted be written to disk?",
122 magLimit = Field[float](
124 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be subtracted",
127 warpingKernelName = ChoiceField[str](
129 doc=
"Warping kernel",
132 "bilinear":
"bilinear interpolation",
133 "lanczos3":
"Lanczos kernel of order 3",
134 "lanczos4":
"Lanczos kernel of order 4",
135 "lanczos5":
"Lanczos kernel of order 5",
136 "lanczos6":
"Lanczos kernel of order 6",
137 "lanczos7":
"Lanczos kernel of order 7",
140 scalingType = ChoiceField[str](
142 doc=
"How the model should be scaled to each bright star; implemented options are "
143 "`annularFlux` to reuse the annular flux of each stamp, or `leastSquare` to perform "
144 "least square fitting on each pixel with no bad mask plane set.",
145 default=
"leastSquare",
147 "annularFlux":
"reuse BrightStarStamp annular flux measurement",
148 "leastSquare":
"find least square scaling factor",
151 badMaskPlanes = ListField[str](
153 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of "
154 "the scaling factor (`BAD` should always be included). Ignored if scalingType is `annularFlux`, "
155 "as the stamps are expected to already be normalized.",
159 default=(
"BAD",
"CR",
"CROSSTALK",
"EDGE",
"NO_DATA",
"SAT",
"SUSPECT",
"UNMASKEDNAN"),
161 doApplySkyCorr = Field[bool](
163 doc=
"Apply full focal plane sky correction before extracting stars?",
168class SubtractBrightStarsTask(PipelineTask):
169 """Use an extended PSF model to subtract bright stars from a calibrated
170 exposure (i.e. at single-visit level).
172 This task uses both a set of bright star stamps produced by
174 and an extended PSF model produced by
178 ConfigClass = SubtractBrightStarsConfig
179 _DefaultName = "subtractBrightStars"
181 def __init__(self, *args, **kwargs):
182 super().__init__(*args, **kwargs)
184 self.statsControl, self.statsFlag =
None,
None
186 def _setUpStatistics(self, exampleMask):
187 """Configure statistics control and flag, for use if ``scalingType`` is
190 if self.config.scalingType ==
"leastSquare":
193 andMask = reduce(ior, (exampleMask.getPlaneBitMask(bm)
for bm
in self.config.badMaskPlanes))
194 self.statsControl.setAndMask(andMask)
195 self.statsFlag = stringToStatisticsProperty(
"SUM")
197 def applySkyCorr(self, calexp, skyCorr):
198 """Apply correction to the sky background level.
199 Sky corrections can be generated via the SkyCorrectionTask within the
200 pipe_tools module. Because the sky model used by that code extends over
201 the entire focal plane, this can produce better sky subtraction.
202 The calexp is updated
in-place.
208 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`
209 Full focal plane sky correction, obtained by running
212 if isinstance(calexp, Exposure):
213 calexp = calexp.getMaskedImage()
214 calexp -= skyCorr.getImage()
216 def scaleModel(self, model, star, inPlace=True, nb90Rots=0):
217 """Compute scaling factor to be applied to the extended PSF so that its
218 amplitude matches that of an individual star.
222 model : `~lsst.afw.image.MaskedImageF`
223 The extended PSF model, shifted (and potentially warped) to match
224 the bright star
's positioning.
226 A stamp centered on the bright star to be subtracted.
228 Whether the model should be scaled in place. Default
is `
True`.
230 The number of 90-degrees rotations to apply to the star stamp.
234 scalingFactor : `float`
235 The factor by which the model image should be multiplied
for it
236 to be scaled to the input bright star.
238 if self.config.scalingType ==
"annularFlux":
239 scalingFactor = star.annularFlux
240 elif self.config.scalingType ==
"leastSquare":
241 if self.statsControl
is None:
242 self._setUpStatistics(star.stamp_im.mask)
243 starIm = star.stamp_im.clone()
245 starIm = rotateImageBy90(starIm, nb90Rots)
247 starIm *= star.annularFlux
251 xy.image.array *= model.image.array
253 xx.image.array = model.image.array**2
255 xySum = makeStatistics(xy, self.statsFlag, self.statsControl).getValue()
256 xxSum = makeStatistics(xx, self.statsFlag, self.statsControl).getValue()
257 scalingFactor = xySum / xxSum
if xxSum
else 1
259 model.image *= scalingFactor
262 def runQuantum(self, butlerQC, inputRefs, outputRefs):
264 inputs = butlerQC.get(inputRefs)
265 dataId = butlerQC.quantum.dataId
266 subtractor, _ = self.run(**inputs, dataId=dataId)
267 if self.config.doWriteSubtractedExposure:
268 outputExposure = inputs[
"inputExposure"].clone()
269 outputExposure.image -= subtractor.image
271 outputExposure =
None
272 outputBackgroundExposure = subtractor
if self.config.doWriteSubtractor
else None
273 output = Struct(outputExposure=outputExposure, outputBackgroundExposure=outputBackgroundExposure)
274 butlerQC.put(output, outputRefs)
276 def run(self, inputExposure, inputBrightStarStamps, inputExtendedPsf, dataId, skyCorr=None):
277 """Iterate over all bright stars in an exposure to scale the extended
278 PSF model before subtracting bright stars.
282 inputExposure : `~lsst.afw.image.ExposureF`
283 The image from which bright stars should be subtracted.
284 inputBrightStarStamps :
286 Set of stamps centered on each bright star to be subtracted,
290 Extended PSF model, produced by
292 dataId : `dict`
or `~lsst.daf.butler.DataCoordinate`
293 The dataId of the exposure (
and detector) bright stars should be
295 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`, optional
296 Full focal plane sky correction, obtained by running
298 `doApplySkyCorr`
is set to `
True`, `skyCorr` cannot be `
None`.
302 subtractorExp : `~lsst.afw.image.ExposureF`
303 An Exposure containing a scaled bright star model fit to every
304 bright star profile; its image can then be subtracted
from the
306 invImages : `list` [`~lsst.afw.image.MaskedImageF`]
307 A list of small images (
"stamps") containing the model, each scaled
308 to its corresponding input bright star.
310 inputExpBBox = inputExposure.getBBox()
311 if self.config.doApplySkyCorr
and (skyCorr
is not None):
313 "Applying sky correction to exposure %s (exposure will be modified in-place).", dataId
315 self.applySkyCorr(inputExposure, skyCorr)
318 subtractorExp = ExposureF(bbox=inputExposure.getBBox())
319 subtractor = subtractorExp.maskedImage
321 model = inputExtendedPsf(dataId[
"detector"]).clone()
322 modelStampSize = model.getDimensions()
323 inv90Rots = 4 - inputBrightStarStamps.nb90Rots % 4
324 model = rotateImageBy90(model, inv90Rots)
329 for star
in inputBrightStarStamps:
330 if star.gaiaGMag < self.config.magLimit:
332 model.setXY0(star.position)
334 invTransform = star.archive_element.inverted()
335 invOrigin = Point2I(invTransform.applyForward(Point2D(star.position)))
336 bbox =
Box2I(corner=invOrigin, dimensions=modelStampSize)
337 invImage = MaskedImageF(bbox)
339 goodPix = warpImage(invImage, model, invTransform, warpCont)
342 f
"Warping of a model failed for star {star.gaiaId}: " "no good pixel in output"
345 self.scaleModel(invImage, star, inPlace=
True, nb90Rots=inv90Rots)
348 invImage.image.array[np.isnan(invImage.image.array)] = 0
349 bbox.clip(inputExpBBox)
350 if bbox.getArea() > 0:
351 subtractor[bbox] += invImage[bbox]
352 invImages.append(invImage)
353 return subtractorExp, invImages
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A class to manipulate images, masks, and variance as a single object.
Pass parameters to a Statistics object.
Parameters to control convolution.
An integer coordinate rectangle.