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