Coverage for python/lsst/cell_coadds/_stitched_coadd.py: 30%
106 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-14 02:24 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-14 02:24 -0700
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 asExposure(self) -> ExposureF:
157 """Return an `lsst.afw.image.Exposure` view of this piecewise image."""
158 result = ExposureF(self.asMaskedImage())
159 # Exposure components derived from "common" components are all simple.
160 result.setWcs(self._cell_coadd.wcs)
161 result.setFilter(FilterLabel(band=self.band))
162 if self.units is CoaddUnits.nJy:
163 result.setPhotoCalib(PhotoCalib(1.0))
164 result.metadata["BUNIT"] = "nJy"
166 # We can't do result.setId here, because:
167 #
168 # - we don't know whether this should be a packed tract+patch+band ID
169 # or just a tract+patch ID;
170 #
171 # - we don't know how to pack the information we have anyway.
172 #
173 # Maybe DM-31924 will provide a solution to at least the latter.
174 # result.setId(self._cell_coadd.identifiers.patch)
176 # We could add CoaddInputs here, but without WCS, PSF, etc in them;
177 # it's not clear that's good enough or even useful, given that the cell
178 # provide a much more clean view of what the inputs are at any given
179 # point.
181 # PSF is the first of many components that need piecewise
182 # implementations. More to do here for at least aperture corrections
183 # and transmission curves.
184 result.setPsf(self.psf)
186 return result
188 @property
189 def ap_corr_map(self) -> StitchedCoaddApCorrMap:
190 """Stitch the aperture correction maps from the cell coadd.
192 This converts the aperture correction maps from each cell into a single
193 `ApCorrMap` that quacks like `lsst.afw.image.ApCorrMap`. The resulting
194 object has the fields to correct as keys and a
195 `StitchedApertureCorrection` for each field.
197 Notes
198 -----
199 These cannot be attached to an `~lsst.afw.image.Exposure` object.
200 """
201 if self._ap_corr_map is None:
202 ap_corr_map: dict[str, StitchedApertureCorrection] = {}
203 field_names = self._cell_coadd.cells.arbitrary.aperture_corrected_algorithms
204 for field_name in field_names:
205 gc = GridContainer[float](shape=self.grid.shape)
206 for scc in self._cell_coadd.cells.values():
207 gc[scc.identifiers.cell] = scc.aperture_correction_map[field_name]
208 ap_corr_map[field_name] = StitchedApertureCorrection(self.grid, gc)
210 self._ap_corr_map = ap_corr_map
212 return self._ap_corr_map
214 @property
215 def ccds(self) -> StitchedExposureCatalog:
216 """An ExposureCatalog-like object that contains the information about
217 the ``ObservationIdentifiers`` at any point on the coadd. It can also
218 be used as an iterable to iterate over the unique (visit, detector)
219 combinations.
220 """
221 if self._ccds is None:
222 gc = GridContainer[tuple[ObservationIdentifiers, ...]](shape=self.grid.shape)
223 for idx, scc in self._cell_coadd.cells.items():
224 gc[idx] = scc.inputs
225 self._ccds = StitchedExposureCatalog(self.grid, gc)
227 return self._ccds
229 @property
230 def visits(self) -> StitchedExposureCatalog:
231 """An ExposureCatalog-like object that contains the information about
232 the ``ObservationIdentifiers`` at any point on the coadd. It can also
233 be used as an iterable to iterate over the unique visits.
234 """
235 if self._visits is None:
236 gc = GridContainer[tuple[ObservationIdentifiers, ...]](shape=self.grid.shape)
237 for idx, scc in self._cell_coadd.cells.items():
238 gc[idx] = tuple(
239 sorted({dataclasses.replace(ccd_input, detector=-1) for ccd_input in scc.inputs})
240 )
241 self._visits = StitchedExposureCatalog(self.grid, gc)
243 return self._visits