LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
brightStarStamps.py
Go to the documentation of this file.
1# This file is part of meas_algorithms.
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"""Collection of small images (stamps), each centered on a bright star.
23"""
24
25__all__ = ["BrightStarStamp", "BrightStarStamps"]
26
27from dataclasses import dataclass
28from operator import ior
29from functools import reduce
30from typing import Optional
31import numpy as np
32
33from lsst.afw.image import MaskedImageF
34from lsst.afw import geom as afwGeom
35from lsst.afw import math as afwMath
36from lsst.afw import table as afwTable
37from lsst.geom import Point2I
38from .stamps import Stamps, AbstractStamp, readFitsWithOptions
39
40
41@dataclass
43 """Single stamp centered on a bright star, normalized by its
44 annularFlux.
45
46 Parameters
47 ----------
49 Pixel data for this postage stamp
50 position : `lsst.geom.Point2I`
51 Origin of the stamps in its origin exposure (pixels)
52 gaiaGMag : `float`
53 Gaia G magnitude for the object in this stamp
54 gaiaId : `int`
55 Gaia object identifier
56 annularFlux : `Optional[float]`
57 Flux in an annulus around the object
58 """
59 stamp_im: MaskedImageF
60 gaiaGMag: float
61 gaiaId: int
62 position: Point2I
63 archive_element: Optional[afwTable.io.Persistable] = None
64 annularFlux: Optional[float] = None
65
66 @classmethod
67 def factory(cls, stamp_im, metadata, idx, archive_element=None):
68 """This method is needed to service the FITS reader.
69 We need a standard interface to construct objects like this.
70 Parameters needed to construct this object are passed in via
71 a metadata dictionary and then passed to the constructor of
72 this class. This particular factory method requires keys:
73 G_MAGS, GAIA_IDS, and ANNULAR_FLUXES. They should each
74 point to lists of values.
75
76 Parameters
77 ----------
79 Pixel data to pass to the constructor
80 metadata : `dict`
81 Dictionary containing the information
82 needed by the constructor.
83 idx : `int`
84 Index into the lists in ``metadata``
85 archive_element : `lsst.afwTable.io.Persistable`, optional
86 Archive element (e.g. Transform or WCS) associated with this stamp.
87
88 Returns
89 -------
90 brightstarstamp : `BrightStarStamp`
91 An instance of this class
92 """
93 if 'X0S' in metadata and 'Y0S' in metadata:
94 x0 = metadata.getArray('X0S')[idx]
95 y0 = metadata.getArray('Y0S')[idx]
96 position = Point2I(x0, y0)
97 else:
98 position = None
99 return cls(stamp_im=stamp_im,
100 gaiaGMag=metadata.getArray('G_MAGS')[idx],
101 gaiaId=metadata.getArray('GAIA_IDS')[idx],
102 position=position,
103 archive_element=archive_element,
104 annularFlux=metadata.getArray('ANNULAR_FLUXES')[idx])
105
106 def measureAndNormalize(self, annulus, statsControl=afwMath.StatisticsControl(),
107 statsFlag=afwMath.stringToStatisticsProperty("MEAN"),
108 badMaskPlanes=('BAD', 'SAT', 'NO_DATA')):
109 """Compute "annularFlux", the integrated flux within an annulus
110 around an object's center, and normalize it.
111
112 Since the center of bright stars are saturated and/or heavily affected
113 by ghosts, we measure their flux in an annulus with a large enough
114 inner radius to avoid the most severe ghosts and contain enough
115 non-saturated pixels.
116
117 Parameters
118 ----------
119 annulus : `lsst.afw.geom.spanSet.SpanSet`
120 SpanSet containing the annulus to use for normalization.
121 statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional
122 StatisticsControl to be used when computing flux over all pixels
123 within the annulus.
124 statsFlag : `lsst.afw.math.statistics.Property`, optional
125 statsFlag to be passed on to ``afwMath.makeStatistics`` to compute
126 annularFlux. Defaults to a simple MEAN.
127 badMaskPlanes : `collections.abc.Collection` [`str`]
128 Collection of mask planes to ignore when computing annularFlux.
129 """
130 stampSize = self.stamp_im.getDimensions()
131 # create image with the same pixel values within annulus, NO_DATA
132 # elsewhere
133 maskPlaneDict = self.stamp_im.mask.getMaskPlaneDict()
134 annulusImage = MaskedImageF(stampSize, planeDict=maskPlaneDict)
135 annulusMask = annulusImage.mask
136 annulusMask.array[:] = 2**maskPlaneDict['NO_DATA']
137 annulus.copyMaskedImage(self.stamp_im, annulusImage)
138 # set mask planes to be ignored
139 andMask = reduce(ior, (annulusMask.getPlaneBitMask(bm) for bm in badMaskPlanes))
140 statsControl.setAndMask(andMask)
141 # compute annularFlux
142 annulusStat = afwMath.makeStatistics(annulusImage, statsFlag, statsControl)
143 self.annularFlux = annulusStat.getValue()
144 if np.isnan(self.annularFlux):
145 raise RuntimeError("Annular flux computation failed, likely because no pixels were valid.")
146 # normalize stamps
147 self.stamp_im.image.array /= self.annularFlux
148 return None
149
150
152 """Collection of bright star stamps and associated metadata.
153
154 Parameters
155 ----------
156 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
157 Sequence of star stamps. Cannot contain both normalized and
158 unnormalized stamps.
159 innerRadius : `int`, optional
160 Inner radius value, in pixels. This and ``outerRadius`` define the
161 annulus used to compute the ``"annularFlux"`` values within each
162 ``starStamp``. Must be provided if ``normalize`` is True.
163 outerRadius : `int`, optional
164 Outer radius value, in pixels. This and ``innerRadius`` define the
165 annulus used to compute the ``"annularFlux"`` values within each
166 ``starStamp``. Must be provided if ``normalize`` is True.
167 nb90Rots : `int`, optional
168 Number of 90 degree rotations required to compensate for detector
169 orientation.
170 metadata : `lsst.daf.base.PropertyList`, optional
171 Metadata associated with the bright stars.
172 use_mask : `bool`
173 If `True` read and write mask data. Default `True`.
174 use_variance : `bool`
175 If ``True`` read and write variance data. Default ``False``.
176 use_archive : `bool`
177 If ``True`` read and write an Archive that contains a Persistable
178 associated with each stamp. In the case of bright stars, this is
179 usually a ``TransformPoint2ToPoint2``, used to warp each stamp
180 to the same pixel grid before stacking.
181
182 Raises
183 ------
184 ValueError
185 Raised if one of the star stamps provided does not contain the
186 required keys.
187 AttributeError
188 Raised if there is a mix-and-match of normalized and unnormalized
189 stamps, stamps normalized with different annulus definitions, or if
190 stamps are to be normalized but annular radii were not provided.
191
192
193 Notes
194 -----
195 A butler can be used to read only a part of the stamps, specified by a
196 bbox:
197
198 >>> starSubregions = butler.get("brightStarStamps", dataId, parameters={'bbox': bbox})
199 """
200
201 def __init__(self, starStamps, innerRadius=None, outerRadius=None, nb90Rots=None,
202 metadata=None, use_mask=True, use_variance=False, use_archive=False):
203 super().__init__(starStamps, metadata, use_mask, use_variance, use_archive)
204 # Ensure stamps contain a flux measurement if and only if they are
205 # already expected to be normalized
206 self._checkNormalization(False, innerRadius, outerRadius)
207 self._innerRadius, self._outerRadius = innerRadius, outerRadius
208 if innerRadius is not None and outerRadius is not None:
209 self.normalized = True
210 else:
211 self.normalized = False
212 self.nb90Rots = nb90Rots
213
214 @classmethod
215 def initAndNormalize(cls, starStamps, innerRadius, outerRadius, nb90Rots=None,
216 metadata=None, use_mask=True, use_variance=False,
217 use_archive=False, imCenter=None,
218 discardNanFluxObjects=True,
219 statsControl=afwMath.StatisticsControl(),
220 statsFlag=afwMath.stringToStatisticsProperty("MEAN"),
221 badMaskPlanes=('BAD', 'SAT', 'NO_DATA')):
222 """Normalize a set of bright star stamps and initialize a
223 BrightStarStamps instance.
224
225 Since the center of bright stars are saturated and/or heavily affected
226 by ghosts, we measure their flux in an annulus with a large enough
227 inner radius to avoid the most severe ghosts and contain enough
228 non-saturated pixels.
229
230 Parameters
231 ----------
232 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
233 Sequence of star stamps. Cannot contain both normalized and
234 unnormalized stamps.
235 innerRadius : `int`
236 Inner radius value, in pixels. This and ``outerRadius`` define the
237 annulus used to compute the ``"annularFlux"`` values within each
238 ``starStamp``.
239 outerRadius : `int`
240 Outer radius value, in pixels. This and ``innerRadius`` define the
241 annulus used to compute the ``"annularFlux"`` values within each
242 ``starStamp``.
243 nb90Rots : `int`, optional
244 Number of 90 degree rotations required to compensate for detector
245 orientation.
246 metadata : `lsst.daf.base.PropertyList`, optional
247 Metadata associated with the bright stars.
248 use_mask : `bool`
249 If `True` read and write mask data. Default `True`.
250 use_variance : `bool`
251 If ``True`` read and write variance data. Default ``False``.
252 use_archive : `bool`
253 If ``True`` read and write an Archive that contains a Persistable
254 associated with each stamp. In the case of bright stars, this is
255 usually a ``TransformPoint2ToPoint2``, used to warp each stamp
256 to the same pixel grid before stacking.
257 imCenter : `collections.abc.Sequence`, optional
258 Center of the object, in pixels. If not provided, the center of the
259 first stamp's pixel grid will be used.
260 discardNanFluxObjects : `bool`
261 Whether objects with NaN annular flux should be discarded.
262 If False, these objects will not be normalized.
263 statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional
264 StatisticsControl to be used when computing flux over all pixels
265 within the annulus.
266 statsFlag : `lsst.afw.math.statistics.Property`, optional
267 statsFlag to be passed on to ``afwMath.makeStatistics`` to compute
268 annularFlux. Defaults to a simple MEAN.
269 badMaskPlanes : `collections.abc.Collection` [`str`]
270 Collection of mask planes to ignore when computing annularFlux.
271
272 Raises
273 ------
274 ValueError
275 Raised if one of the star stamps provided does not contain the
276 required keys.
277 AttributeError
278 Raised if there is a mix-and-match of normalized and unnormalized
279 stamps, stamps normalized with different annulus definitions, or if
280 stamps are to be normalized but annular radii were not provided.
281 """
282 if imCenter is None:
283 stampSize = starStamps[0].stamp_im.getDimensions()
284 imCenter = stampSize[0]//2, stampSize[1]//2
285 # Create SpanSet of annulus
286 outerCircle = afwGeom.SpanSet.fromShape(outerRadius, afwGeom.Stencil.CIRCLE, offset=imCenter)
287 innerCircle = afwGeom.SpanSet.fromShape(innerRadius, afwGeom.Stencil.CIRCLE, offset=imCenter)
288 annulus = outerCircle.intersectNot(innerCircle)
289 # Initialize (unnormalized) brightStarStamps instance
290 bss = cls(starStamps, innerRadius=None, outerRadius=None, nb90Rots=nb90Rots,
291 metadata=metadata, use_mask=use_mask,
292 use_variance=use_variance, use_archive=use_archive)
293 # Ensure no stamps had already been normalized
294 bss._checkNormalization(True, innerRadius, outerRadius)
295 bss._innerRadius, bss._outerRadius = innerRadius, outerRadius
296 # Apply normalization
297 for j, stamp in enumerate(bss._stamps):
298 try:
299 stamp.measureAndNormalize(annulus, statsControl=statsControl, statsFlag=statsFlag,
300 badMaskPlanes=badMaskPlanes)
301 except RuntimeError:
302 # Optionally keep NaN flux objects, for bookkeeping purposes,
303 # and to avoid having to re-find and redo the preprocessing
304 # steps needed before bright stars can be subtracted.
305 if discardNanFluxObjects:
306 bss._stamps.pop(j)
307 else:
308 stamp.annularFlux = np.nan
309 bss.normalized = True
310 return bss
311
312 def _refresh_metadata(self):
313 """Refresh the metadata. Should be called before writing this object
314 out.
315 """
316 # add full list of positions, Gaia magnitudes, IDs and annularFlxes to
317 # shared metadata
318 self._metadata["G_MAGS"] = self.getMagnitudes()
319 self._metadata["GAIA_IDS"] = self.getGaiaIds()
320 positions = self.getPositions()
321 self._metadata["X0S"] = [xy0[0] for xy0 in positions]
322 self._metadata["Y0S"] = [xy0[1] for xy0 in positions]
323 self._metadata["ANNULAR_FLUXES"] = self.getAnnularFluxes()
324 self._metadata["NORMALIZED"] = self.normalized
325 self._metadata["INNER_RADIUS"] = self._innerRadius
326 self._metadata["OUTER_RADIUS"] = self._outerRadius
327 if self.nb90Rots is not None:
328 self._metadata["NB_90_ROTS"] = self.nb90Rots
329 return None
330
331 @classmethod
332 def readFits(cls, filename):
333 """Build an instance of this class from a file.
334
335 Parameters
336 ----------
337 filename : `str`
338 Name of the file to read
339 """
341
342 @classmethod
343 def readFitsWithOptions(cls, filename, options):
344 """Build an instance of this class with options.
345
346 Parameters
347 ----------
348 filename : `str`
349 Name of the file to read
350 options : `PropertyList`
351 Collection of metadata parameters
352 """
353 stamps, metadata = readFitsWithOptions(filename, BrightStarStamp.factory, options)
354 nb90Rots = metadata["NB_90_ROTS"] if "NB_90_ROTS" in metadata else None
355 if metadata["NORMALIZED"]:
356 return cls(stamps,
357 innerRadius=metadata["INNER_RADIUS"], outerRadius=metadata["OUTER_RADIUS"],
358 nb90Rots=nb90Rots, metadata=metadata, use_mask=metadata['HAS_MASK'],
359 use_variance=metadata['HAS_VARIANCE'], use_archive=metadata['HAS_ARCHIVE'])
360 else:
361 return cls(stamps, nb90Rots=nb90Rots, metadata=metadata, use_mask=metadata['HAS_MASK'],
362 use_variance=metadata['HAS_VARIANCE'], use_archive=metadata['HAS_ARCHIVE'])
363
364 def append(self, item, innerRadius=None, outerRadius=None):
365 """Add an additional bright star stamp.
366
367 Parameters
368 ----------
369 item : `BrightStarStamp`
370 Bright star stamp to append.
371 innerRadius : `int`, optional
372 Inner radius value, in pixels. This and ``outerRadius`` define the
373 annulus used to compute the ``"annularFlux"`` values within each
374 ``BrightStarStamp``.
375 outerRadius : `int`, optional
376 Outer radius value, in pixels. This and ``innerRadius`` define the
377 annulus used to compute the ``"annularFlux"`` values within each
378 ``BrightStarStamp``.
379 """
380 if not isinstance(item, BrightStarStamp):
381 raise ValueError(f"Can only add instances of BrightStarStamp, got {type(item)}.")
382 if (item.annularFlux is None) == self.normalized:
383 raise AttributeError("Trying to append an unnormalized stamp to a normalized BrightStarStamps "
384 "instance, or vice-versa.")
385 else:
386 self._checkRadius(innerRadius, outerRadius)
387 self._stamps.append(item)
388 return None
389
390 def extend(self, bss):
391 """Extend BrightStarStamps instance by appending elements from another
392 instance.
393
394 Parameters
395 ----------
396 bss : `BrightStarStamps`
397 Other instance to concatenate.
398 """
399 if not isinstance(bss, BrightStarStamps):
400 raise ValueError('Can only extend with a BrightStarStamps object. '
401 f'Got {type(bss)}.')
402 self._checkRadius(bss._innerRadius, bss._outerRadius)
403 self._stamps += bss._stamps
404
405 def getMagnitudes(self):
406 """Retrieve Gaia G magnitudes for each star.
407
408 Returns
409 -------
410 gaiaGMags : `list` [`float`]
411 """
412 return [stamp.gaiaGMag for stamp in self._stamps]
413
414 def getGaiaIds(self):
415 """Retrieve Gaia IDs for each star.
416
417 Returns
418 -------
419 gaiaIds : `list` [`int`]
420 """
421 return [stamp.gaiaId for stamp in self._stamps]
422
424 """Retrieve normalization factors for each star.
425
426 These are computed by integrating the flux in annulus centered on the
427 bright star, far enough from center to be beyond most severe ghosts and
428 saturation. The inner and outer radii that define the annulus can be
429 recovered from the metadata.
430
431 Returns
432 -------
433 annularFluxes : `list` [`float`]
434 """
435 return [stamp.annularFlux for stamp in self._stamps]
436
437 def selectByMag(self, magMin=None, magMax=None):
438 """Return the subset of bright star stamps for objects with specified
439 magnitude cuts (in Gaia G).
440
441 Parameters
442 ----------
443 magMin : `float`, optional
444 Keep only stars fainter than this value.
445 magMax : `float`, optional
446 Keep only stars brighter than this value.
447 """
448 subset = [stamp for stamp in self._stamps
449 if (magMin is None or stamp.gaiaGMag > magMin)
450 and (magMax is None or stamp.gaiaGMag < magMax)]
451 # This is an optimization to save looping over the init argument when
452 # it is already guaranteed to be the correct type
453 instance = BrightStarStamps((),
454 innerRadius=self._innerRadius, outerRadius=self._outerRadius,
455 metadata=self._metadata)
456 instance._stamps = subset
457 return instance
458
459 def _checkRadius(self, innerRadius, outerRadius):
460 """Ensure provided annulus radius is consistent with that already
461 present in the instance, or with arguments passed on at initialization.
462 """
463 if innerRadius != self._innerRadius or outerRadius != self._outerRadius:
464 raise AttributeError("Trying to mix stamps normalized with annulus radii "
465 f"{innerRadius, outerRadius} with those of BrightStarStamp instance\n"
466 f"(computed with annular radii {self._innerRadius, self._outerRadius}).")
467
468 def _checkNormalization(self, normalize, innerRadius, outerRadius):
469 """Ensure there is no mixing of normalized and unnormalized stars, and
470 that, if requested, normalization can be performed.
471 """
472 noneFluxCount = self.getAnnularFluxes().count(None)
473 nStamps = len(self)
474 nFluxVals = nStamps - noneFluxCount
475 if noneFluxCount and noneFluxCount < nStamps:
476 # at least one stamp contains an annularFlux value (i.e. has been
477 # normalized), but not all of them do
478 raise AttributeError(f"Only {nFluxVals} stamps contain an annularFlux value.\nAll stamps in a "
479 "BrightStarStamps instance must either be normalized with the same annulus "
480 "definition, or none of them can contain an annularFlux value.")
481 elif normalize:
482 # stamps are to be normalized; ensure annular radii are specified
483 # and they have no annularFlux
484 if innerRadius is None or outerRadius is None:
485 raise AttributeError("For stamps to be normalized (normalize=True), please provide a valid "
486 "value (in pixels) for both innerRadius and outerRadius.")
487 elif noneFluxCount < nStamps:
488 raise AttributeError(f"{nFluxVals} stamps already contain an annularFlux value. For stamps to"
489 " be normalized, all their annularFlux must be None.")
490 elif innerRadius is not None and outerRadius is not None:
491 # Radii provided, but normalize=False; check that stamps
492 # already contain annularFluxes
493 if noneFluxCount:
494 raise AttributeError(f"{noneFluxCount} stamps contain no annularFlux, but annular radius "
495 "values were provided and normalize=False.\nTo normalize stamps, set "
496 "normalize to True.")
497 else:
498 # At least one radius value is missing; ensure no stamps have
499 # already been normalized
500 if nFluxVals:
501 raise AttributeError(f"{nFluxVals} stamps contain an annularFlux value. If stamps have "
502 "been normalized, the innerRadius and outerRadius values used must "
503 "be provided.")
504 return None
A class to manipulate images, masks, and variance as a single object.
Definition: MaskedImage.h:74
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
def factory(cls, stamp_im, metadata, idx, archive_element=None)
def measureAndNormalize(self, annulus, statsControl=afwMath.StatisticsControl(), statsFlag=afwMath.stringToStatisticsProperty("MEAN"), badMaskPlanes=('BAD', 'SAT', 'NO_DATA'))
def __init__(self, starStamps, innerRadius=None, outerRadius=None, nb90Rots=None, metadata=None, use_mask=True, use_variance=False, use_archive=False)
def _checkNormalization(self, normalize, innerRadius, outerRadius)
def append(self, item, innerRadius=None, outerRadius=None)
def initAndNormalize(cls, starStamps, innerRadius, outerRadius, nb90Rots=None, metadata=None, use_mask=True, use_variance=False, use_archive=False, imCenter=None, discardNanFluxObjects=True, statsControl=afwMath.StatisticsControl(), statsFlag=afwMath.stringToStatisticsProperty("MEAN"), badMaskPlanes=('BAD', 'SAT', 'NO_DATA'))
def readFitsWithOptions(cls, filename, options)
Definition: stamps.py:325
def readFitsWithOptions(cls, filename, options)
Definition: stamps.py:462
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:361
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
Definition: Statistics.cc:762