lsst.pipe.tasks g32c4e0c91b+ac4e7d02e6
Loading...
Searching...
No Matches
extended_psf_candidates.py
Go to the documentation of this file.
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
22from __future__ import annotations
23
24__all__ = [
25 "ExtendedPsfCandidateInfo",
26 "ExtendedPsfCandidateSerializationModel",
27 "ExtendedPsfCandidatesSerializationModel",
28 "ExtendedPsfCandidate",
29 "ExtendedPsfCandidates",
30]
31
32import functools
33from collections.abc import Sequence
34from types import EllipsisType
35from typing import Any
36
37from pydantic import BaseModel, Field
38
39from lsst.images import (
40 Box,
41 Image,
42 ImageSerializationModel,
43 Mask,
44 MaskedImage,
45 MaskedImageSerializationModel,
46 MaskSchema,
47 Projection,
48 fits,
49)
50from lsst.images.serialization import ArchiveTree, InputArchive, MetadataValue, OutputArchive, Quantity
51from lsst.images.utils import is_none
52from lsst.resources import ResourcePathExpression
53
54
55class ExtendedPsfCandidateInfo(BaseModel):
56 """Information about a star in an `ExtendedPsfCandidate`.
57
58 Attributes
59 ----------
60 visit : `int`, optional
61 The visit during which the star was observed.
62 detector : `int`, optional
63 The detector on which the star was observed.
64 ref_id : `int`, optional
65 The reference catalog ID for the star.
66 ref_mag : `float`, optional
67 The reference magnitude for the star.
68 position_x : `float`, optional
69 The x-coordinate of the star in the focal plane.
70 position_y : `float`, optional
71 The y-coordinate of the star in the focal plane.
72 focal_plane_radius : `~lsst.images.utils.Quantity`, optional
73 The radius of the star from the center of the focal plane.
74 focal_plane_angle : `~lsst.images.utils.Quantity`, optional
75 The angle of the star in the focal plane, measured from the +x axis.
76 """
77
78 visit: int | None = None
79 detector: int | None = None
80 ref_id: int | None = None
81 ref_mag: float | None = None
82 position_x: float | None = None
83 position_y: float | None = None
84 focal_plane_radius: Quantity | None = None
85 focal_plane_angle: Quantity | None = None
86
87 def __str__(self) -> str:
88 attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
89 return f"ExtendedPsfCandidateInfo({attrs})"
90
91 __repr__ = __str__
92
93
94class ExtendedPsfCandidateSerializationModel[P: BaseModel](MaskedImageSerializationModel[P]):
95 """A Pydantic model to represent a serialized `ExtendedPsfCandidate`."""
96
97 psf_kernel_image: ImageSerializationModel[P] | None = Field(
98 default=None,
99 exclude_if=is_none,
100 description="Kernel image of the PSF at the cutout center.",
101 )
102 star_info: ExtendedPsfCandidateInfo = Field(description="Information about the star in the cutout.")
103
104 def deserialize(self, archive: InputArchive[Any], *, bbox: Box | None = None) -> ExtendedPsfCandidate:
105 masked_image = super().deserialize(archive, bbox=bbox)
106 psf_kernel_image = (
107 self.psf_kernel_image.deserialize(archive) if self.psf_kernel_image is not None else None
108 )
110 masked_image.image,
111 mask=masked_image.mask,
112 variance=masked_image.variance,
113 psf_kernel_image=psf_kernel_image,
114 star_info=self.star_info,
115 )._finish_deserialize(self)
116
117
118class ExtendedPsfCandidatesSerializationModel[P: BaseModel](ArchiveTree):
119 """A Pydantic model to represent serialized `ExtendedPsfCandidates`."""
120
121 candidates: list[ExtendedPsfCandidateSerializationModel[P]] = Field(
122 default_factory=list,
123 description="The candidate cutouts in this collection.",
124 )
125
126 def deserialize(self, archive: InputArchive[Any]) -> ExtendedPsfCandidates:
128 [candidate_model.deserialize(archive) for candidate_model in self.candidates],
129 metadata=self.metadata,
130 )
131
132
133class ExtendedPsfCandidate(MaskedImage):
134 """A cutout centered on a star, with associated metadata.
135
136 Parameters
137 ----------
138 image : `~lsst.images.Image`
139 The main data image for this star cutout.
140 mask : `~lsst.images.Mask`, optional
141 Bitmask that annotates the main image's pixels.
142 variance : `~lsst.images.Image`, optional
143 Per-pixel variance estimates for the image.
144 mask_schema : `~lsst.images.MaskSchema`, optional
145 Schema for the mask, required if a mask is provided.
146 projection : `~lsst.images.Projection`, optional
147 Projection to map pixels to the sky.
148 metadata : `dict` [`str`, `MetadataValue`], optional
149 Additional metadata to associate with this cutout.
150 psf_kernel_image : `~lsst.images.Image`, optional
151 Kernel image of the PSF at the cutout center.
152 star_info : `ExtendedPsfCandidateInfo`, optional
153 Information about the star in the cutout.
154
155 Attributes
156 ----------
157 psf_kernel_image : `~lsst.images.Image`
158 Kernel image of the PSF at the cutout center.
159 star_info : `ExtendedPsfCandidateInfo`
160 Information about the star in this cutout.
161 """
162
164 self,
165 image: Image,
166 *,
167 mask: Mask | None = None,
168 variance: Image | None = None,
169 mask_schema: MaskSchema | None = None,
170 projection: Projection | None = None,
171 metadata: dict[str, MetadataValue] | None = None,
172 psf_kernel_image: Image | None = None,
173 star_info: ExtendedPsfCandidateInfo | None = None,
174 ):
175 super().__init__(
176 image,
177 mask=mask,
178 variance=variance,
179 mask_schema=mask_schema,
180 projection=projection,
181 metadata=metadata,
182 )
183
184 self._psf_kernel_image = psf_kernel_image
186
187 def __getitem__(self, bbox: Box | EllipsisType) -> ExtendedPsfCandidate:
188 if bbox is ...:
189 return self
190 super().__getitem__(bbox)
191 return self._transfer_metadata(
193 # Projection propagates from the image.
194 self.image[bbox],
195 mask=self.mask[bbox],
196 variance=self.variance[bbox],
197 psf_kernel_image=self.psf_kernel_image,
198 star_info=self.star_info,
199 ),
200 bbox=bbox,
201 )
202
203 def __str__(self) -> str:
204 return f"ExtendedPsfCandidate({self.image!s}, {list(self.mask.schema.names)}, {self.star_info})"
205
206 def __repr__(self) -> str:
207 return (
208 f"ExtendedPsfCandidate({self.image!r}, mask_schema={self.mask.schema!r}, "
209 f"star_info={self.star_info!r})"
210 )
211
212 @property
213 def psf_kernel_image(self) -> Image:
214 """Kernel image of the PSF at the cutout center."""
215 if self._psf_kernel_image is None:
216 raise RuntimeError("No PSF kernel image is attached to this ExtendedPsfCandidate.")
217 return self._psf_kernel_image
218
219 @property
220 def star_info(self) -> ExtendedPsfCandidateInfo:
221 """Return the ExtendedPsfCandidateInfo associated with this star."""
222 return self._star_info
223
224 def copy(self) -> ExtendedPsfCandidate:
225 """Deep-copy the star cutout, metadata, and star info."""
226 return self._transfer_metadata(
228 image=self._image.copy(),
229 mask=self._mask.copy(),
230 variance=self._variance.copy(),
231 psf_kernel_image=self._psf_kernel_image,
232 star_info=self._star_info.model_copy(),
233 ),
234 copy=True,
235 )
236
237 def serialize(self, archive: OutputArchive[Any]) -> ExtendedPsfCandidateSerializationModel:
238 masked_image_model = super().serialize(archive)
239 serialized_psf_kernel_image = (
240 archive.serialize_direct(
241 "psf_kernel_image",
242 functools.partial(self._psf_kernel_image.serialize, save_projection=False),
243 )
244 if self._psf_kernel_image is not None
245 else None
246 )
248 **masked_image_model.model_dump(),
249 psf_kernel_image=serialized_psf_kernel_image,
250 star_info=self.star_info,
251 )
252
253 @staticmethod
254 def _get_archive_tree_type[P: BaseModel](
255 pointer_type: type[P],
256 ) -> type[ExtendedPsfCandidateSerializationModel[P]]:
257 return ExtendedPsfCandidateSerializationModel[pointer_type]
258
259
260class ExtendedPsfCandidates(Sequence[ExtendedPsfCandidate]):
261 """A collection of star cutouts.
262
263 Parameters
264 ----------
265 candidates : `Iterable` [`ExtendedPsfCandidate`]
266 Collection of `ExtendedPsfCandidate` instances.
267 metadata : `dict` [`str`, `MetadataValue`], optional
268 Global metadata associated with the collection.
269
270 Attributes
271 ----------
272 metadata : `dict` [`str`, `MetadataValue`]
273 Global metadata associated with the collection.
274 ref_id_map : `dict` [`int`, `ExtendedPsfCandidate`]
275 A mapping from reference IDs to `ExtendedPsfCandidate` objects.
276 Only includes candidates with valid reference IDs.
277 """
278
280 self,
281 candidates: Sequence[ExtendedPsfCandidate],
282 metadata: dict[str, MetadataValue] | None = None,
283 ):
284 self._candidates = list(candidates)
285 self._metadata = {} if metadata is None else dict(metadata)
286 self._ref_id_map = {
287 candidate.star_info.ref_id: candidate
288 for candidate in self
289 if candidate.star_info.ref_id is not None
290 }
291
292 def __len__(self):
293 return len(self._candidates)
294
295 def __getitem__(self, index):
296 if isinstance(index, slice):
297 return ExtendedPsfCandidates(self._candidates[index], metadata=self._metadata)
298 return self._candidates[index]
299
300 def __iter__(self):
301 return iter(self._candidates)
302
303 def __str__(self) -> str:
304 return f"ExtendedPsfCandidates(length={len(self)})"
305
306 __repr__ = __str__
307
308 @property
309 def metadata(self):
310 """Return the collection's global metadata as a dict."""
311 return self._metadata
312
313 @property
314 def ref_id_map(self):
315 """Map reference IDs to `ExtendedPsfCandidate` objects."""
316 return self._ref_id_map
317
318 @classmethod
319 def read_fits(cls, url: ResourcePathExpression) -> ExtendedPsfCandidates:
320 """Read a collection from a FITS file.
321
322 Parameters
323 ----------
324 url
325 URL of the file to read; may be any type supported by
326 `lsst.resources.ResourcePath`.
327 """
328 return fits.read(cls, url).deserialized
329
330 def write_fits(self, filename: str) -> None:
331 """Write the collection to a FITS file.
332
333 Parameters
334 ----------
335 filename
336 Name of the file to write to. Must not already exist.
337 """
338 fits.write(self, filename)
339
340 def serialize(self, archive: OutputArchive[Any]) -> ExtendedPsfCandidatesSerializationModel:
342 candidates=[
343 archive.serialize_direct(f"candidate_{index}", candidate.serialize)
344 for index, candidate in enumerate(self._candidates)
345 ],
346 metadata=self._metadata,
347 )
348
349 @staticmethod
350 def _get_archive_tree_type[P: BaseModel](
351 pointer_type: type[P],
352 ) -> type[ExtendedPsfCandidatesSerializationModel[P]]:
353 return ExtendedPsfCandidatesSerializationModel[pointer_type]
ExtendedPsfCandidateSerializationModel serialize(self, OutputArchive[Any] archive)
__init__(self, Image image, *, Mask|None mask=None, Image|None variance=None, MaskSchema|None mask_schema=None, Projection|None projection=None, dict[str, MetadataValue]|None metadata=None, Image|None psf_kernel_image=None, ExtendedPsfCandidateInfo|None star_info=None)
ExtendedPsfCandidatesSerializationModel serialize(self, OutputArchive[Any] archive)
__init__(self, Sequence[ExtendedPsfCandidate] candidates, dict[str, MetadataValue]|None metadata=None)
ExtendedPsfCandidate deserialize(self, InputArchive[Any] archive, *, Box|None bbox=None)