LSST Applications g1653933729+34a971ddd9,g28da252d5a+6ecf39ab43,g2bbee38e9b+7117419c07,g2bc492864f+7117419c07,g2cdde0e794+704103fe75,g3156d2b45e+6e87dc994a,g347aa1857d+7117419c07,g35bb328faa+34a971ddd9,g3a166c0a6a+7117419c07,g3e281a1b8c+8ec26ec694,g4005a62e65+ba0306790b,g414038480c+40f8eb5d96,g569e0e2b34+d8e684fe9f,g6bb7d3c5bc+b9f74a9651,g717e5f8c0f+b9f74a9651,g76fe02a360+28e3f83f63,g80478fca09+fdb1f299e2,g82479be7b0+75dabfbf8d,g858d7b2824+b9f74a9651,g8cd86fa7b1+ed49507c07,g90e5cb8612+3848bed617,g9125e01d80+34a971ddd9,g979bb04a14+a66ccdebd6,g9a700aaafa+34a971ddd9,ga5288a1d22+3504ff4bc9,gae0086650b+34a971ddd9,gae74b0b5c6+761ee125be,gb3d89ef091+14ce01cc3a,gb58c049af0+ace264a4f2,gc28159a63d+7117419c07,gcf0d15dbbd+34f38ce56c,gda6a2b7d83+34f38ce56c,gdaeeff99f8+7774323b41,ge33fd446bb+b9f74a9651,ge5cf12406b+d328917b0a,ge79ae78c31+7117419c07,gf0baf85859+890af219f9,gf5289d68f6+da9a3f2f8e,w.2024.35
LSST Data Management Base Package
Loading...
Searching...
No Matches
subtractBrightStars.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22"""Retrieve extended PSF model and subtract bright stars at visit level."""
23
24__all__ = ["SubtractBrightStarsConnections", "SubtractBrightStarsConfig", "SubtractBrightStarsTask"]
25
26import logging
27from functools import reduce
28from operator import ior
29
30import numpy as np
31from lsst.afw.geom import SpanSet, Stencil
32from lsst.afw.image import Exposure, ExposureF, MaskedImageF
33from lsst.afw.math import (
34 StatisticsControl,
35 WarpingControl,
36 makeStatistics,
37 rotateImageBy90,
38 stringToStatisticsProperty,
39 warpImage,
40)
41from lsst.geom import Box2I, Point2D, Point2I
42from lsst.meas.algorithms import LoadReferenceObjectsConfig, ReferenceObjectLoader
43from lsst.meas.algorithms.brightStarStamps import BrightStarStamp, BrightStarStamps
44from lsst.pex.config import ChoiceField, ConfigField, Field, ListField
45from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections, Struct
46from lsst.pipe.base.connectionTypes import Input, Output, PrerequisiteInput
47from lsst.pipe.tasks.processBrightStars import ProcessBrightStarsTask
48
49logger = logging.getLogger(__name__)
50
51
53 PipelineTaskConnections,
54 dimensions=("instrument", "visit", "detector"),
55 defaultTemplates={
56 "outputExposureName": "brightStar_subtracted",
57 "outputBackgroundName": "brightStars",
58 "badStampsName": "brightStars",
59 },
60):
61 inputExposure = Input(
62 doc="Input exposure from which to subtract bright star stamps.",
63 name="calexp",
64 storageClass="ExposureF",
65 dimensions=(
66 "visit",
67 "detector",
68 ),
69 )
70 inputBrightStarStamps = Input(
71 doc="Set of preprocessed postage stamps, each centered on a single bright star.",
72 name="brightStarStamps",
73 storageClass="BrightStarStamps",
74 dimensions=(
75 "visit",
76 "detector",
77 ),
78 )
79 inputExtendedPsf = Input(
80 doc="Extended PSF model.",
81 name="extended_psf",
82 storageClass="ExtendedPsf",
83 dimensions=("band",),
84 )
85 skyCorr = Input(
86 doc="Input Sky Correction to be subtracted from the calexp if ``doApplySkyCorr``=True.",
87 name="skyCorr",
88 storageClass="Background",
89 dimensions=(
90 "instrument",
91 "visit",
92 "detector",
93 ),
94 )
95 refCat = PrerequisiteInput(
96 doc="Reference catalog that contains bright star positions",
97 name="gaia_dr3_20230707",
98 storageClass="SimpleCatalog",
99 dimensions=("skypix",),
100 multiple=True,
101 deferLoad=True,
102 )
103 outputExposure = Output(
104 doc="Exposure with bright stars subtracted.",
105 name="{outputExposureName}_calexp",
106 storageClass="ExposureF",
107 dimensions=(
108 "visit",
109 "detector",
110 ),
111 )
112 outputBackgroundExposure = Output(
113 doc="Exposure containing only the modelled bright stars.",
114 name="{outputBackgroundName}_calexp_background",
115 storageClass="ExposureF",
116 dimensions=(
117 "visit",
118 "detector",
119 ),
120 )
121 outputBadStamps = Output(
122 doc="The stamps that are not normalized and consequently not subtracted from the exposure.",
123 name="{badStampsName}_unsubtracted_stamps",
124 storageClass="BrightStarStamps",
125 dimensions=(
126 "visit",
127 "detector",
128 ),
129 )
130
131 def __init__(self, *, config=None):
132 super().__init__(config=config)
133 if not config.doApplySkyCorr:
134 self.inputs.remove("skyCorr")
135
136
137class SubtractBrightStarsConfig(PipelineTaskConfig, pipelineConnections=SubtractBrightStarsConnections):
138 """Configuration parameters for SubtractBrightStarsTask"""
139
140 doWriteSubtractor = Field[bool](
141 doc="Should an exposure containing all bright star models be written to disk?",
142 default=True,
143 )
144 doWriteSubtractedExposure = Field[bool](
145 doc="Should an exposure with bright stars subtracted be written to disk?",
146 default=True,
147 )
148 magLimit = Field[float](
149 doc="Magnitude limit, in Gaia G; all stars brighter than this value will be subtracted",
150 default=18,
151 )
152 minValidAnnulusFraction = Field[float](
153 doc="Minimum number of valid pixels that must fall within the annulus for the bright star to be "
154 "saved for subsequent generation of a PSF.",
155 default=0.0,
156 )
157 numSigmaClip = Field[float](
158 doc="Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
159 default=4,
160 )
161 numIter = Field[int](
162 doc="Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
163 default=3,
164 )
165 warpingKernelName = ChoiceField[str](
166 doc="Warping kernel",
167 default="lanczos5",
168 allowed={
169 "bilinear": "bilinear interpolation",
170 "lanczos3": "Lanczos kernel of order 3",
171 "lanczos4": "Lanczos kernel of order 4",
172 "lanczos5": "Lanczos kernel of order 5",
173 "lanczos6": "Lanczos kernel of order 6",
174 "lanczos7": "Lanczos kernel of order 7",
175 },
176 )
177 scalingType = ChoiceField[str](
178 doc="How the model should be scaled to each bright star; implemented options are "
179 "`annularFlux` to reuse the annular flux of each stamp, or `leastSquare` to perform "
180 "least square fitting on each pixel with no bad mask plane set.",
181 default="leastSquare",
182 allowed={
183 "annularFlux": "reuse BrightStarStamp annular flux measurement",
184 "leastSquare": "find least square scaling factor",
185 },
186 )
187 annularFluxStatistic = ChoiceField[str](
188 doc="Type of statistic to use to compute annular flux.",
189 default="MEANCLIP",
190 allowed={
191 "MEAN": "mean",
192 "MEDIAN": "median",
193 "MEANCLIP": "clipped mean",
194 },
195 )
196 badMaskPlanes = ListField[str](
197 doc="Mask planes that, if set, lead to associated pixels not being included in the computation of "
198 "the scaling factor (`BAD` should always be included). Ignored if scalingType is `annularFlux`, "
199 "as the stamps are expected to already be normalized.",
200 # Note that `BAD` should always be included, as secondary detected
201 # sources (i.e., detected sources other than the primary source of
202 # interest) also get set to `BAD`.
203 default=("BAD", "CR", "CROSSTALK", "EDGE", "NO_DATA", "SAT", "SUSPECT", "UNMASKEDNAN"),
204 )
205 subtractionBox = ListField[int](
206 doc="Size of the stamps to be extracted, in pixels.",
207 default=(250, 250),
208 )
209 subtractionBoxBuffer = Field[float](
210 doc=(
211 "'Buffer' (multiplicative) factor to be applied to determine the size of the stamp the "
212 "processed stars will be saved in. This is also the size of the extended PSF model. The buffer "
213 "region is masked and contain no data and subtractionBox determines the region where contains "
214 "the data."
215 ),
216 default=1.1,
217 )
218 doApplySkyCorr = Field[bool](
219 doc="Apply full focal plane sky correction before extracting stars?",
220 default=True,
221 )
222 refObjLoader = ConfigField[LoadReferenceObjectsConfig](
223 doc="Reference object loader for astrometric calibration.",
224 )
225
226
227class SubtractBrightStarsTask(PipelineTask):
228 """Use an extended PSF model to subtract bright stars from a calibrated
229 exposure (i.e. at single-visit level).
230
231 This task uses both a set of bright star stamps produced by
232 `~lsst.pipe.tasks.processBrightStars.ProcessBrightStarsTask`
233 and an extended PSF model produced by
234 `~lsst.pipe.tasks.extended_psf.MeasureExtendedPsfTask`.
235 """
236
237 ConfigClass = SubtractBrightStarsConfig
238 _DefaultName = "subtractBrightStars"
239
240 def __init__(self, *args, **kwargs):
241 super().__init__(*args, **kwargs)
242 # Placeholders to set up Statistics if scalingType is leastSquare.
243 self.statsControl, self.statsFlag = None, None
244 # Warping control; only contains shiftingALg provided in config.
245 self.warpControl = WarpingControl(self.config.warpingKernelName)
246
247 def runQuantum(self, butlerQC, inputRefs, outputRefs):
248 # Docstring inherited.
249 inputs = butlerQC.get(inputRefs)
250 if inputs["inputExtendedPsf"].default_extended_psf is None:
251 if not self._detectorInRegions(inputs["inputExposure"], inputs["inputExtendedPsf"]):
252 self.log.warn(
253 "Extended PSF model is not available for detector %i. Skipping withouth processing this "
254 "exposure.",
255 inputs["inputExposure"].detector.getId(),
256 )
257 return None
258 dataId = butlerQC.quantum.dataId
259 refObjLoader = ReferenceObjectLoader(
260 dataIds=[ref.datasetRef.dataId for ref in inputRefs.refCat],
261 refCats=inputs.pop("refCat"),
262 name=self.config.connections.refCat,
263 config=self.config.refObjLoader,
264 )
265 subtractor, _, badStamps = self.run(**inputs, dataId=dataId, refObjLoader=refObjLoader)
266 if self.config.doWriteSubtractedExposure:
267 outputExposure = inputs["inputExposure"].clone()
268 outputExposure.image -= subtractor.image
269 if self.config.doApplySkyCorr and (inputs["skyCorr"] is not None):
270 outputExposure.image += inputs["skyCorr"].getImage()
271 else:
272 outputExposure = None
273 outputBackgroundExposure = subtractor if self.config.doWriteSubtractor else None
274 # In its current state, the code produces outputBadStamps which are the
275 # stamps of stars that have not been subtracted from the image for any
276 # reason. If all the stars are subtracted from the calexp, the output
277 # is an empty fits file.
278 output = Struct(
279 outputExposure=outputExposure,
280 outputBackgroundExposure=outputBackgroundExposure,
281 outputBadStamps=badStamps,
282 )
283 butlerQC.put(output, outputRefs)
284
285 def run(
286 self, inputExposure, inputBrightStarStamps, inputExtendedPsf, dataId, skyCorr=None, refObjLoader=None
287 ):
288 """Iterate over all bright stars in an exposure to scale the extended
289 PSF model before subtracting bright stars.
290
291 Parameters
292 ----------
293 inputExposure : `~lsst.afw.image.ExposureF`
294 The image from which bright stars should be subtracted.
295 inputBrightStarStamps :
296 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamps`
297 Set of stamps centered on each bright star to be subtracted,
298 produced by running
299 `~lsst.pipe.tasks.processBrightStars.ProcessBrightStarsTask`.
300 inputExtendedPsf : `~lsst.pipe.tasks.extended_psf.ExtendedPsf`
301 Extended PSF model, produced by
302 `~lsst.pipe.tasks.extended_psf.MeasureExtendedPsfTask`.
303 dataId : `dict` or `~lsst.daf.butler.DataCoordinate`
304 The dataId of the exposure (and detector) bright stars should be
305 subtracted from.
306 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`, optional
307 Full focal plane sky correction, obtained by running
308 `~lsst.pipe.tasks.skyCorrection.SkyCorrectionTask`. If
309 `doApplySkyCorr` is set to `True`, `skyCorr` cannot be `None`.
310 refObjLoader : `~lsst.meas.algorithms.ReferenceObjectLoader`, optional
311 Loader to find objects within a reference catalog.
312
313 Returns
314 -------
315 subtractorExp : `~lsst.afw.image.ExposureF`
316 An Exposure containing a scaled bright star model fit to every
317 bright star profile; its image can then be subtracted from the
318 input exposure.
319 invImages : `list` [`~lsst.afw.image.MaskedImageF`]
320 A list of small images ("stamps") containing the model, each scaled
321 to its corresponding input bright star.
322 """
323 self.inputExpBBox = inputExposure.getBBox()
324 if self.config.doApplySkyCorr and (skyCorr is not None):
325 self.log.info(
326 "Applying sky correction to exposure %s (exposure will be modified in-place).", dataId
327 )
328 self.applySkyCorr(inputExposure, skyCorr)
329
330 # Create an empty image the size of the exposure.
331 # TODO: DM-31085 (set mask planes).
332 subtractorExp = ExposureF(bbox=inputExposure.getBBox())
333 subtractor = subtractorExp.maskedImage
334
335 # Make a copy of the input model.
336 self.model = inputExtendedPsf(dataId["detector"]).clone()
337 self.modelStampSize = self.model.getDimensions()
338 # Number of 90 deg. rotations to reverse each stamp's rotation.
339 self.inv90Rots = 4 - inputBrightStarStamps.nb90Rots % 4
340 self.model = rotateImageBy90(self.model, self.inv90Rots)
341
342 brightStarList = self.makeBrightStarList(inputBrightStarStamps, inputExposure, refObjLoader)
343 invImages = []
344 subtractor, invImages = self.buildSubtractor(
345 inputBrightStarStamps, subtractor, invImages, multipleAnnuli=False
346 )
347 if brightStarList:
348 self.setMissedStarsStatsControl()
349 # This may change when multiple star bins are used for PSF
350 # creation.
351 innerRadius = inputBrightStarStamps._innerRadius
352 outerRadius = inputBrightStarStamps._outerRadius
353 brightStarStamps, badStamps = BrightStarStamps.initAndNormalize(
354 brightStarList,
355 innerRadius=innerRadius,
356 outerRadius=outerRadius,
357 nb90Rots=self.warpOutputs.nb90Rots,
358 imCenter=self.warper.modelCenter,
359 use_archive=True,
360 statsControl=self.missedStatsControl,
361 statsFlag=self.missedStatsFlag,
362 badMaskPlanes=self.warper.config.badMaskPlanes,
363 discardNanFluxObjects=False,
364 forceFindFlux=True,
365 )
366 self.metadata["subtractedStarCount"] = len(inputBrightStarStamps) + len(brightStarStamps)
367 self.psf_annular_fluxes = self.findPsfAnnularFluxes(brightStarStamps)
368 subtractor, invImages = self.buildSubtractor(
369 brightStarStamps, subtractor, invImages, multipleAnnuli=True
370 )
371 else:
372 self.metadata["subtractedStarCount"] = len(inputBrightStarStamps)
373 badStamps = []
374 badStamps = BrightStarStamps(badStamps)
375
376 return subtractorExp, invImages, badStamps
377
378 def _detectorInRegions(self, inputExposure, inputExtendedPsf):
379 """Determine whether the input exposure's detector is in the region(s)
380 where the extended PSF model(s) is(are) available.
381
382 Parameters
383 ----------
384 inputExposure : `lsst.afw.image.ExposureF`
385 The image from which bright stars should be subtracted. The ID of
386 the detector will be used to determine whether the detector is in
387 the region(s) where the extended PSF model(s) is(are) available.
388 inputExtendedPsf: `~lsst.pipe.tasks.extended_psf.ExtendedPsf`
389 Extended PSF model(s), produced by
390 `~lsst.pipe.tasks.extended_psf.MeasureExtendedPsfTask`. The ID's of
391 the detectors in the region(s) where the extended PSF model(s)
392 is(are) available will be used to cross match with the ID of the
393 input exposure's detector.
394
395 Returns
396 -------
397 `bool`
398 True if the detector is in the region(s) where the extended PSF
399 model(s) is(are) available, False otherwise.
400 """
401 availableDetectors = [
402 detector
403 for detectorList in inputExtendedPsf.detectors_focal_plane_regions.values()
404 for detector in detectorList.detectors
405 ]
406 if inputExposure.detector.getId() in availableDetectors:
407 return True
408 else:
409 return False
410
411 def _setUpStatistics(self, exampleMask):
412 """Configure statistics control and flag, for use if ``scalingType`` is
413 `leastSquare`.
414 """
415 if self.config.scalingType == "leastSquare":
416 # Set the mask planes which will be ignored.
417 andMask = reduce(
418 ior,
419 (exampleMask.getPlaneBitMask(bm) for bm in self.config.badMaskPlanes),
420 )
421 self.statsControl = StatisticsControl(
422 andMask=andMask,
423 )
424 self.statsFlag = stringToStatisticsProperty("SUM")
425
426 def applySkyCorr(self, calexp, skyCorr):
427 """Apply correction to the sky background level.
428 Sky corrections can be generated via the SkyCorrectionTask within the
429 pipe_tools module. Because the sky model used by that code extends over
430 the entire focal plane, this can produce better sky subtraction.
431 The calexp is updated in-place.
432
433 Parameters
434 ----------
435 calexp : `~lsst.afw.image.Exposure` or `~lsst.afw.image.MaskedImage`
436 Calibrated exposure.
437 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`
438 Full focal plane sky correction, obtained by running
439 `~lsst.pipe.tasks.skyCorrection.SkyCorrectionTask`.
440 """
441 if isinstance(calexp, Exposure):
442 calexp = calexp.getMaskedImage()
443 calexp -= skyCorr.getImage()
444
445 def scaleModel(self, model, star, inPlace=True, nb90Rots=0, psf_annular_flux=1.0):
446 """Compute scaling factor to be applied to the extended PSF so that its
447 amplitude matches that of an individual star.
448
449 Parameters
450 ----------
451 model : `~lsst.afw.image.MaskedImageF`
452 The extended PSF model, shifted (and potentially warped) to match
453 the bright star position.
454 star : `~lsst.meas.algorithms.brightStarStamps.BrightStarStamp`
455 A stamp centered on the bright star to be subtracted.
456 inPlace : `bool`
457 Whether the model should be scaled in place. Default is `True`.
458 nb90Rots : `int`
459 The number of 90-degrees rotations to apply to the star stamp.
460 psf_annular_flux: `float`, optional
461 The annular flux of the PSF model at the radius where the flux of
462 the given star is determined. This is 1 for stars present in
463 inputBrightStarStamps, but can be different for stars that are
464 missing from inputBrightStarStamps.
465
466 Returns
467 -------
468 scalingFactor : `float`
469 The factor by which the model image should be multiplied for it
470 to be scaled to the input bright star.
471 """
472 if self.config.scalingType == "annularFlux":
473 scalingFactor = star.annularFlux * psf_annular_flux
474 elif self.config.scalingType == "leastSquare":
475 if self.statsControl is None:
476 self._setUpStatistics(star.stamp_im.mask)
477 starIm = star.stamp_im.clone()
478 # Rotate the star postage stamp.
479 starIm = rotateImageBy90(starIm, nb90Rots)
480 # Reverse the prior star flux normalization ("unnormalize").
481 starIm *= star.annularFlux
482 # The estimator of the scalingFactor (f) that minimizes (Y-fX)^2
483 # is E[XY]/E[XX].
484 xy = starIm.clone()
485 xy.image.array *= model.image.array
486 xx = starIm.clone()
487 xx.image.array = model.image.array**2
488 # Compute the least squares scaling factor.
489 xySum = makeStatistics(xy, self.statsFlag, self.statsControl).getValue()
490 xxSum = makeStatistics(xx, self.statsFlag, self.statsControl).getValue()
491 scalingFactor = xySum / xxSum if xxSum else 1
492 if inPlace:
493 model.image *= scalingFactor
494 return scalingFactor
495
496 def _overrideWarperConfig(self):
497 """Override the warper config with the config of this task.
498
499 This override is necessary for stars that are missing from the
500 inputBrightStarStamps object but still need to be subtracted.
501 """
502 # TODO: Replace these copied values with a warperConfig.
503 self.warper.config.minValidAnnulusFraction = self.config.minValidAnnulusFraction
504 self.warper.config.numSigmaClip = self.config.numSigmaClip
505 self.warper.config.numIter = self.config.numIter
506 self.warper.config.annularFluxStatistic = self.config.annularFluxStatistic
507 self.warper.config.badMaskPlanes = self.config.badMaskPlanes
508 self.warper.config.stampSize = self.config.subtractionBox
509 self.warper.modelStampBuffer = self.config.subtractionBoxBuffer
510 self.warper.config.magLimit = self.config.magLimit
511 self.warper.setModelStamp()
512
513 def setMissedStarsStatsControl(self):
514 """Configure statistics control for processing missing stars from
515 inputBrightStarStamps.
516 """
517 self.missedStatsControl = StatisticsControl(
518 numSigmaClip=self.warper.config.numSigmaClip,
519 numIter=self.warper.config.numIter,
520 )
521 self.missedStatsFlag = stringToStatisticsProperty(self.warper.config.annularFluxStatistic)
522
523 def setWarpTask(self):
524 """Create an instance of ProcessBrightStarsTask that will be used to
525 produce stamps of stars to be subtracted.
526 """
527 self.warper = ProcessBrightStarsTask()
528 self._overrideWarperConfig()
529 self.warper.modelCenter = self.modelStampSize[0] // 2, self.modelStampSize[1] // 2
530
531 def makeBrightStarList(self, inputBrightStarStamps, inputExposure, refObjLoader):
532 """Make a list of bright stars that are missing from
533 inputBrightStarStamps to be subtracted.
534
535 Parameters
536 ----------
537 inputBrightStarStamps :
538 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamps`
539 Set of stamps centered on each bright star to be subtracted,
540 produced by running
541 `~lsst.pipe.tasks.processBrightStars.ProcessBrightStarsTask`.
542 inputExposure : `~lsst.afw.image.ExposureF`
543 The image from which bright stars should be subtracted.
544 refObjLoader : `~lsst.meas.algorithms.ReferenceObjectLoader`, optional
545 Loader to find objects within a reference catalog.
546
547 Returns
548 -------
549 brightStarList:
550 A list containing
551 `lsst.meas.algorithms.brightStarStamps.BrightStarStamp` of stars to
552 be subtracted.
553 """
554 self.setWarpTask()
555 missedStars = self.warper.extractStamps(
556 inputExposure, refObjLoader=refObjLoader, inputBrightStarStamps=inputBrightStarStamps
557 )
558 if missedStars.starStamps:
559 self.warpOutputs = self.warper.warpStamps(missedStars.starStamps, missedStars.pixCenters)
560 brightStarList = [
562 stamp_im=warp,
563 archive_element=transform,
564 position=self.warpOutputs.xy0s[j],
565 gaiaGMag=missedStars.gMags[j],
566 gaiaId=missedStars.gaiaIds[j],
567 minValidAnnulusFraction=self.warper.config.minValidAnnulusFraction,
568 )
569 for j, (warp, transform) in enumerate(
570 zip(self.warpOutputs.warpedStars, self.warpOutputs.warpTransforms)
571 )
572 ]
573 self.metadata["allStarCount"] = len(inputBrightStarStamps) + len(brightStarList)
574 else:
575 brightStarList = []
576 self.metadata["allStarCount"] = len(inputBrightStarStamps)
577 return brightStarList
578
579 def initAnnulusImage(self):
580 """Initialize an annulus image of the given star.
581
582 Returns
583 -------
584 annulusImage : `~lsst.afw.image.MaskedImageF`
585 The initialized annulus image.
586 """
587 maskPlaneDict = self.model.mask.getMaskPlaneDict()
588 annulusImage = MaskedImageF(self.modelStampSize, planeDict=maskPlaneDict)
589 annulusImage.mask.array[:] = 2 ** maskPlaneDict["NO_DATA"]
590 return annulusImage
591
592 def createAnnulus(self, brightStarStamp):
593 """Create a circular annulus around the given star.
594
595 The circular annulus is set based on the inner and outer optimal radii.
596 These radii describe the annulus where the flux of the star is found.
597 The aim is to create the same annulus for the PSF model, eventually
598 measuring the model flux around that annulus.
599 An optimal radius usually differs from the radius where the PSF model
600 is normalized.
601
602 Parameters
603 ----------
604 brightStarStamp :
605 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamp`
606 A stamp of a bright star to be subtracted.
607
608 Returns
609 -------
610 annulus : `~lsst.afw.image.MaskedImageF`
611 An annulus of the given star.
612 """
613 # Create SpanSet of annulus.
614 outerCircle = SpanSet.fromShape(
615 brightStarStamp.optimalOuterRadius, Stencil.CIRCLE, offset=self.warper.modelCenter
616 )
617 innerCircle = SpanSet.fromShape(
618 brightStarStamp.optimalInnerRadius, Stencil.CIRCLE, offset=self.warper.modelCenter
619 )
620 annulus = outerCircle.intersectNot(innerCircle)
621 return annulus
622
623 def applyStatsControl(self, annulusImage):
624 """Apply statistics control to the PSF annulus image.
625
626 Parameters
627 ----------
628 annulusImage : `~lsst.afw.image.MaskedImageF`
629 An image containing an annulus of the given model.
630
631 Returns
632 -------
633 annularFlux: float
634 The annular flux of the PSF model at the radius where the flux of
635 the given star is determined.
636 """
637 andMask = reduce(
638 ior, (annulusImage.mask.getPlaneBitMask(bm) for bm in self.warper.config.badMaskPlanes)
639 )
640 self.missedStatsControl.setAndMask(andMask)
641 annulusStat = makeStatistics(annulusImage, self.missedStatsFlag, self.missedStatsControl)
642 return annulusStat.getValue()
643
644 def findPsfAnnularFlux(self, brightStarStamp, maskedModel):
645 """Find the annular flux of the PSF model within a specified annulus.
646
647 This flux will be used for re-scaling the PSF to the level of stars
648 with bad stamps. Stars with bad stamps are those without a flux within
649 the normalization annulus.
650
651 Parameters
652 ----------
653 brightStarStamp :
654 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamp`
655 A stamp of a bright star to be subtracted.
656 maskedModel : `~lsst.afw.image.MaskedImageF`
657 A masked image of the PSF model.
658
659 Returns
660 -------
661 annularFlux: float (between 0 and 1)
662 The annular flux of the PSF model at the radius where the flux of
663 the given star is determined.
664 """
665 annulusImage = self.initAnnulusImage()
666 annulus = self.createAnnulus(brightStarStamp)
667 annulus.copyMaskedImage(maskedModel, annulusImage)
668 annularFlux = self.applyStatsControl(annulusImage)
669 return annularFlux
670
671 def findPsfAnnularFluxes(self, brightStarStamps):
672 """Find the annular fluxes of the given PSF model.
673
674 Parameters
675 ----------
676 brightStarStamps :
677 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamps`
678 The stamps of stars that will be subtracted from the exposure.
679
680 Returns
681 -------
682 PsfAnnularFluxes: numpy.array
683 A two column numpy.array containing annular fluxes of the PSF at
684 radii where the flux for stars exist (could be found).
685
686 Notes
687 -----
688 While the PSF model is normalized at a certain radius, the annular flux
689 of a star around that radius might be impossible to find. Therefore, we
690 have to scale the PSF model considering a radius where the star has an
691 identified flux. To do that, the flux of the model should be found and
692 used to adjust the scaling step.
693 """
694 outerRadii = []
695 annularFluxes = []
696 maskedModel = MaskedImageF(self.model.image)
697 # The model has wrong bbox values. Should be fixed in extended_psf.py?
698 maskedModel.setXY0(0, 0)
699 for star in brightStarStamps:
700 if star.optimalOuterRadius not in outerRadii:
701 annularFlux = self.findPsfAnnularFlux(star, maskedModel)
702 outerRadii.append(star.optimalOuterRadius)
703 annularFluxes.append(annularFlux)
704 return np.array([outerRadii, annularFluxes]).T
705
706 def preparePlaneModelStamp(self, brightStarStamp):
707 """Prepare the PSF plane model stamp.
708
709 It is called PlaneModel because, while it is a PSF model stamp that is
710 warped and rotated to the same orientation of a chosen star, it is not
711 yet scaled to the brightness level of the star.
712
713 Parameters
714 ----------
715 brightStarStamp :
716 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamp`
717 The stamp of the star to which the PSF model will be scaled.
718
719 Returns
720 -------
721 bbox: `~lsst.geom.Box2I`
722 Contains the corner coordination and the dimensions of the model
723 stamp.
724
725 invImage: `~lsst.afw.image.MaskedImageF`
726 The extended PSF model, shifted (and potentially warped and
727 rotated) to match the bright star position.
728
729 Raises
730 ------
731 RuntimeError
732 Raised if warping of the model failed.
733
734 Notes
735 -----
736 Since detectors have different orientations, the PSF model should be
737 rotated to match the orientation of the detectors in some cases. To do
738 that, the code uses the inverse of the transform that is applied to the
739 bright star stamp to match the orientation of the detector.
740 """
741 # Set the origin.
742 self.model.setXY0(brightStarStamp.position)
743 # Create an empty destination image.
744 invTransform = brightStarStamp.archive_element.inverted()
745 invOrigin = Point2I(invTransform.applyForward(Point2D(brightStarStamp.position)))
746 bbox = Box2I(corner=invOrigin, dimensions=self.modelStampSize)
747 invImage = MaskedImageF(bbox)
748 # Apply inverse transform.
749 goodPix = warpImage(invImage, self.model, invTransform, self.warpControl)
750 if not goodPix:
751 # Do we want to find another way or just subtract the non-warped
752 # scaled model?
753 # Currently the code just leaves the failed ones un-subtracted.
754 raise RuntimeError(
755 f"Warping of a model failed for star {brightStarStamp.gaiaId}: no good pixel in output."
756 )
757 return bbox, invImage
758
759 def addScaledModel(self, subtractor, brightStarStamp, multipleAnnuli=False):
760 """Add the scaled model of the given star to the subtractor plane.
761
762 Parameters
763 ----------
764 subtractor : `~lsst.afw.image.MaskedImageF`
765 The full image containing the scaled model of bright stars to be
766 subtracted from the input exposure.
767 brightStarStamp :
768 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamp`
769 The stamp of the star of which the PSF model will be scaled and
770 added to the subtractor.
771 multipleAnnuli : bool, optional
772 If true, the model should be scaled based on a flux at a radius
773 other than its normalization radius.
774
775 Returns
776 -------
777 subtractor : `~lsst.afw.image.MaskedImageF`
778 The input subtractor full image with the added scaled model at the
779 given star's location in the exposure.
780 invImage: `~lsst.afw.image.MaskedImageF`
781 The extended PSF model, shifted (and potentially warped) to match
782 the bright star position.
783 """
784 bbox, invImage = self.preparePlaneModelStamp(brightStarStamp)
785 bbox.clip(self.inputExpBBox)
786 if bbox.getArea() > 0:
787 if multipleAnnuli:
788 cond = self.psf_annular_fluxes[:, 0] == brightStarStamp.optimalOuterRadius
789 psf_annular_flux = self.psf_annular_fluxes[cond, 1][0]
790 self.scaleModel(
791 invImage,
792 brightStarStamp,
793 inPlace=True,
794 nb90Rots=self.inv90Rots,
795 psf_annular_flux=psf_annular_flux,
796 )
797 else:
798 self.scaleModel(invImage, brightStarStamp, inPlace=True, nb90Rots=self.inv90Rots)
799 # Replace NaNs before subtraction (all NaNs have the NO_DATA flag).
800 invImage.image.array[np.isnan(invImage.image.array)] = 0
801 subtractor[bbox] += invImage[bbox]
802 return subtractor, invImage
803
804 def buildSubtractor(self, brightStarStamps, subtractor, invImages, multipleAnnuli=False):
805 """Build an image containing potentially multiple scaled PSF models,
806 each at the location of a given bright star.
807
808 Parameters
809 ----------
810 brightStarStamps :
811 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamps`
812 Set of stamps centered on each bright star to be subtracted,
813 produced by running
814 `~lsst.pipe.tasks.processBrightStars.ProcessBrightStarsTask`.
815 subtractor : `~lsst.afw.image.MaskedImageF`
816 The Exposure that will contain the scaled model of bright stars to
817 be subtracted from the exposure.
818 invImages : `list`
819 A list containing extended PSF models, shifted (and potentially
820 warped) to match the bright stars positions.
821 multipleAnnuli : bool, optional
822 This will be passed to addScaledModel method, by default False.
823
824 Returns
825 -------
826 subtractor : `~lsst.afw.image.MaskedImageF`
827 An Exposure containing a scaled bright star model fit to every
828 bright star profile; its image can then be subtracted from the
829 input exposure.
830 invImages: list
831 A list containing the extended PSF models, shifted (and potentially
832 warped) to match bright stars' positions.
833 """
834 for star in brightStarStamps:
835 if star.gaiaGMag < self.config.magLimit:
836 try:
837 # Add the scaled model at the star location to subtractor.
838 subtractor, invImage = self.addScaledModel(subtractor, star, multipleAnnuli)
839 invImages.append(invImage)
840 except RuntimeError as err:
841 logger.error(err)
842 return subtractor, invImages
Pass parameters to a Statistics object.
Definition Statistics.h:83
Parameters to control convolution.
An integer coordinate rectangle.
Definition Box.h:55