diff --git a/python/lsst/meas/algorithms/__init__.py b/python/lsst/meas/algorithms/__init__.py index eff9e02e..59e9742b 100644 --- a/python/lsst/meas/algorithms/__init__.py +++ b/python/lsst/meas/algorithms/__init__.py @@ -56,7 +56,6 @@ from .dynamicDetection import * from .makePsfCandidates import * from .stamps import * -from .brightStarStamps import * from .accumulator_mean_stack import * from .scaleVariance import * from .noise_covariance import * diff --git a/python/lsst/meas/algorithms/brightStarStamps.py b/python/lsst/meas/algorithms/brightStarStamps.py deleted file mode 100644 index 9701c523..00000000 --- a/python/lsst/meas/algorithms/brightStarStamps.py +++ /dev/null @@ -1,425 +0,0 @@ -# This file is part of meas_algorithms. -# -# Developed for the LSST Data Management System. -# This product includes software developed by the LSST Project -# (https://www.lsst.org). -# See the COPYRIGHT file at the top-level directory of this distribution -# for details of code ownership. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Collection of small images (postage stamps) centered on bright stars.""" - -from __future__ import annotations - -__all__ = ["BrightStarStamp", "BrightStarStamps"] - -from collections.abc import Sequence -from dataclasses import dataclass - -import numpy as np - -from lsst.afw.detection import Psf -from lsst.afw.fits import Fits, readMetadata -from lsst.afw.geom import SkyWcs -from lsst.afw.image import ImageFitsReader, MaskedImageF, MaskFitsReader -from lsst.afw.table.io import InputArchive, OutputArchive -from lsst.daf.base import PropertyList -from lsst.geom import Angle, Point2D, degrees -from lsst.meas.algorithms.stamps import AbstractStamp -from lsst.utils.introspection import get_full_type_name - - -@dataclass -class BrightStarStamp(AbstractStamp): - """A single postage stamp centered on a bright star. - - Attributes - ---------- - stamp_im : `~lsst.afw.image.MaskedImageF` - The pixel data for this stamp. - psf : `~lsst.afw.detection.Psf`, optional - The point-spread function for this star. - wcs : `~lsst.afw.geom.SkyWcs`, optional - World coordinate system associated with the stamp. - visit : `int`, optional - Visit number of the observation. - detector : `int`, optional - Detector ID within the visit. - ref_id : `int`, optional - Reference catalog ID of the star. - ref_mag : `float`, optional - Reference catalog magnitude of the star. - position : `~lsst.geom.Point2D`, optional - Center position of the star on the detector in pixel coordinates. - focal_plane_radius : `float`, optional - Radial distance from the focal plane center in tangent-plane pixels. - focal_plane_angle : `~lsst.geom.Angle`, optional - Azimuthal angle on the focal plane (counterclockwise from +X). - scale : `float`, optional - Flux scaling factor applied to the PSF model. - scale_err : `float`, optional - Error in the flux scale. - pedestal : `float`, optional - Background pedestal level. - pedestal_err : `float`, optional - Error on the pedestal. - pedestal_scale_cov : `float`, optional - Covariance between pedestal and scale. - gradient_x : `float`, optional - Background gradient in the X direction. - gradient_y : `float`, optional - Background gradient in the Y direction. - curvature_x: `float`, optional - Background curvature in the X direction. - curvature_y: `float`, optional - Background curvature in the Y direction. - curvature_xy: `float`, optional - The xy component of the second order fit. - global_reduced_chi_squared : `float`, optional - Reduced chi-squared for the global model fit. - global_degrees_of_freedom : `int`, optional - Degrees of freedom for the global model fit. - psf_reduced_chi_squared : `float`, optional - Reduced chi-squared for the PSF fit. - psf_degrees_of_freedom : `int`, optional - Degrees of freedom for the PSF fit. - psf_masked_flux_fraction : `float`, optional - Fraction of flux masked in the PSF. - - Notes - ----- - This class is designed to be used with `BrightStarStamps`, which manages - collections of these stamps and handles reading/writing them to FITS files. - The `factory` class method provides a standard interface to construct - instances from image data and metadata, while the `_getMetadata` method - extracts metadata for storage in FITS headers. - """ - - stamp_im: MaskedImageF - psf: Psf | None - wcs: SkyWcs | None - visit: int | None - detector: int | None - ref_id: int | None - ref_mag: float | None - position: Point2D | None - focal_plane_radius: float | None - focal_plane_angle: Angle | None - scale: float | None - scale_err: float | None - pedestal: float | None - pedestal_err: float | None - pedestal_scale_cov: float | None - gradient_x: float | None - gradient_y: float | None - curvature_x: float | None - curvature_y: float | None - curvature_xy: float | None - global_reduced_chi_squared: float | None - global_degrees_of_freedom: int | None - psf_reduced_chi_squared: float | None - psf_degrees_of_freedom: int | None - psf_masked_flux_fraction: float | None - - # Mapping of metadata keys to attribute names - _metadata_attribute_map = { - "VISIT": "visit", - "DETECTOR": "detector", - "REF_ID": "ref_id", - "REF_MAG": "ref_mag", - "POSITION_X": "position.x", - "POSITION_Y": "position.y", - "FOCAL_PLANE_RADIUS": "focal_plane_radius", - "FOCAL_PLANE_ANGLE_DEGREES": "focal_plane_angle", - "SCALE": "scale", - "SCALE_ERR": "scale_err", - "PEDESTAL": "pedestal", - "PEDESTAL_ERR": "pedestal_err", - "PEDESTAL_SCALE_COV": "pedestal_scale_cov", - "GRADIENT_X": "gradient_x", - "GRADIENT_Y": "gradient_y", - "CURVATURE_X": "curvature_x", - "CURVATURE_Y": "curvature_y", - "CURVATURE_XY": "curvature_xy", - "GLOBAL_REDUCED_CHI_SQUARED": "global_reduced_chi_squared", - "GLOBAL_DEGREES_OF_FREEDOM": "global_degrees_of_freedom", - "PSF_REDUCED_CHI_SQUARED": "psf_reduced_chi_squared", - "PSF_DEGREES_OF_FREEDOM": "psf_degrees_of_freedom", - "PSF_MASKED_FLUX_FRACTION": "psf_masked_flux_fraction", - } - - def _getMetadata(self) -> PropertyList: - """Extract metadata from the stamp's attributes. - - This method constructs a `PropertyList` containing metadata - extracted from the stamp's attributes. It is used when writing the - stamp to a FITS file to store relevant metadata in the FITS headers. - - Returns - ------- - metadata : `PropertyList` - A `PropertyList` containing the metadata, or `None` if no - metadata attributes are defined. - """ - metadata = PropertyList() - for metadata_key, attribute_name in self._metadata_attribute_map.items(): - if "." in attribute_name: - top_attr, sub_attr = attribute_name.split(".") - value = getattr(getattr(self, top_attr), sub_attr) - elif metadata_key == "FOCAL_PLANE_ANGLE_DEGREES": - value = getattr(self, attribute_name).asDegrees() - else: - value = getattr(self, attribute_name) - metadata[metadata_key] = value - return metadata - - @property - def metadata(self) -> PropertyList: - """Return the stamp's metadata as a PropertyList.""" - return self._getMetadata() - - @classmethod - def factory( - cls, - stamp_im: MaskedImageF, - psf: Psf | None, - wcs: SkyWcs | None, - metadata: PropertyList, - ) -> BrightStarStamp: - """Construct a `BrightStarStamp` from image data and metadata. - - This method provides a standard interface to create a `BrightStarStamp` - from its image data, PSF, WCS, and associated metadata. - It is used by the `BrightStarStamps.readFits` method to construct - individual bright star stamps from FITS files. - - Parameters - ---------- - stamp_im : `~lsst.afw.image.MaskedImageF` - Masked image for the stamp. - psf : `~lsst.afw.detection.Psf`, optional - Point-spread function for the stamp. - wcs : `~lsst.afw.geom.SkyWcs`, optional - World coordinate system for the stamp. - metadata : `PropertyList` - Metadata associated with the stamp, containing keys for all - required attributes. - - Returns - ------- - brightStarStamp : `BrightStarStamp` - The constructed `BrightStarStamp` instance. - """ - kwargs = {} - - for metadata_key, attribute_name in cls._metadata_attribute_map.items(): - if "." in attribute_name: # for nested attributes like position.x - top_attr, sub_attr = attribute_name.split(".") - if top_attr not in kwargs: # avoid overwriting position - if top_attr == "position": # make an initial Point2D - kwargs[top_attr] = Point2D(0, 0) - setattr(kwargs[top_attr], sub_attr, metadata[metadata_key]) - elif attribute_name == "focal_plane_angle": - kwargs[attribute_name] = Angle(metadata[metadata_key], degrees) - else: - kwargs[attribute_name] = metadata[metadata_key] - - return cls(stamp_im=stamp_im, psf=psf, wcs=wcs, **kwargs) - - -class BrightStarStamps(Sequence[BrightStarStamp]): - """A collection of bright star stamps. - - Parameters - ---------- - brightStarStamps : `Iterable` [`BrightStarStamp`] - Collection of `BrightStarStamp` instances. - metadata : `~lsst.daf.base.PropertyList`, optional - Global metadata associated with the collection. - """ - - def __init__( - self, - brightStarStamps: Sequence[BrightStarStamp], - metadata: PropertyList | None = None, - ): - self._stamps = list(brightStarStamps) - self._metadata = PropertyList() if metadata is None else metadata.deepCopy() - self.by_ref_id = {stamp.ref_id: stamp for stamp in self} - - def __len__(self): - return len(self._stamps) - - def __getitem__(self, index): - if isinstance(index, slice): - return BrightStarStamps(self._stamps[index], metadata=self._metadata) - return self._stamps[index] - - def __iter__(self): - return iter(self._stamps) - - @property - def metadata(self): - """Return the collection's global metadata as a PropertyList.""" - return self._metadata - - @classmethod - def readFits(cls, filename: str) -> BrightStarStamps: - """Make a `BrightStarStamps` object from a FITS file. - - Parameters - ---------- - filename : `str` - Name of the FITS file to read. - - Returns - ------- - brightStarStamps : `BrightStarStamps` - The constructed `BrightStarStamps` instance. - """ - return cls.readFitsWithOptions(filename, None) - - @classmethod - def readFitsWithOptions(cls, filename: str, options: PropertyList | None) -> BrightStarStamps: - """Make a `BrightStarStamps` object from a FITS file, with options. - - Parameters - ---------- - filename : `str` - Name of the FITS file to read. - options : `~lsst.daf.base.PropertyList`, optional - Options for reading the FITS file. Not currently used. - - Returns - ------- - brightStarStamps : `BrightStarStamps` - The constructed `BrightStarStamps` instance. - """ - with Fits(filename, "r") as fits_file: - stamp_planes = {} - stamp_psf_ids = {} - stamp_wcs_ids = {} - stamp_metadata = {} - archive = None - - for hdu_num in range(1, fits_file.countHdus()): # Skip primary HDU - metadata = readMetadata(filename, hdu=hdu_num) - extname = metadata["EXTNAME"] - stamp_id: int | None = metadata.get("EXTVER", None) - - # Skip non-image BINTABLEs (except ARCHIVE_INDEX) - if metadata["XTENSION"] == "BINTABLE" and not metadata.get("ZIMAGE", False): - if extname != "ARCHIVE_INDEX": - continue - - # Handle the archive index separately - if extname == "ARCHIVE_INDEX": - fits_file.setHdu(hdu_num) - archive = InputArchive.readFits(fits_file) - continue - elif metadata.get("EXTTYPE") == "ARCHIVE_DATA": - continue - - # Select reader and dtype - if extname == "IMAGE": - reader = ImageFitsReader(filename, hdu=hdu_num) - dtype = np.dtype(MaskedImageF.dtype) - stamp_psf_ids[stamp_id] = metadata.pop("PSF", None) - stamp_wcs_ids[stamp_id] = metadata.pop("WCS", None) - stamp_metadata[stamp_id] = metadata - elif extname == "MASK": - reader = MaskFitsReader(filename, hdu=hdu_num) - dtype = None - elif extname == "VARIANCE": - reader = ImageFitsReader(filename, hdu=hdu_num) - dtype = np.dtype("float32") - else: - raise ValueError(f"Unknown extension type: {extname}") - - if stamp_id is not None: - stamp_planes.setdefault(stamp_id, {})[extname.lower()] = reader.read(dtype=dtype) - - primary_metadata = readMetadata(filename, hdu=0) - num_stamps = primary_metadata["N_STAMPS"] - - if len(stamp_planes) != num_stamps: - raise ValueError( - f"Number of stamps read ({len(stamp_planes)}) does not agree with the " - f"number of stamps recorded in the primary HDU metadata ({num_stamps})." - ) - if archive is None: - raise ValueError("No archive index was found in the FITS file; cannot read PSF or WCS.") - - brightStarStamps = [] - for stamp_id in range(1, num_stamps + 1): # Need to increment by one as EXTVER starts at 1 - stamp = MaskedImageF(**stamp_planes[stamp_id]) - psf = archive.get(stamp_psf_ids[stamp_id]) - wcs = archive.get(stamp_wcs_ids[stamp_id]) - brightStarStamps.append(BrightStarStamp.factory(stamp, psf, wcs, stamp_metadata[stamp_id])) - - return cls(brightStarStamps, primary_metadata) - - def writeFits(self, filename: str): - """Write this `BrightStarStamps` object to a FITS file. - - Parameters - ---------- - filename : `str` - Name of the FITS file to write. - """ - metadata = self._metadata.deepCopy() - - # Store metadata in the primary HDU - metadata["N_STAMPS"] = len(self._stamps) - metadata["VERSION"] = 2 # Record version number in case of future code changes - metadata["STAMPCLS"] = get_full_type_name(self) - - # Create and write to the FITS file within a context manager - with Fits(filename, "w") as fits_file: - fits_file.createEmpty() - - # Store Persistables in an OutputArchive - output_archive = OutputArchive() - stamp_psf_ids = [] - stamp_wcs_ids = [] - for stamp in self._stamps: - stamp_psf_ids.append(output_archive.put(stamp.psf)) - stamp_wcs_ids.append(output_archive.put(stamp.wcs)) - - # Write to the FITS file - fits_file.writeMetadata(metadata) - del metadata - output_archive.writeFits(fits_file) - - # Add all pixel data to extension HDUs; note: EXTVER should be 1-based - for stamp_id, (stamp, stamp_psf_id, stamp_wcs_id) in enumerate( - zip(self._stamps, stamp_psf_ids, stamp_wcs_ids), - start=1, - ): - metadata = PropertyList() - metadata.update({"EXTVER": stamp_id, "EXTNAME": "IMAGE"}) - if stamp_metadata := stamp._getMetadata(): - metadata.update(stamp_metadata) - metadata["PSF"] = stamp_psf_id - metadata["WCS"] = stamp_wcs_id - stamp.stamp_im.getImage().writeFits(filename, metadata=metadata, mode="a") - - metadata = PropertyList() - metadata.update({"EXTVER": stamp_id, "EXTNAME": "MASK"}) - stamp.stamp_im.getMask().writeFits(filename, metadata=metadata, mode="a") - - metadata = PropertyList() - metadata.update({"EXTVER": stamp_id, "EXTNAME": "VARIANCE"}) - stamp.stamp_im.getVariance().writeFits(filename, metadata=metadata, mode="a") diff --git a/tests/test_brightStarStamps.py b/tests/test_brightStarStamps.py deleted file mode 100644 index de3dee79..00000000 --- a/tests/test_brightStarStamps.py +++ /dev/null @@ -1,120 +0,0 @@ -# This file is part of meas_algorithms. -# -# Developed for the LSST Data Management System. -# This product includes software developed by the LSST Project -# (https://www.lsst.org). -# See the COPYRIGHT file at the top-level directory of this distribution -# for details of code ownership. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import unittest -from tempfile import NamedTemporaryFile - -import lsst.utils.tests -import numpy as np -from lsst.afw.image import MaskedImageF -from lsst.daf.base import PropertyList -from lsst.meas.algorithms import BrightStarStamp, BrightStarStamps -from lsst.meas.algorithms.stamps import StampsBase - - -class BrightStarStampsTestCase(lsst.utils.tests.TestCase): - """Test BrightStarStamps.""" - - def setUp(self): - rng = np.random.Generator(np.random.MT19937(seed=5)) - stamp_size = (25, 25) - - # Generate simulated bright star stamps - brightStarStamps = [] - self.metadatas = [] - for i in range(3): - stamp = MaskedImageF(*stamp_size) - stamp_array = stamp.image.array - stamp_array += rng.random(stamp_size) - psf = None - wcs = None - metadata = PropertyList() - metadata.set("VISIT", i) - metadata.set("DETECTOR", i + 1) - metadata.set("REF_ID", f"ref{i}") - metadata.set("REF_MAG", float(i * 5)) - metadata.set("POSITION_X", rng.random()) - metadata.set("POSITION_Y", rng.random()) - metadata.set("FOCAL_PLANE_RADIUS", rng.random()) - metadata.set("FOCAL_PLANE_ANGLE_DEGREES", rng.random()) - metadata.set("SCALE", rng.random()) - metadata.set("SCALE_ERR", rng.random()) - metadata.set("PEDESTAL", rng.random()) - metadata.set("PEDESTAL_ERR", rng.random()) - metadata.set("PEDESTAL_SCALE_COV", rng.random()) - metadata.set("GRADIENT_X", rng.random()) - metadata.set("GRADIENT_Y", rng.random()) - metadata.set("CURVATURE_X", rng.random()) - metadata.set("CURVATURE_Y", rng.random()) - metadata.set("CURVATURE_XY", rng.random()) - metadata.set("GLOBAL_REDUCED_CHI_SQUARED", rng.random()) - metadata.set("GLOBAL_DEGREES_OF_FREEDOM", rng.integers(1, 100)) - metadata.set("PSF_REDUCED_CHI_SQUARED", rng.random()) - metadata.set("PSF_DEGREES_OF_FREEDOM", rng.integers(1, 100)) - metadata.set("PSF_MASKED_FLUX_FRACTION", rng.random()) - self.metadatas.append(metadata) - brightStarStamp = BrightStarStamp.factory(stamp, psf, wcs, metadata) - brightStarStamps.append(brightStarStamp) - self.primary_metadata = PropertyList() - self.primary_metadata.set("TEST_KEY", "TEST VALUE") - self.brightStarStamps = BrightStarStamps(brightStarStamps, self.primary_metadata) - - def tearDown(self): - del self.brightStarStamps - del self.metadatas - del self.primary_metadata - - def testBrightStarStamps(self): - """Test that BrightStarStamps can be serialized and deserialized.""" - - with NamedTemporaryFile() as file: - self.brightStarStamps.writeFits(file.name) - # Test StampsBase correctly bounces to BrightStarStamps readFits - brightStarStamps = StampsBase.readFits(file.name) - - primary_metadata = brightStarStamps.metadata - self.assertEqual(self.primary_metadata["TEST_KEY"], primary_metadata["TEST_KEY"]) - self.assertEqual(len(self.metadatas), primary_metadata["N_STAMPS"]) - for input_metadata, input_stamp, output_stamp in zip( - self.metadatas, self.brightStarStamps, brightStarStamps - ): - output_metadata = output_stamp.metadata - for key in output_metadata.names(): - input_value = input_metadata[key] - output_value = output_metadata[key] - if isinstance(input_value, float): - self.assertAlmostEqual(input_value, output_value, places=10) - else: - self.assertEqual(input_value, output_value) - self.assertMaskedImagesAlmostEqual(input_stamp.stamp_im, output_stamp.stamp_im) - - -class MemoryTester(lsst.utils.tests.MemoryTestCase): - pass - - -def setup_module(module): - lsst.utils.tests.init() - - -if __name__ == "__main__": - lsst.utils.tests.init() - unittest.main()