22"""Retrieve extended PSF model and subtract bright stars at calexp (ie
26__all__ = [
"SubtractBrightStarsConnections",
"SubtractBrightStarsConfig",
"SubtractBrightStarsTask"]
28from functools
import reduce
29from operator
import ior
38 stringToStatisticsProperty,
41from lsst.geom import Box2I, Point2D, Point2I
43from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections, Struct
48 PipelineTaskConnections,
49 dimensions=(
"instrument",
"visit",
"detector"),
50 defaultTemplates={
"outputExposureName":
"brightStar_subtracted",
"outputBackgroundName":
"brightStars"},
52 inputExposure = cT.Input(
53 doc=
"Input exposure from which to subtract bright star stamps.",
55 storageClass=
"ExposureF",
61 inputBrightStarStamps = cT.Input(
62 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
63 name=
"brightStarStamps",
64 storageClass=
"BrightStarStamps",
70 inputExtendedPsf = cT.Input(
71 doc=
"Extended PSF model.",
73 storageClass=
"ExtendedPsf",
77 doc=
"Input Sky Correction to be subtracted from the calexp if ``doApplySkyCorr``=True.",
79 storageClass=
"Background",
86 outputExposure = cT.Output(
87 doc=
"Exposure with bright stars subtracted.",
88 name=
"{outputExposureName}_calexp",
89 storageClass=
"ExposureF",
95 outputBackgroundExposure = cT.Output(
96 doc=
"Exposure containing only the modelled bright stars.",
97 name=
"{outputBackgroundName}_calexp_background",
98 storageClass=
"ExposureF",
105 def __init__(self, *, config=None):
106 super().__init__(config=config)
107 if not config.doApplySkyCorr:
108 self.inputs.remove(
"skyCorr")
111class SubtractBrightStarsConfig(PipelineTaskConfig, pipelineConnections=SubtractBrightStarsConnections):
112 """Configuration parameters for SubtractBrightStarsTask"""
114 doWriteSubtractor = Field[bool](
116 doc=
"Should an exposure containing all bright star models be written to disk?",
119 doWriteSubtractedExposure = Field[bool](
121 doc=
"Should an exposure with bright stars subtracted be written to disk?",
124 magLimit = Field[float](
126 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be subtracted",
129 warpingKernelName = ChoiceField[str](
131 doc=
"Warping kernel",
134 "bilinear":
"bilinear interpolation",
135 "lanczos3":
"Lanczos kernel of order 3",
136 "lanczos4":
"Lanczos kernel of order 4",
137 "lanczos5":
"Lanczos kernel of order 5",
138 "lanczos6":
"Lanczos kernel of order 6",
139 "lanczos7":
"Lanczos kernel of order 7",
142 scalingType = ChoiceField[str](
144 doc=
"How the model should be scaled to each bright star; implemented options are "
145 "`annularFlux` to reuse the annular flux of each stamp, or `leastSquare` to perform "
146 "least square fitting on each pixel with no bad mask plane set.",
147 default=
"leastSquare",
149 "annularFlux":
"reuse BrightStarStamp annular flux measurement",
150 "leastSquare":
"find least square scaling factor",
153 badMaskPlanes = ListField[str](
155 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of "
156 "the scaling factor (`BAD` should always be included). Ignored if scalingType is `annularFlux`, "
157 "as the stamps are expected to already be normalized.",
161 default=(
"BAD",
"CR",
"CROSSTALK",
"EDGE",
"NO_DATA",
"SAT",
"SUSPECT",
"UNMASKEDNAN"),
163 doApplySkyCorr = Field[bool](
165 doc=
"Apply full focal plane sky correction before extracting stars?",
170class SubtractBrightStarsTask(PipelineTask):
171 """Use an extended PSF model to subtract bright stars from a calibrated
172 exposure (i.e. at single-visit level).
174 This task uses both a set of bright star stamps produced by
176 and an extended PSF model produced by
180 ConfigClass = SubtractBrightStarsConfig
181 _DefaultName = "subtractBrightStars"
183 def __init__(self, *args, **kwargs):
184 super().__init__(*args, **kwargs)
186 self.statsControl, self.statsFlag =
None,
None
188 def _setUpStatistics(self, exampleMask):
189 """Configure statistics control and flag, for use if ``scalingType`` is
192 if self.config.scalingType ==
"leastSquare":
195 andMask = reduce(ior, (exampleMask.getPlaneBitMask(bm)
for bm
in self.config.badMaskPlanes))
196 self.statsControl.setAndMask(andMask)
197 self.statsFlag = stringToStatisticsProperty(
"SUM")
199 def applySkyCorr(self, calexp, skyCorr):
200 """Apply correction to the sky background level.
201 Sky corrections can be generated via the SkyCorrectionTask within the
202 pipe_tools module. Because the sky model used by that code extends over
203 the entire focal plane, this can produce better sky subtraction.
204 The calexp is updated
in-place.
210 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`
211 Full focal plane sky correction, obtained by running
212 `~lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
214 if isinstance(calexp, Exposure):
215 calexp = calexp.getMaskedImage()
216 calexp -= skyCorr.getImage()
218 def scaleModel(self, model, star, inPlace=True, nb90Rots=0):
219 """Compute scaling factor to be applied to the extended PSF so that its
220 amplitude matches that of an individual star.
224 model : `~lsst.afw.image.MaskedImageF`
225 The extended PSF model, shifted (and potentially warped) to match
226 the bright star
's positioning.
228 A stamp centered on the bright star to be subtracted.
230 Whether the model should be scaled in place. Default
is `
True`.
232 The number of 90-degrees rotations to apply to the star stamp.
236 scalingFactor : `float`
237 The factor by which the model image should be multiplied
for it
238 to be scaled to the input bright star.
240 if self.config.scalingType ==
"annularFlux":
241 scalingFactor = star.annularFlux
242 elif self.config.scalingType ==
"leastSquare":
243 if self.statsControl
is None:
244 self._setUpStatistics(star.stamp_im.mask)
245 starIm = star.stamp_im.clone()
247 starIm = rotateImageBy90(starIm, nb90Rots)
249 starIm *= star.annularFlux
253 xy.image.array *= model.image.array
255 xx.image.array = model.image.array**2
257 xySum = makeStatistics(xy, self.statsFlag, self.statsControl).getValue()
258 xxSum = makeStatistics(xx, self.statsFlag, self.statsControl).getValue()
259 scalingFactor = xySum / xxSum
if xxSum
else 1
261 model.image *= scalingFactor
264 def runQuantum(self, butlerQC, inputRefs, outputRefs):
266 inputs = butlerQC.get(inputRefs)
267 dataId = butlerQC.quantum.dataId
268 subtractor, _ = self.run(**inputs, dataId=dataId)
269 if self.config.doWriteSubtractedExposure:
270 outputExposure = inputs[
"inputExposure"].clone()
271 outputExposure.image -= subtractor.image
273 outputExposure =
None
274 outputBackgroundExposure = subtractor
if self.config.doWriteSubtractor
else None
275 output = Struct(outputExposure=outputExposure, outputBackgroundExposure=outputBackgroundExposure)
276 butlerQC.put(output, outputRefs)
278 def run(self, inputExposure, inputBrightStarStamps, inputExtendedPsf, dataId, skyCorr=None):
279 """Iterate over all bright stars in an exposure to scale the extended
280 PSF model before subtracting bright stars.
284 inputExposure : `~lsst.afw.image.exposure.exposure.ExposureF`
285 The image from which bright stars should be subtracted.
286 inputBrightStarStamps :
288 Set of stamps centered on each bright star to be subtracted,
292 Extended PSF model, produced by
294 dataId : `dict`
or `~lsst.daf.butler.DataCoordinate`
295 The dataId of the exposure (
and detector) bright stars should be
297 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`, optional
298 Full focal plane sky correction, obtained by running
299 `~lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`. If
300 `doApplySkyCorr`
is set to `
True`, `skyCorr` cannot be `
None`.
304 subtractorExp : `~lsst.afw.image.ExposureF`
305 An Exposure containing a scaled bright star model fit to every
306 bright star profile; its image can then be subtracted
from the
308 invImages : `list` [`~lsst.afw.image.MaskedImageF`]
309 A list of small images (
"stamps") containing the model, each scaled
310 to its corresponding input bright star.
312 inputExpBBox = inputExposure.getBBox()
313 if self.config.doApplySkyCorr
and (skyCorr
is not None):
315 "Applying sky correction to exposure %s (exposure will be modified in-place).", dataId
317 self.applySkyCorr(inputExposure, skyCorr)
320 subtractorExp = ExposureF(bbox=inputExposure.getBBox())
321 subtractor = subtractorExp.maskedImage
323 model = inputExtendedPsf(dataId[
"detector"]).clone()
324 modelStampSize = model.getDimensions()
325 inv90Rots = 4 - inputBrightStarStamps.nb90Rots % 4
326 model = rotateImageBy90(model, inv90Rots)
331 for star
in inputBrightStarStamps:
332 if star.gaiaGMag < self.config.magLimit:
334 model.setXY0(star.position)
336 invTransform = star.archive_element.inverted()
337 invOrigin = Point2I(invTransform.applyForward(Point2D(star.position)))
338 bbox =
Box2I(corner=invOrigin, dimensions=modelStampSize)
339 invImage = MaskedImageF(bbox)
341 goodPix = warpImage(invImage, model, invTransform, warpCont)
344 f
"Warping of a model failed for star {star.gaiaId}: " "no good pixel in output"
347 self.scaleModel(invImage, star, inPlace=
True, nb90Rots=inv90Rots)
350 invImage.image.array[np.isnan(invImage.image.array)] = 0
351 bbox.clip(inputExpBBox)
352 if bbox.getArea() > 0:
353 subtractor[bbox] += invImage[bbox]
354 invImages.append(invImage)
355 return subtractorExp, invImages