LSST Applications g00d0e8bbd7+8c5ae1fdc5,g013ef56533+603670b062,g083dd6704c+2e189452a7,g199a45376c+0ba108daf9,g1c5cce2383+bc9f6103a4,g1fd858c14a+cd69ed4fc1,g210f2d0738+c4742f2e9e,g262e1987ae+612fa42d85,g29ae962dfc+83d129e820,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+5eaa884f2a,g47891489e3+e32160a944,g53246c7159+8c5ae1fdc5,g5b326b94bb+dcc56af22d,g64539dfbff+c4742f2e9e,g67b6fd64d1+e32160a944,g74acd417e5+c122e1277d,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g88cb488625+47d24e4084,g89139ef638+e32160a944,g8d7436a09f+d14b4ff40a,g8ea07a8fe4+b212507b11,g90f42f885a+e1755607f3,g97be763408+34be90ab8c,g98df359435+ec1fa61bf1,ga2180abaac+8c5ae1fdc5,ga9e74d7ce9+43ac651df0,gbf99507273+8c5ae1fdc5,gc2a301910b+c4742f2e9e,gca7fc764a6+e32160a944,gd7ef33dd92+e32160a944,gdab6d2f7ff+c122e1277d,gdb1e2cdc75+1b18322db8,ge410e46f29+e32160a944,ge41e95a9f2+c4742f2e9e,geaed405ab2+0d91c11c6d,w.2025.44
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 (postage stamps) centered on bright stars."""
23
24from __future__ import annotations
25
26__all__ = ["BrightStarStamp", "BrightStarStamps"]
27
28from collections.abc import Sequence
29from dataclasses import dataclass
30
31import numpy as np
32
33from lsst.afw.detection import Psf
34from lsst.afw.fits import Fits, readMetadata
35from lsst.afw.geom import SkyWcs
36from lsst.afw.image import ImageFitsReader, MaskedImageF, MaskFitsReader
37from lsst.afw.table.io import InputArchive, OutputArchive
38from lsst.daf.base import PropertyList
39from lsst.geom import Angle, Point2D, degrees
40from lsst.meas.algorithms.stamps import AbstractStamp
41from lsst.utils.introspection import get_full_type_name
42
43
44@dataclass
46 """A single postage stamp centered on a bright star.
47
48 Attributes
49 ----------
50 stamp_im : `~lsst.afw.image.MaskedImageF`
51 The pixel data for this stamp.
52 psf : `~lsst.afw.detection.Psf`, optional
53 The point-spread function for this star.
54 wcs : `~lsst.afw.geom.SkyWcs`, optional
55 World coordinate system associated with the stamp.
56 visit : `int`, optional
57 Visit number of the observation.
58 detector : `int`, optional
59 Detector ID within the visit.
60 ref_id : `int`, optional
61 Reference catalog ID of the star.
62 ref_mag : `float`, optional
63 Reference catalog magnitude of the star.
64 position : `~lsst.geom.Point2D`, optional
65 Center position of the star on the detector in pixel coordinates.
66 focal_plane_radius : `float`, optional
67 Radial distance from the focal plane center in tangent-plane pixels.
68 focal_plane_angle : `~lsst.geom.Angle`, optional
69 Azimuthal angle on the focal plane (counterclockwise from +X).
70 scale : `float`, optional
71 Flux scaling factor applied to the PSF model.
72 scale_err : `float`, optional
73 Error in the flux scale.
74 pedestal : `float`, optional
75 Background pedestal level.
76 pedestal_err : `float`, optional
77 Error on the pedestal.
78 pedestal_scale_cov : `float`, optional
79 Covariance between pedestal and scale.
80 gradient_x : `float`, optional
81 Background gradient in the X direction.
82 gradient_y : `float`, optional
83 Background gradient in the Y direction.
84 global_reduced_chi_squared : `float`, optional
85 Reduced chi-squared for the global model fit.
86 global_degrees_of_freedom : `int`, optional
87 Degrees of freedom for the global model fit.
88 psf_reduced_chi_squared : `float`, optional
89 Reduced chi-squared for the PSF fit.
90 psf_degrees_of_freedom : `int`, optional
91 Degrees of freedom for the PSF fit.
92 psf_masked_flux_fraction : `float`, optional
93 Fraction of flux masked in the PSF.
94
95 Notes
96 -----
97 This class is designed to be used with `BrightStarStamps`, which manages
98 collections of these stamps and handles reading/writing them to FITS files.
99 The `factory` class method provides a standard interface to construct
100 instances from image data and metadata, while the `_getMetadata` method
101 extracts metadata for storage in FITS headers.
102 """
103
104 stamp_im: MaskedImageF
105 psf: Psf | None
106 wcs: SkyWcs | None
107 visit: int | None
108 detector: int | None
109 ref_id: int | None
110 ref_mag: float | None
111 position: Point2D | None
112 focal_plane_radius: float | None
113 focal_plane_angle: Angle | None
114 scale: float | None
115 scale_err: float | None
116 pedestal: float | None
117 pedestal_err: float | None
118 pedestal_scale_cov: float | None
119 gradient_x: float | None
120 gradient_y: float | None
121 global_reduced_chi_squared: float | None
122 global_degrees_of_freedom: int | None
123 psf_reduced_chi_squared: float | None
124 psf_degrees_of_freedom: int | None
125 psf_masked_flux_fraction: float | None
126
127 # Mapping of metadata keys to attribute names
128 _metadata_attribute_map = {
129 "VISIT": "visit",
130 "DETECTOR": "detector",
131 "REF_ID": "ref_id",
132 "REF_MAG": "ref_mag",
133 "POSITION_X": "position.x",
134 "POSITION_Y": "position.y",
135 "FOCAL_PLANE_RADIUS": "focal_plane_radius",
136 "FOCAL_PLANE_ANGLE_DEGREES": "focal_plane_angle",
137 "SCALE": "scale",
138 "SCALE_ERR": "scale_err",
139 "PEDESTAL": "pedestal",
140 "PEDESTAL_ERR": "pedestal_err",
141 "PEDESTAL_SCALE_COV": "pedestal_scale_cov",
142 "GRADIENT_X": "gradient_x",
143 "GRADIENT_Y": "gradient_y",
144 "GLOBAL_REDUCED_CHI_SQUARED": "global_reduced_chi_squared",
145 "GLOBAL_DEGREES_OF_FREEDOM": "global_degrees_of_freedom",
146 "PSF_REDUCED_CHI_SQUARED": "psf_reduced_chi_squared",
147 "PSF_DEGREES_OF_FREEDOM": "psf_degrees_of_freedom",
148 "PSF_MASKED_FLUX_FRACTION": "psf_masked_flux_fraction",
149 }
150
151 def _getMetadata(self) -> PropertyList:
152 """Extract metadata from the stamp's attributes.
153
154 This method constructs a `PropertyList` containing metadata
155 extracted from the stamp's attributes. It is used when writing the
156 stamp to a FITS file to store relevant metadata in the FITS headers.
157
158 Returns
159 -------
160 metadata : `PropertyList`
161 A `PropertyList` containing the metadata, or `None` if no
162 metadata attributes are defined.
163 """
164 metadata = PropertyList()
165 for metadata_key, attribute_name in self._metadata_attribute_map.items():
166 if "." in attribute_name:
167 top_attr, sub_attr = attribute_name.split(".")
168 value = getattr(getattr(self, top_attr), sub_attr)
169 elif metadata_key == "FOCAL_PLANE_ANGLE_DEGREES":
170 value = getattr(self, attribute_name).asDegrees()
171 else:
172 value = getattr(self, attribute_name)
173 metadata[metadata_key] = value
174 return metadata
175
176 @property
177 def metadata(self) -> PropertyList:
178 """Return the stamp's metadata as a PropertyList."""
179 return self._getMetadata()
180
181 @classmethod
183 cls,
184 stamp_im: MaskedImageF,
185 psf: Psf | None,
186 wcs: SkyWcs | None,
187 metadata: PropertyList,
188 ) -> BrightStarStamp:
189 """Construct a `BrightStarStamp` from image data and metadata.
190
191 This method provides a standard interface to create a `BrightStarStamp`
192 from its image data, PSF, WCS, and associated metadata.
193 It is used by the `BrightStarStamps.readFits` method to construct
194 individual bright star stamps from FITS files.
195
196 Parameters
197 ----------
198 stamp_im : `~lsst.afw.image.MaskedImageF`
199 Masked image for the stamp.
200 psf : `~lsst.afw.detection.Psf`, optional
201 Point-spread function for the stamp.
202 wcs : `~lsst.afw.geom.SkyWcs`, optional
203 World coordinate system for the stamp.
204 metadata : `PropertyList`
205 Metadata associated with the stamp, containing keys for all
206 required attributes.
207
208 Returns
209 -------
210 brightStarStamp : `BrightStarStamp`
211 The constructed `BrightStarStamp` instance.
212 """
213 kwargs = {}
214
215 for metadata_key, attribute_name in cls._metadata_attribute_map.items():
216 if "." in attribute_name: # for nested attributes like position.x
217 top_attr, sub_attr = attribute_name.split(".")
218 if top_attr not in kwargs: # avoid overwriting position
219 if top_attr == "position": # make an initial Point2D
220 kwargs[top_attr] = Point2D(0, 0)
221 setattr(kwargs[top_attr], sub_attr, metadata[metadata_key])
222 elif attribute_name == "focal_plane_angle":
223 kwargs[attribute_name] = Angle(metadata[metadata_key], degrees)
224 else:
225 kwargs[attribute_name] = metadata[metadata_key]
226
227 return cls(stamp_im=stamp_im, psf=psf, wcs=wcs, **kwargs)
228
229
230class BrightStarStamps(Sequence[BrightStarStamp]):
231 """A collection of bright star stamps.
232
233 Parameters
234 ----------
235 brightStarStamps : `Iterable` [`BrightStarStamp`]
236 Collection of `BrightStarStamp` instances.
237 metadata : `~lsst.daf.base.PropertyList`, optional
238 Global metadata associated with the collection.
239 """
240
242 self,
243 brightStarStamps: Sequence[BrightStarStamp],
244 metadata: PropertyList | None = None,
245 ):
246 self._stamps = list(brightStarStamps)
247 self._metadata = PropertyList() if metadata is None else metadata.deepCopy()
248 self.by_ref_id = {stamp.ref_id: stamp for stamp in self}
249
250 def __len__(self):
251 return len(self._stamps)
252
253 def __getitem__(self, index):
254 if isinstance(index, slice):
255 return BrightStarStamps(self._stamps[index], metadata=self._metadata)
256 return self._stamps[index]
257
258 def __iter__(self):
259 return iter(self._stamps)
260
261 @property
262 def metadata(self):
263 """Return the collection's global metadata as a PropertyList."""
264 return self._metadata
265
266 @classmethod
267 def readFits(cls, filename: str) -> BrightStarStamps:
268 """Make a `BrightStarStamps` object from a FITS file.
269
270 Parameters
271 ----------
272 filename : `str`
273 Name of the FITS file to read.
274
275 Returns
276 -------
277 brightStarStamps : `BrightStarStamps`
278 The constructed `BrightStarStamps` instance.
279 """
280 return cls.readFitsWithOptions(filename, None)
281
282 @classmethod
283 def readFitsWithOptions(cls, filename: str, options: PropertyList | None) -> BrightStarStamps:
284 """Make a `BrightStarStamps` object from a FITS file, with options.
285
286 Parameters
287 ----------
288 filename : `str`
289 Name of the FITS file to read.
290 options : `~lsst.daf.base.PropertyList`, optional
291 Options for reading the FITS file. Not currently used.
292
293 Returns
294 -------
295 brightStarStamps : `BrightStarStamps`
296 The constructed `BrightStarStamps` instance.
297 """
298 with Fits(filename, "r") as fits_file:
299 stamp_planes = {}
300 stamp_psf_ids = {}
301 stamp_wcs_ids = {}
302 stamp_metadata = {}
303 archive = None
304
305 for hdu_num in range(1, fits_file.countHdus()): # Skip primary HDU
306 metadata = readMetadata(filename, hdu=hdu_num)
307 extname = metadata["EXTNAME"]
308 stamp_id: int | None = metadata.get("EXTVER", None)
309
310 # Skip non-image BINTABLEs (except ARCHIVE_INDEX)
311 if metadata["XTENSION"] == "BINTABLE" and not metadata.get("ZIMAGE", False):
312 if extname != "ARCHIVE_INDEX":
313 continue
314
315 # Handle the archive index separately
316 if extname == "ARCHIVE_INDEX":
317 fits_file.setHdu(hdu_num)
318 archive = InputArchive.readFits(fits_file)
319 continue
320 elif metadata.get("EXTTYPE") == "ARCHIVE_DATA":
321 continue
322
323 # Select reader and dtype
324 if extname == "IMAGE":
325 reader = ImageFitsReader(filename, hdu=hdu_num)
326 dtype = np.dtype(MaskedImageF.dtype)
327 stamp_psf_ids[stamp_id] = metadata.pop("PSF", None)
328 stamp_wcs_ids[stamp_id] = metadata.pop("WCS", None)
329 stamp_metadata[stamp_id] = metadata
330 elif extname == "MASK":
331 reader = MaskFitsReader(filename, hdu=hdu_num)
332 dtype = None
333 elif extname == "VARIANCE":
334 reader = ImageFitsReader(filename, hdu=hdu_num)
335 dtype = np.dtype("float32")
336 else:
337 raise ValueError(f"Unknown extension type: {extname}")
338
339 if stamp_id is not None:
340 stamp_planes.setdefault(stamp_id, {})[extname.lower()] = reader.read(dtype=dtype)
341
342 primary_metadata = readMetadata(filename, hdu=0)
343 num_stamps = primary_metadata["N_STAMPS"]
344
345 if len(stamp_planes) != num_stamps:
346 raise ValueError(
347 f"Number of stamps read ({len(stamp_planes)}) does not agree with the "
348 f"number of stamps recorded in the primary HDU metadata ({num_stamps})."
349 )
350 if archive is None:
351 raise ValueError("No archive index was found in the FITS file; cannot read PSF or WCS.")
352
353 brightStarStamps = []
354 for stamp_id in range(1, num_stamps + 1): # Need to increment by one as EXTVER starts at 1
355 stamp = MaskedImageF(**stamp_planes[stamp_id])
356 psf = archive.get(stamp_psf_ids[stamp_id])
357 wcs = archive.get(stamp_wcs_ids[stamp_id])
358 brightStarStamps.append(BrightStarStamp.factory(stamp, psf, wcs, stamp_metadata[stamp_id]))
359
360 return cls(brightStarStamps, primary_metadata)
361
362 def writeFits(self, filename: str):
363 """Write this `BrightStarStamps` object to a FITS file.
364
365 Parameters
366 ----------
367 filename : `str`
368 Name of the FITS file to write.
369 """
370 metadata = self._metadata.deepCopy()
371
372 # Store metadata in the primary HDU
373 metadata["N_STAMPS"] = len(self._stamps)
374 metadata["VERSION"] = 2 # Record version number in case of future code changes
375 metadata["STAMPCLS"] = get_full_type_name(self)
376
377 # Create and write to the FITS file within a context manager
378 with Fits(filename, "w") as fits_file:
379 fits_file.createEmpty()
380
381 # Store Persistables in an OutputArchive
382 output_archive = OutputArchive()
383 stamp_psf_ids = []
384 stamp_wcs_ids = []
385 for stamp in self._stamps:
386 stamp_psf_ids.append(output_archive.put(stamp.psf))
387 stamp_wcs_ids.append(output_archive.put(stamp.wcs))
388
389 # Write to the FITS file
390 fits_file.writeMetadata(metadata)
391 del metadata
392 output_archive.writeFits(fits_file)
393
394 # Add all pixel data to extension HDUs; note: EXTVER should be 1-based
395 for stamp_id, (stamp, stamp_psf_id, stamp_wcs_id) in enumerate(
396 zip(self._stamps, stamp_psf_ids, stamp_wcs_ids),
397 start=1,
398 ):
399 metadata = PropertyList()
400 metadata.update({"EXTVER": stamp_id, "EXTNAME": "IMAGE"})
401 if stamp_metadata := stamp._getMetadata():
402 metadata.update(stamp_metadata)
403 metadata["PSF"] = stamp_psf_id
404 metadata["WCS"] = stamp_wcs_id
405 stamp.stamp_im.getImage().writeFits(filename, metadata=metadata, mode="a")
406
407 metadata = PropertyList()
408 metadata.update({"EXTVER": stamp_id, "EXTNAME": "MASK"})
409 stamp.stamp_im.getMask().writeFits(filename, metadata=metadata, mode="a")
410
411 metadata = PropertyList()
412 metadata.update({"EXTVER": stamp_id, "EXTNAME": "VARIANCE"})
413 stamp.stamp_im.getVariance().writeFits(filename, metadata=metadata, mode="a")
A simple struct that combines the two arguments that must be passed to most cfitsio routines and cont...
Definition fits.h:308
A FITS reader class for regular Images.
A FITS reader class for Masks.
A multi-catalog archive object used to save table::io::Persistable objects.
Class for storing ordered metadata with comments.
A class representing an angle.
Definition Angle.h:128
BrightStarStamp factory(cls, MaskedImageF stamp_im, Psf|None psf, SkyWcs|None wcs, PropertyList metadata)
__init__(self, Sequence[BrightStarStamp] brightStarStamps, PropertyList|None metadata=None)
BrightStarStamps readFitsWithOptions(cls, str filename, PropertyList|None options)