22"""Collection of small images (stamps), each centered on a bright star."""
24__all__ = [
"BrightStarStamp",
"BrightStarStamps"]
26from collections.abc
import Collection
27from dataclasses
import dataclass
28from functools
import reduce
29from operator
import ior
34from lsst.afw.math import Property, StatisticsControl, makeStatistics, stringToStatisticsProperty
38from .stamps
import AbstractStamp, Stamps, readFitsWithOptions
43 """Single stamp centered on a bright star, normalized by its annularFlux.
48 Pixel data for this postage stamp
50 Gaia G magnitude
for the object
in this stamp
52 Gaia object identifier
54 Origin of the stamps
in its origin exposure (pixels)
56 Archive element (e.g. Transform
or WCS) associated
with this stamp.
57 annularFlux : `float`
or None, optional
58 Flux
in an annulus around the object
61 stamp_im: MaskedImageF
65 archive_element: Persistable | None =
None
66 annularFlux: float |
None =
None
69 def factory(cls, stamp_im, metadata, idx, archive_element=None):
70 """This method is needed to service the FITS reader. We need a standard
71 interface to construct objects like this. Parameters needed to
72 construct this object are passed in via a metadata dictionary
and then
73 passed to the constructor of this
class. This particular factory
74 method requires keys: G_MAGS, GAIA_IDS,
and ANNULAR_FLUXES. They should
75 each point to lists of values.
80 Pixel data to
pass to the constructor
82 Dictionary containing the information
83 needed by the constructor.
85 Index into the lists
in ``metadata``
87 Archive element (e.g. Transform
or WCS) associated
with this stamp.
91 brightstarstamp : `BrightStarStamp`
92 An instance of this
class
94 if "X0S" in metadata
and "Y0S" in metadata:
95 x0 = metadata.getArray(
"X0S")[idx]
96 y0 = metadata.getArray(
"Y0S")[idx]
97 position = Point2I(x0, y0)
102 gaiaGMag=metadata.getArray(
"G_MAGS")[idx],
103 gaiaId=metadata.getArray(
"GAIA_IDS")[idx],
105 archive_element=archive_element,
106 annularFlux=metadata.getArray(
"ANNULAR_FLUXES")[idx],
113 statsFlag: Property = stringToStatisticsProperty(
"MEAN"),
114 badMaskPlanes: Collection[str] = (
"BAD",
"SAT",
"NO_DATA"),
116 """Compute "annularFlux", the integrated flux within an annulus
117 around an object's center, and normalize it.
119 Since the center of bright stars are saturated and/
or heavily affected
120 by ghosts, we measure their flux
in an annulus
with a large enough
121 inner radius to avoid the most severe ghosts
and contain enough
122 non-saturated pixels.
126 annulus : `~lsst.afw.geom.spanSet.SpanSet`
127 SpanSet containing the annulus to use
for normalization.
128 statsControl : `~lsst.afw.math.statistics.StatisticsControl`, optional
129 StatisticsControl to be used when computing flux over all pixels
131 statsFlag : `~lsst.afw.math.statistics.Property`, optional
132 statsFlag to be passed on to ``afwMath.makeStatistics`` to compute
133 annularFlux. Defaults to a simple MEAN.
134 badMaskPlanes : `collections.abc.Collection` [`str`]
135 Collection of mask planes to ignore when computing annularFlux.
137 stampSize = self.stamp_im.getDimensions()
139 maskPlaneDict = self.stamp_im.mask.getMaskPlaneDict()
140 annulusImage = MaskedImageF(stampSize, planeDict=maskPlaneDict)
141 annulusMask = annulusImage.mask
142 annulusMask.array[:] = 2 ** maskPlaneDict[
"NO_DATA"]
143 annulus.copyMaskedImage(self.stamp_im, annulusImage)
145 andMask = reduce(ior, (annulusMask.getPlaneBitMask(bm)
for bm
in badMaskPlanes))
146 statsControl.setAndMask(andMask)
148 annulusStat = makeStatistics(annulusImage, statsFlag, statsControl)
151 raise RuntimeError(
"Annular flux computation failed, likely because no pixels were valid.")
158 """Collection of bright star stamps and associated metadata.
162 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
163 Sequence of star stamps. Cannot contain both normalized and
165 innerRadius : `int`, optional
166 Inner radius value,
in pixels. This
and ``outerRadius`` define the
167 annulus used to compute the ``
"annularFlux"`` values within each
168 ``starStamp``. Must be provided
if ``normalize``
is True.
169 outerRadius : `int`, optional
170 Outer radius value,
in pixels. This
and ``innerRadius`` define the
171 annulus used to compute the ``
"annularFlux"`` values within each
172 ``starStamp``. Must be provided
if ``normalize``
is True.
173 nb90Rots : `int`, optional
174 Number of 90 degree rotations required to compensate
for detector
177 Metadata associated
with the bright stars.
179 If `
True` read
and write mask data. Default `
True`.
180 use_variance : `bool`
181 If ``
True`` read
and write variance data. Default ``
False``.
183 If ``
True`` read
and write an Archive that contains a Persistable
184 associated
with each stamp. In the case of bright stars, this
is
185 usually a ``TransformPoint2ToPoint2``, used to warp each stamp
186 to the same pixel grid before stacking.
191 Raised
if one of the star stamps provided does
not contain the
194 Raised
if there
is a mix-
and-match of normalized
and unnormalized
195 stamps, stamps normalized
with different annulus definitions,
or if
196 stamps are to be normalized but annular radii were
not provided.
200 A butler can be used to read only a part of the stamps, specified by a
203 >>> starSubregions = butler.get(
206 parameters={
"bbox": bbox}
221 super().
__init__(starStamps, metadata, use_mask, use_variance, use_archive)
225 self._innerRadius, self.
_outerRadius = innerRadius, outerRadius
226 if innerRadius
is not None and outerRadius
is not None:
244 discardNanFluxObjects=True,
245 statsControl=StatisticsControl(),
246 statsFlag=stringToStatisticsProperty(
"MEAN"),
247 badMaskPlanes=(
"BAD",
"SAT",
"NO_DATA"),
249 """Normalize a set of bright star stamps and initialize a
250 BrightStarStamps instance.
252 Since the center of bright stars are saturated and/
or heavily affected
253 by ghosts, we measure their flux
in an annulus
with a large enough
254 inner radius to avoid the most severe ghosts
and contain enough
255 non-saturated pixels.
259 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
260 Sequence of star stamps. Cannot contain both normalized
and
263 Inner radius value,
in pixels. This
and ``outerRadius`` define the
264 annulus used to compute the ``
"annularFlux"`` values within each
267 Outer radius value,
in pixels. This
and ``innerRadius`` define the
268 annulus used to compute the ``
"annularFlux"`` values within each
270 nb90Rots : `int`, optional
271 Number of 90 degree rotations required to compensate
for detector
274 Metadata associated
with the bright stars.
276 If `
True` read
and write mask data. Default `
True`.
277 use_variance : `bool`
278 If ``
True`` read
and write variance data. Default ``
False``.
280 If ``
True`` read
and write an Archive that contains a Persistable
281 associated
with each stamp. In the case of bright stars, this
is
282 usually a ``TransformPoint2ToPoint2``, used to warp each stamp
283 to the same pixel grid before stacking.
284 imCenter : `collections.abc.Sequence`, optional
285 Center of the object,
in pixels. If
not provided, the center of the
286 first stamp
's pixel grid will be used.
287 discardNanFluxObjects : `bool`
288 Whether objects with NaN annular flux should be discarded.
289 If
False, these objects will
not be normalized.
290 statsControl : `~lsst.afw.math.statistics.StatisticsControl`, optional
291 StatisticsControl to be used when computing flux over all pixels
293 statsFlag : `~lsst.afw.math.statistics.Property`, optional
294 statsFlag to be passed on to ``~lsst.afw.math.makeStatistics`` to
295 compute annularFlux. Defaults to a simple MEAN.
296 badMaskPlanes : `collections.abc.Collection` [`str`]
297 Collection of mask planes to ignore when computing annularFlux.
302 Raised
if one of the star stamps provided does
not contain the
305 Raised
if there
is a mix-
and-match of normalized
and unnormalized
306 stamps, stamps normalized
with different annulus definitions,
or if
307 stamps are to be normalized but annular radii were
not provided.
310 stampSize = starStamps[0].stamp_im.getDimensions()
311 imCenter = stampSize[0] // 2, stampSize[1] // 2
313 outerCircle = SpanSet.fromShape(outerRadius, Stencil.CIRCLE, offset=imCenter)
314 innerCircle = SpanSet.fromShape(innerRadius, Stencil.CIRCLE, offset=imCenter)
315 annulus = outerCircle.intersectNot(innerCircle)
324 use_variance=use_variance,
325 use_archive=use_archive,
328 bss._checkNormalization(
True, innerRadius, outerRadius)
329 bss._innerRadius, bss._outerRadius = innerRadius, outerRadius
331 for j, stamp
in enumerate(bss._stamps):
333 stamp.measureAndNormalize(
334 annulus, statsControl=statsControl, statsFlag=statsFlag, badMaskPlanes=badMaskPlanes
340 if discardNanFluxObjects:
343 stamp.annularFlux = np.nan
344 bss.normalized =
True
347 def _refresh_metadata(self):
348 """Refresh metadata. Should be called before writing the object out."""
354 self.
_metadata[
"X0S"] = [xy0[0]
for xy0
in positions]
355 self.
_metadata[
"Y0S"] = [xy0[1]
for xy0
in positions]
358 self.
_metadata[
"INNER_RADIUS"] = self._innerRadius
366 """Build an instance of this class from a file.
371 Name of the file to read
376 def readFitsWithOptions(cls, filename, options):
377 """Build an instance of this class with options.
382 Name of the file to read
383 options : `PropertyList`
384 Collection of metadata parameters
386 stamps, metadata = readFitsWithOptions(filename, BrightStarStamp.factory, options)
387 nb90Rots = metadata["NB_90_ROTS"]
if "NB_90_ROTS" in metadata
else None
388 if metadata[
"NORMALIZED"]:
391 innerRadius=metadata[
"INNER_RADIUS"],
392 outerRadius=metadata[
"OUTER_RADIUS"],
395 use_mask=metadata[
"HAS_MASK"],
396 use_variance=metadata[
"HAS_VARIANCE"],
397 use_archive=metadata[
"HAS_ARCHIVE"],
404 use_mask=metadata[
"HAS_MASK"],
405 use_variance=metadata[
"HAS_VARIANCE"],
406 use_archive=metadata[
"HAS_ARCHIVE"],
409 def append(self, item, innerRadius=None, outerRadius=None):
410 """Add an additional bright star stamp.
414 item : `BrightStarStamp`
415 Bright star stamp to append.
416 innerRadius : `int`, optional
417 Inner radius value, in pixels. This
and ``outerRadius`` define the
418 annulus used to compute the ``
"annularFlux"`` values within each
420 outerRadius : `int`, optional
421 Outer radius value,
in pixels. This
and ``innerRadius`` define the
422 annulus used to compute the ``
"annularFlux"`` values within each
425 if not isinstance(item, BrightStarStamp):
426 raise ValueError(f
"Can only add instances of BrightStarStamp, got {type(item)}.")
427 if (item.annularFlux
is None) == self.
normalized:
428 raise AttributeError(
429 "Trying to append an unnormalized stamp to a normalized BrightStarStamps "
430 "instance, or vice-versa."
438 """Extend BrightStarStamps instance by appending elements from another
443 bss : `BrightStarStamps`
444 Other instance to concatenate.
446 if not isinstance(bss, BrightStarStamps):
447 raise ValueError(f
"Can only extend with a BrightStarStamps object. Got {type(bss)}.")
452 """Retrieve Gaia G magnitudes for each star.
456 gaiaGMags : `list` [`float`]
458 return [stamp.gaiaGMag
for stamp
in self.
_stamps]
461 """Retrieve Gaia IDs for each star.
465 gaiaIds : `list` [`int`]
467 return [stamp.gaiaId
for stamp
in self.
_stamps]
470 """Retrieve normalization factors for each star.
472 These are computed by integrating the flux in annulus centered on the
473 bright star, far enough
from center to be beyond most severe ghosts
and
474 saturation. The inner
and outer radii that define the annulus can be
475 recovered
from the metadata.
479 annularFluxes : `list` [`float`]
481 return [stamp.annularFlux
for stamp
in self.
_stamps]
484 """Return the subset of bright star stamps for objects with specified
485 magnitude cuts (in Gaia G).
489 magMin : `float`, optional
490 Keep only stars fainter than this value.
491 magMax : `float`, optional
492 Keep only stars brighter than this value.
497 if (magMin
is None or stamp.gaiaGMag > magMin)
and (magMax
is None or stamp.gaiaGMag < magMax)
504 instance._stamps = subset
507 def _checkRadius(self, innerRadius, outerRadius):
508 """Ensure provided annulus radius is consistent with that already
509 present in the instance,
or with arguments passed on at initialization.
511 if innerRadius != self._innerRadius
or outerRadius != self.
_outerRadius:
512 raise AttributeError(
513 "Trying to mix stamps normalized with annulus radii "
514 f
"{innerRadius, outerRadius} with those of BrightStarStamp instance\n"
515 f
"(computed with annular radii {self._innerRadius, self._outerRadius})."
518 def _checkNormalization(self, normalize, innerRadius, outerRadius):
519 """Ensure there is no mixing of normalized and unnormalized stars, and
520 that, if requested, normalization can be performed.
524 nFluxVals = nStamps - noneFluxCount
525 if noneFluxCount
and noneFluxCount < nStamps:
528 raise AttributeError(
529 f
"Only {nFluxVals} stamps contain an annularFlux value.\nAll stamps in a "
530 "BrightStarStamps instance must either be normalized with the same annulus "
531 "definition, or none of them can contain an annularFlux value."
536 if innerRadius
is None or outerRadius
is None:
537 raise AttributeError(
538 "For stamps to be normalized (normalize=True), please provide a valid "
539 "value (in pixels) for both innerRadius and outerRadius."
541 elif noneFluxCount < nStamps:
542 raise AttributeError(
543 f
"{nFluxVals} stamps already contain an annularFlux value. For stamps to "
544 "be normalized, all their annularFlux must be None."
546 elif innerRadius
is not None and outerRadius
is not None:
550 raise AttributeError(
551 f
"{noneFluxCount} stamps contain no annularFlux, but annular radius "
552 "values were provided and normalize=False.\nTo normalize stamps, set "
559 raise AttributeError(
560 f
"{nFluxVals} stamps contain an annularFlux value. If stamps have "
561 "been normalized, the innerRadius and outerRadius values used must "
def factory(cls, stamp_im, metadata, idx, archive_element=None)
def measureAndNormalize(self, SpanSet annulus, StatisticsControl statsControl=StatisticsControl(), Property statsFlag=stringToStatisticsProperty("MEAN"), Collection[str] 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 readFits(cls, filename)
def append(self, item, innerRadius=None, outerRadius=None)
def selectByMag(self, magMin=None, magMax=None)
def getAnnularFluxes(self)
def readFitsWithOptions(cls, filename, options)
def initAndNormalize(cls, starStamps, innerRadius, outerRadius, nb90Rots=None, metadata=None, use_mask=True, use_variance=False, use_archive=False, imCenter=None, discardNanFluxObjects=True, statsControl=StatisticsControl(), statsFlag=stringToStatisticsProperty("MEAN"), badMaskPlanes=("BAD", "SAT", "NO_DATA"))
def _checkRadius(self, innerRadius, outerRadius)
def readFitsWithOptions(cls, filename, options)
def readFitsWithOptions(cls, filename, options)