Coverage for python/lsst/cell_coadds/_stitched_coadd.py: 30%
109 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 02:13 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 02:13 -0800
1# This file is part of cell_coadds.
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/>.
22from __future__ import annotations
24__all__ = ("StitchedCoadd",)
26import dataclasses
27from collections.abc import Iterator, Set
28from functools import partial
29from typing import TYPE_CHECKING
31from lsst.afw.image import ExposureF, FilterLabel, PhotoCalib
32from lsst.geom import Box2I, Point2I
34from ._common_components import CoaddUnits, CommonComponents, CommonComponentsProperties
35from ._grid_container import GridContainer
36from ._identifiers import ObservationIdentifiers
37from ._image_planes import ImagePlanes, ViewImagePlanes
38from ._stitched_aperture_correction import StitchedApertureCorrection
39from ._stitched_exposure_catalog import StitchedExposureCatalog
40from ._stitched_image_planes import StitchedImagePlanes
41from ._stitched_psf import StitchedPsf
42from ._uniform_grid import UniformGrid
43from .typing_helpers import StitchedCoaddApCorrMap
45if TYPE_CHECKING: 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true
46 from ._multiple_cell_coadd import MultipleCellCoadd
49class StitchedCoadd(StitchedImagePlanes, CommonComponentsProperties):
50 """A lazy-evaluation coadd that stitches together images from adjacent
51 cells.
53 Parameters
54 ----------
55 cell_coadd : `MultipleCellCoadd`
56 Cell-based coadd to stitch together.
57 bbox : `Box2I`, optional
58 The region over which a contiguous coadd is desired. Defaults to
59 ``cell_coadd.outer_bbox``.
61 Notes
62 -----
63 This class simply inserts subimages from each cell into the full image,
64 doing so when an attribute is first accessed to avoid stitching together
65 planes that may never be accessed.
67 A `StitchedCoadd` cannot be serialized in FITS format directly. Instead,
68 the recommended way is to serialize the `MultipleCellCoadd` instance that
69 was used to construct the object and reconstruct the `StitchedCoadd` by
70 calling the `stitch` method on it. A less recommended way is to call the
71 `asExposure` method to get an `lsst.afw.image.Exposure` object and persist
72 that to the disk.
73 """
75 def __init__(self, cell_coadd: MultipleCellCoadd, *, bbox: Box2I | None = None):
76 super().__init__()
77 if bbox is None:
78 bbox = cell_coadd.outer_bbox
79 elif not cell_coadd.outer_bbox.contains(bbox):
80 raise ValueError(
81 f"Cell coadd inner bounding box {cell_coadd.outer_bbox} does not "
82 f"contain stitch target area {bbox}."
83 )
84 self._bbox = bbox
85 self._cell_coadd = cell_coadd
86 self._psf: StitchedPsf | None = None
87 self._ap_corr_map: StitchedCoaddApCorrMap | None = None
88 self._ccds: StitchedExposureCatalog | None = None
89 self._visits: StitchedExposureCatalog | None = None
90 self._common = cell_coadd.common
92 @property
93 def bbox(self) -> Box2I:
94 # Docstring inherited.
95 return self._bbox
97 @property
98 def grid(self) -> UniformGrid:
99 """Object that defines the piecewise grid (of inner cell regions) that
100 this object stitches together.
102 This may include cells outside the region covered by these image
103 planes.
104 """
105 return self._cell_coadd.grid
107 def _iter_cell_planes(self) -> Iterator[ImagePlanes]:
108 # Docstring inherited.
109 x_max, y_max = self.grid.index(self.bbox.getMax())
111 for cell in self._cell_coadd.cells.values():
112 bbox = cell.inner.bbox
113 if cell.identifiers.cell.x == 0:
114 # This is a special case for the first column of cells.
115 bbox.include(Point2I(cell.outer.bbox.beginX, cell.outer.bbox.centerY))
116 elif cell.identifiers.cell.x == x_max:
117 # This is a special case for the last column of cells.
118 bbox.include(Point2I(cell.outer.bbox.endX, cell.outer.bbox.centerY))
120 if cell.identifiers.cell.y == 0:
121 # This is a special case for the last row of cells.
122 bbox.include(Point2I(cell.outer.bbox.centerX, cell.outer.bbox.beginY))
123 elif cell.identifiers.cell.y == y_max:
124 # This is a special case for the first row of cells.
125 bbox.include(Point2I(cell.outer.bbox.centerX, cell.outer.bbox.endY))
127 bbox.clip(cell.outer.bbox)
128 make_view = partial(cell.make_view, bbox=bbox)
129 yield ViewImagePlanes(cell.outer, bbox=bbox, make_view=make_view)
131 @property
132 def n_noise_realizations(self) -> int:
133 # Docstring inherited.
134 return self._cell_coadd.n_noise_realizations
136 @property
137 def mask_fraction_names(self) -> Set[str]:
138 # Docstring inherited.
139 return self._cell_coadd.mask_fraction_names
141 @property
142 def psf(self) -> StitchedPsf:
143 """The piecewise PSF of this image."""
144 if self._psf is None:
145 self._psf = StitchedPsf(
146 self._cell_coadd.cells.rebuild_transformed(lambda cell: cell.psf_image.convertD()),
147 self._cell_coadd.grid,
148 )
149 return self._psf
151 @property
152 def common(self) -> CommonComponents:
153 # Docstring inherited.
154 return self._cell_coadd.common
156 def set_cell_edges(self, *, edge_width: int = 1, edge_mask_name: str = "CELL_EDGE") -> None:
157 """Set a mask bit indicating the inner cell edges.
159 Parameters
160 ----------
161 edge_width : `int`, optional
162 The width of the edge region to flag, in pixels.
163 edge_mask_name : `str`, optional
164 The name of the mask plane to add for the edge region.
165 """
166 for cell in self._cell_coadd.cells.values():
167 cell._set_cell_edges(edge_width=edge_width, edge_mask_name=edge_mask_name)
169 def asExposure(
170 self,
171 *,
172 noise_index: int | None = None,
173 ) -> ExposureF:
174 """Return an `lsst.afw.image.Exposure` view of this piecewise image.
176 Parameters
177 ----------
178 noise_index : `int` or `None`, optional
179 If specified, return an exposure containing the specified noise
180 realization instead of the data image.
182 Returns
183 -------
184 exposure : `lsst.afw.image.ExposureF`
185 The stitched exposure.
186 """
187 result = ExposureF(self.asMaskedImage(noise_index=noise_index))
188 # Exposure components derived from "common" components are all simple.
189 result.setWcs(self._cell_coadd.wcs)
190 result.setFilter(FilterLabel(band=self.band))
191 if self.units is CoaddUnits.nJy:
192 result.setPhotoCalib(PhotoCalib(1.0))
193 result.metadata["BUNIT"] = "nJy"
195 # We can't do result.setId here, because:
196 #
197 # - we don't know whether this should be a packed tract+patch+band ID
198 # or just a tract+patch ID;
199 #
200 # - we don't know how to pack the information we have anyway.
201 #
202 # Maybe DM-31924 will provide a solution to at least the latter.
203 # result.setId(self._cell_coadd.identifiers.patch)
205 # We could add CoaddInputs here, but without WCS, PSF, etc in them;
206 # it's not clear that's good enough or even useful, given that the cell
207 # provide a much more clean view of what the inputs are at any given
208 # point.
210 # PSF is the first of many components that need piecewise
211 # implementations. More to do here for at least aperture corrections
212 # and transmission curves.
213 result.setPsf(self.psf)
215 return result
217 @property
218 def ap_corr_map(self) -> StitchedCoaddApCorrMap:
219 """Stitch the aperture correction maps from the cell coadd.
221 This converts the aperture correction maps from each cell into a single
222 `ApCorrMap` that quacks like `lsst.afw.image.ApCorrMap`. The resulting
223 object has the fields to correct as keys and a
224 `StitchedApertureCorrection` for each field.
226 Notes
227 -----
228 These cannot be attached to an `~lsst.afw.image.Exposure` object.
229 """
230 if self._ap_corr_map is None:
231 ap_corr_map: dict[str, StitchedApertureCorrection] = {}
232 field_names = self._cell_coadd.cells.arbitrary.aperture_corrected_algorithms
233 for field_name in field_names:
234 gc = GridContainer[float](shape=self.grid.shape)
235 for scc in self._cell_coadd.cells.values():
236 gc[scc.identifiers.cell] = scc.aperture_correction_map[field_name]
237 ap_corr_map[field_name] = StitchedApertureCorrection(self.grid, gc)
239 self._ap_corr_map = ap_corr_map
241 return self._ap_corr_map
243 @property
244 def ccds(self) -> StitchedExposureCatalog:
245 """An ExposureCatalog-like object that contains the information about
246 the ``ObservationIdentifiers`` at any point on the coadd. It can also
247 be used as an iterable to iterate over the unique (visit, detector)
248 combinations.
249 """
250 if self._ccds is None:
251 gc = GridContainer[tuple[ObservationIdentifiers, ...]](shape=self.grid.shape)
252 for idx, scc in self._cell_coadd.cells.items():
253 gc[idx] = tuple(scc.inputs.keys())
254 self._ccds = StitchedExposureCatalog(self.grid, gc)
256 return self._ccds
258 @property
259 def visits(self) -> StitchedExposureCatalog:
260 """An ExposureCatalog-like object that contains the information about
261 the ``ObservationIdentifiers`` at any point on the coadd. It can also
262 be used as an iterable to iterate over the unique visits.
263 """
264 if self._visits is None:
265 gc = GridContainer[tuple[ObservationIdentifiers, ...]](shape=self.grid.shape)
266 for idx, scc in self._cell_coadd.cells.items():
267 gc[idx] = tuple(
268 sorted({dataclasses.replace(ccd_input, detector=-1) for ccd_input in scc.inputs})
269 )
270 self._visits = StitchedExposureCatalog(self.grid, gc)
272 return self._visits