Coverage for python/lsst/pipe/tasks/extended_psf.py : 16%

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 pipe_tasks.
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"""Read preprocessed bright stars and stack them to build an extended
23PSF model.
24"""
26from dataclasses import dataclass
27from typing import List
29from lsst.afw import image as afwImage
30from lsst.afw import fits as afwFits
31from lsst.daf.base import PropertyList
34@dataclass
35class FocalPlaneRegionExtendedPsf:
36 """Single extended PSF over a focal plane region.
38 The focal plane region is defined through a list
39 of detectors.
41 Parameters
42 ----------
43 extended_psf_image : `lsst.afw.image.MaskedImageF`
44 Image of the extended PSF model.
45 detector_list : `list` [`int`]
46 List of detector IDs that define the focal plane region over which this
47 extended PSF model has been built (and can be used).
48 """
49 extended_psf_image: afwImage.MaskedImageF
50 detector_list: List[int]
53class ExtendedPsf:
54 """Extended PSF model.
56 Each instance may contain a default extended PSF, a set of extended PSFs
57 that correspond to different focal plane regions, or both. At this time,
58 focal plane regions are always defined as a subset of detectors.
60 Parameters
61 ----------
62 default_extended_psf : `lsst.afw.image.MaskedImageF`
63 Extended PSF model to be used as default (or only) extended PSF model.
64 """
65 def __init__(self, default_extended_psf=None):
66 self.default_extended_psf = default_extended_psf
67 self.focal_plane_regions = {}
68 self.detectors_focal_plane_regions = {}
70 def add_regional_extended_psf(self, extended_psf_image, region_name, detector_list):
71 """Add a new focal plane region, along wit hits extended PSF, to the
72 ExtendedPsf instance.
74 Parameters
75 ----------
76 extended_psf_image : `lsst.afw.image.MaskedImageF`
77 Extended PSF model for the region.
78 region_name : `str`
79 Name of the focal plane region. Will be converted to all-uppercase.
80 detector_list : `list` [`int`]
81 List of IDs for the detectors that define the focal plane region.
82 """
83 region_name = region_name.upper()
84 if region_name in self.focal_plane_regions:
85 raise ValueError(f"Region name {region_name} is already used by this ExtendedPsf instance.")
86 self.focal_plane_regions[region_name] = FocalPlaneRegionExtendedPsf(
87 extended_psf_image=extended_psf_image, detector_list=detector_list)
88 for det in detector_list:
89 self.detectors_focal_plane_regions[det] = region_name
91 def __call__(self, detector=None):
92 """Return the appropriate extended PSF.
94 If the instance contains no extended PSF defined over focal plane
95 regions, the default extended PSF will be returned regardless of
96 whether a detector ID was passed as argument.
98 Parameters
99 ----------
100 detector : `int`, optional
101 Detector ID. If focal plane region PSFs are defined, is used to
102 determine which model to return.
104 Returns
105 -------
106 extendedPsfImage : `lsst.afw.image.MaskedImageF`
107 The extended PSF model. If this instance contains extended PSFs
108 defined over focal plane regions, the extended PSF model for the
109 region that contains ``detector`` is returned. If not, the default
110 extended PSF is returned.
111 """
112 if detector is None:
113 if self.default_extended_psf is None:
114 raise ValueError("No default extended PSF available; please provide detector number.")
115 return self.default_extended_psf
116 elif not self.focal_plane_regions:
117 return self.default_extended_psf
118 return self.get_regional_extended_psf(detector=detector)
120 def __len__(self):
121 """Returns the number of extended PSF models present in the instance.
123 Note that if the instance contains both a default model and a set of
124 focal plane region models, the length of the instance will be the
125 number of regional models, plus one (the default). This is true even
126 in the case where the default model is one of the focal plane
127 region-specific models.
128 """
129 n_regions = len(self.focal_plane_regions)
130 if self.default_extended_psf is not None:
131 n_regions += 1
132 return n_regions
134 def get_regional_extended_psf(self, region_name=None, detector=None):
135 """Returns the extended PSF for a focal plane region.
137 The region can be identified either by name, or through a detector ID.
139 Parameters
140 ----------
141 region_name : `str` or `None`, optional
142 Name of the region for which the extended PSF should be retrieved.
143 Ignored if ``detector`` is provided. Must be provided if
144 ``detector`` is None.
145 detector : `int` or `None`, optional
146 If provided, returns the extended PSF for the focal plane region
147 that includes this detector.
149 Raises
150 ------
151 ValueError
152 Raised if neither ``detector`` nor ``regionName`` is provided.
153 """
154 if detector is None:
155 if region_name is None:
156 raise ValueError("One of either a regionName or a detector number must be provided.")
157 return self.focal_plane_regions[region_name].extended_psf_image
158 return self.focal_plane_regions[self.detectors_focal_plane_regions[detector]].extended_psf_image
160 def write_fits(self, filename):
161 """Write this object to a file.
163 Parameters
164 ----------
165 filename : `str`
166 Name of file to write.
167 """
168 # Create primary HDU with global metadata.
169 metadata = PropertyList()
170 metadata["HAS_DEFAULT"] = self.default_extended_psf is not None
171 if self.focal_plane_regions:
172 metadata["HAS_REGIONS"] = True
173 metadata["REGION_NAMES"] = list(self.focal_plane_regions.keys())
174 for region, e_psf_region in self.focal_plane_regions.items():
175 metadata[region] = e_psf_region.detector_list
176 else:
177 metadata["HAS_REGIONS"] = False
178 fits_primary = afwFits.Fits(filename, "w")
179 fits_primary.createEmpty()
180 fits_primary.writeMetadata(metadata)
181 fits_primary.closeFile()
182 # Write default extended PSF.
183 if self.default_extended_psf is not None:
184 default_hdu_metadata = PropertyList()
185 default_hdu_metadata.update({"REGION": "DEFAULT", "EXTNAME": "IMAGE"})
186 self.default_extended_psf.image.writeFits(filename, metadata=default_hdu_metadata, mode="a")
187 default_hdu_metadata.update({"REGION": "DEFAULT", "EXTNAME": "MASK"})
188 self.default_extended_psf.mask.writeFits(filename, metadata=default_hdu_metadata, mode="a")
189 # Write extended PSF for each focal plane region.
190 for j, (region, e_psf_region) in enumerate(self.focal_plane_regions.items()):
191 metadata = PropertyList()
192 metadata.update({"REGION": region, "EXTNAME": "IMAGE"})
193 e_psf_region.extended_psf_image.image.writeFits(filename, metadata=metadata, mode="a")
194 metadata.update({"REGION": region, "EXTNAME": "MASK"})
195 e_psf_region.extended_psf_image.mask.writeFits(filename, metadata=metadata, mode="a")
197 def writeFits(self, filename):
198 """Alias for ``write_fits``; exists for compatibility with the Butler.
199 """
200 self.write_fits(filename)
202 @classmethod
203 def read_fits(cls, filename):
204 """Build an instance of this class from a file.
206 Parameters
207 ----------
208 filename : `str`
209 Name of the file to read.
210 """
211 # Extract info from metadata.
212 global_metadata = afwFits.readMetadata(filename, hdu=0)
213 has_default = global_metadata.getBool("HAS_DEFAULT")
214 if global_metadata.getBool("HAS_REGIONS"):
215 focal_plane_region_names = global_metadata.getArray("REGION_NAMES")
216 else:
217 focal_plane_region_names = []
218 f = afwFits.Fits(filename, "r")
219 n_extensions = f.countHdus()
220 extended_psf_parts = {}
221 for j in range(1, n_extensions):
222 md = afwFits.readMetadata(filename, hdu=j)
223 if has_default and md["REGION"] == "DEFAULT":
224 if md["EXTNAME"] == "IMAGE":
225 default_image = afwImage.ImageF(filename, hdu=j)
226 elif md["EXTNAME"] == "MASK":
227 default_mask = afwImage.MaskX(filename, hdu=j)
228 continue
229 if md["EXTNAME"] == "IMAGE":
230 extended_psf_part = afwImage.ImageF(filename, hdu=j)
231 elif md["EXTNAME"] == "MASK":
232 extended_psf_part = afwImage.MaskX(filename, hdu=j)
233 extended_psf_parts.setdefault(md["REGION"], {})[md["EXTNAME"].lower()] = extended_psf_part
234 # Handle default if present.
235 if has_default:
236 extended_psf = cls(afwImage.MaskedImageF(default_image, default_mask))
237 else:
238 extended_psf = cls()
239 # Ensure we recovered an extended PSF for all focal plane regions.
240 if len(extended_psf_parts) != len(focal_plane_region_names):
241 raise ValueError(f"Number of per-region extended PSFs read ({len(extended_psf_parts)}) does not "
242 "match with the number of regions recorded in the metadata "
243 f"({len(focal_plane_region_names)}).")
244 # Generate extended PSF regions mappings.
245 for r_name in focal_plane_region_names:
246 extended_psf_image = afwImage.MaskedImageF(**extended_psf_parts[r_name])
247 detector_list = global_metadata.getArray(r_name)
248 extended_psf.add_regional_extended_psf(extended_psf_image, r_name, detector_list)
249 # Instantiate ExtendedPsf.
250 return extended_psf
252 @classmethod
253 def readFits(cls, filename):
254 """Alias for ``readFits``; exists for compatibility with the Butler.
255 """
256 return cls.read_fits(filename)