Coverage for python/lsst/meas/algorithms/brightStarStamps.py : 25%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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"""
25__all__ = ["BrightStarStamp", "BrightStarStamps"]
27import collections.abc
28from typing import NamedTuple
29from enum import Enum, auto
31import lsst.afw.image as afwImage
32import lsst.afw.fits as afwFits
33from lsst.geom import Box2I, Point2I, Extent2I
34from lsst.daf.base import PropertySet
37class RadiiEnum(Enum):
38 INNER_RADIUS = auto()
39 OUTER_RADIUS = auto()
41 def __str__(self):
42 return self.name
45class BrightStarStamp(NamedTuple):
46 """Single stamp centered on a bright star, normalized by its
47 annularFlux.
48 """
49 starStamp: afwImage.maskedImage.MaskedImageF
50 gaiaGMag: float
51 gaiaId: int
52 annularFlux: float
55class BrightStarStamps(collections.abc.Sequence):
56 """Collection of bright star stamps and associated metadata.
58 Parameters
59 ----------
60 starStamps : `collections.abc.Sequence` [`BrightStarStamp`]
61 Sequence of star stamps.
62 innerRadius : `int`, optional
63 Inner radius value, in pixels. This and ``outerRadius`` define the
64 annulus used to compute the ``"annularFlux"`` values within each
65 ``starStamp``. Must be provided if ``"INNER_RADIUS"`` and
66 ``"OUTER_RADIUS"`` are not present in ``metadata``.
67 outerRadius : `int`, optional
68 Outer radius value, in pixels. This and ``innerRadius`` define the
69 annulus used to compute the ``"annularFlux"`` values within each
70 ``starStamp``. Must be provided if ``"INNER_RADIUS"`` and
71 ``"OUTER_RADIUS"`` are not present in ``metadata``.
72 metadata : `lsst.daf.base.PropertyList`, optional
73 Metadata associated with the bright stars.
75 Raises
76 ------
77 ValueError
78 Raised if one of the star stamps provided does not contain the
79 required keys.
80 AttributeError
81 Raised if the definition of the annulus used to compute each star's
82 normalization factor are not provided, that is, if ``"INNER_RADIUS"``
83 and ``"OUTER_RADIUS"`` are not present in ``metadata`` _and_
84 ``innerRadius`` and ``outerRadius`` are not provided.
86 Notes
87 -----
88 A (gen2) butler can be used to read only a part of the stamps,
89 specified by a bbox:
91 >>> starSubregions = butler.get("brightStarStamps_sub", dataId, bbox=bbox)
92 """
94 def __init__(self, starStamps, innerRadius=None, outerRadius=None,
95 metadata=None,):
96 for item in starStamps:
97 if not isinstance(item, BrightStarStamp):
98 raise ValueError(f"Can only add instances of BrightStarStamp, got {type(item)}")
99 self._starStamps = starStamps
100 self._metadata = PropertySet() if metadata is None else metadata.deepCopy()
101 # Add inner and outer radii to metadata
102 self._checkRadius(innerRadius, RadiiEnum.INNER_RADIUS)
103 self._innerRadius = innerRadius
104 self._checkRadius(outerRadius, RadiiEnum.OUTER_RADIUS)
105 self._outerRadius = outerRadius
107 def __len__(self):
108 return len(self._starStamps)
110 def __getitem__(self, index):
111 return self._starStamps[index]
113 def __iter__(self):
114 return iter(self._starStamps)
116 def append(self, item, innerRadius, outerRadius):
117 """Add an additional bright star stamp.
119 Parameters
120 ----------
121 item : `BrightStarStamp`
122 Bright star stamp to append.
123 innerRadius : `int`
124 Inner radius value, in4 pixels. This and ``outerRadius`` define the
125 annulus used to compute the ``"annularFlux"`` values within each
126 ``starStamp``.
127 outerRadius : `int`, optional
128 Outer radius value, in pixels. This and ``innerRadius`` define the
129 annulus used to compute the ``"annularFlux"`` values within each
130 ``starStamp``.
131 """
132 if not isinstance(item, BrightStarStamp):
133 raise ValueError(f"Can only add instances of BrightStarStamp, got {type(item)}.")
134 self._checkRadius(innerRadius, RadiiEnum.INNER_RADIUS)
135 self._checkRadius(outerRadius, RadiiEnum.OUTER_RADIUS)
136 self._starStamps.append(item)
137 return None
139 def extend(self, bss):
140 """Extend BrightStarStamps instance by appending elements from another
141 instance.
143 Parameters
144 ----------
145 bss : `BrightStarStamps`
146 Other instance to concatenate.
147 """
148 self._checkRadius(bss._innerRadius, RadiiEnum.INNER_RADIUS)
149 self._checkRadius(bss._outerRadius, RadiiEnum.OUTER_RADIUS)
150 self._starStamps += bss._starStamps
152 def getMaskedImages(self):
153 """Retrieve star images.
155 Returns
156 -------
157 maskedImages :
158 `list` [`lsst.afw.image.maskedImage.maskedImage.MaskedImageF`]
159 """
160 return [stamp.starStamp for stamp in self._starStamps]
162 def getMagnitudes(self):
163 """Retrieve Gaia G magnitudes for each star.
165 Returns
166 -------
167 gaiaGMags : `list` [`float`]
168 """
169 return [stamp.gaiaGMag for stamp in self._starStamps]
171 def getGaiaIds(self):
172 """Retrieve Gaia IDs for each star.
174 Returns
175 -------
176 gaiaIds : `list` [`int`]
177 """
178 return [stamp.gaiaId for stamp in self._starStamps]
180 def getAnnularFluxes(self):
181 """Retrieve normalization factors for each star.
183 These are computed by integrating the flux in annulus centered on the
184 bright star, far enough from center to be beyond most severe ghosts and
185 saturation. The inner and outer radii that define the annulus can be
186 recovered from the metadata.
188 Returns
189 -------
190 annularFluxes : list[`float`]
191 """
192 return [stamp.annularFlux for stamp in self._starStamps]
194 def selectByMag(self, magMin=None, magMax=None):
195 """Return the subset of bright star stamps for objects with specified
196 magnitude cuts (in Gaia G).
198 Parameters
199 ----------
200 magMin : `float`, optional
201 Keep only stars fainter than this value.
202 magMax : `float`, optional
203 Keep only stars brighter than this value.
204 """
205 subset = [stamp for stamp in self._starStamps
206 if (magMin is None or stamp.gaiaGMag > magMin)
207 and (magMax is None or stamp.gaiaGMag < magMax)]
208 # This is an optimization to save looping over the init argument when
209 # it is already guaranteed to be the correct type
210 instance = BrightStarStamps((), metadata=self._metadata)
211 instance._starStamps = subset
212 return instance
214 @property
215 def metadata(self):
216 return self._metadata.deepCopy()
218 def _checkRadius(self, radiusValue, metadataEnum):
219 """Ensure provided annulus radius is consistent with that present
220 in metadata. If metadata does not contain annulus radius, add it.
221 """
222 # if a radius value is already present in metadata, ensure it matches
223 # the one given
224 metadataName = str(metadataEnum)
225 if self._metadata.exists(metadataName):
226 if radiusValue is not None:
227 if self._metadata[metadataName] != radiusValue:
228 raise AttributeError("BrightStarStamps instance already contains different annulus radii "
229 + f"values ({metadataName}).")
230 # if not already in metadata, a value must be provided
231 elif radiusValue is None:
232 raise AttributeError("No radius value provided for the AnnularFlux measurement "
233 + f"({metadataName}), and none present in metadata.")
234 else:
235 self._metadata[metadataName] = radiusValue
236 return None
238 def writeFits(self, filename):
239 """Write a single FITS file containing all bright star stamps.
240 """
241 # ensure metadata contains current number of objects
242 self._metadata["N_STARS"] = len(self)
244 # add full list of Gaia magnitudes, IDs and annularFlxes to shared
245 # metadata
246 self._metadata["G_MAGS"] = self.getMagnitudes()
247 self._metadata["GAIA_IDS"] = self.getGaiaIds()
248 self._metadata["ANNULAR_FLUXES"] = self.getAnnularFluxes()
250 # create primary HDU with global metadata
251 fitsPrimary = afwFits.Fits(filename, "w")
252 fitsPrimary.createEmpty()
253 fitsPrimary.writeMetadata(self._metadata)
254 fitsPrimary.closeFile()
256 # add all stamps and mask planes
257 for stamp in self.getMaskedImages():
258 stamp.getImage().writeFits(filename, mode='a')
259 stamp.getMask().writeFits(filename, mode='a')
260 return None
262 @classmethod
263 def readFits(cls, filename):
264 """Read bright star stamps from FITS file.
266 Returns
267 -------
268 bss : `BrightStarStamps`
269 Collection of bright star stamps.
270 """
271 bss = cls.readFitsWithOptions(filename, None)
272 return bss
274 @classmethod
275 def readFitsWithOptions(cls, filename, options):
276 """Read bright star stamps from FITS file, allowing for only a
277 subregion of the stamps to be read.
279 Returns
280 -------
281 bss : `BrightStarStamps`
282 Collection of bright star stamps.
283 """
284 # extract necessary info from metadata
285 visitMetadata = afwFits.readMetadata(filename, hdu=0)
286 nbStarStamps = visitMetadata["N_STARS"]
287 gaiaGMags = visitMetadata.getArray("G_MAGS")
288 gaiaIds = visitMetadata.getArray("GAIA_IDS")
289 annularFluxes = visitMetadata.getArray("ANNULAR_FLUXES")
290 # check if a bbox was provided
291 kwargs = {}
292 if options and options.exists("llcX"):
293 llcX = options["llcX"]
294 llcY = options["llcY"]
295 width = options["width"]
296 height = options["height"]
297 bbox = Box2I(Point2I(llcX, llcY), Extent2I(width, height))
298 kwargs["bbox"] = bbox
299 # read stamps themselves
300 starStamps = []
301 for bStarIdx in range(nbStarStamps):
302 imReader = afwImage.ImageFitsReader(filename, hdu=2*bStarIdx + 1)
303 maskReader = afwImage.MaskFitsReader(filename, hdu=2*(bStarIdx + 1))
304 maskedImage = afwImage.MaskedImageF(image=imReader.read(**kwargs),
305 mask=maskReader.read(**kwargs))
306 starStamps.append(BrightStarStamp(starStamp=maskedImage,
307 gaiaGMag=gaiaGMags[bStarIdx],
308 gaiaId=gaiaIds[bStarIdx],
309 annularFlux=annularFluxes[bStarIdx]))
310 bss = cls(starStamps, metadata=visitMetadata)
311 return bss