Coverage for python / lsst / cell_coadds / _multiple_cell_coadd.py: 38%
91 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 18:39 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 18:39 +0000
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__ = ("MultipleCellCoadd",)
26from collections.abc import Iterable, Set
27from typing import TYPE_CHECKING
29from lsst.geom import Box2I
31from ._common_components import CommonComponents, CommonComponentsProperties
32from ._exploded_coadd import ExplodedCoadd
33from ._grid_container import GridContainer
34from ._single_cell_coadd import SingleCellCoadd
35from ._stitched_coadd import StitchedCoadd
36from ._uniform_grid import UniformGrid
38if TYPE_CHECKING: 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true
39 from lsst.daf.base import PropertySet
40 from lsst.geom import Extent2I
43class MultipleCellCoadd(CommonComponentsProperties):
44 """A data structure for coadds built from many overlapping cells.
46 Notes
47 -----
48 `MultipleCellCoadd` is designed to be used both by measurement algorithms
49 that are able to take advantage of cell boundaries and overlap regions
50 (which can use the ``.cells`` attribute to access `SingleCellCoadd` objects
51 directly) and measurement algorithms that just want one image and don't
52 care (or don't care much) about discontinuities (which can use `stitch` to
53 obtain such an image).
55 Indexing with `Box2I` yields a `MultipleCellCoadd` view containing just the
56 cells that overlap that region.
57 """
59 def __init__(
60 self,
61 cells: Iterable[SingleCellCoadd],
62 grid: UniformGrid,
63 outer_cell_size: Extent2I,
64 psf_image_size: Extent2I,
65 *,
66 common: CommonComponents,
67 inner_bbox: Box2I | None = None,
68 ):
69 self._grid = grid
70 self._outer_cell_size = outer_cell_size
71 self._psf_image_size = psf_image_size
72 self._common = common
73 cells_builder = GridContainer[SingleCellCoadd](self._grid.shape)
74 self._mask_fraction_names: set[str] = set()
76 for cell in cells:
77 index = cell.identifiers.cell
78 cells_builder[index] = cell
79 cell_bbox = self._grid.bbox_of(index)
80 if not cell.outer.bbox.contains(cell_bbox):
81 raise ValueError(
82 f"Cell at index {index} has outer bbox {cell.outer.bbox}, "
83 f"which does not contain {self._grid.bbox_of(index)}."
84 )
85 if not cell_bbox.contains(cell.inner.bbox):
86 raise ValueError(
87 f"Cell at index {index} has inner bbox {cell.inner.bbox}, "
88 f"which is not contained by {self._grid.bbox_of(index)}."
89 )
90 if cell.outer.bbox.getDimensions() != self._outer_cell_size:
91 raise ValueError(
92 f"Cell at index {index} has outer dimensions {cell.outer.bbox.getDimensions()}, "
93 f"but coadd expects {self._outer_cell_size}."
94 )
95 if cell.psf_image.getDimensions() != self._psf_image_size:
96 raise ValueError(
97 f"Cell at index {index} has PSF image with dimensions {cell.psf_image.getDimensions()}, "
98 f"but coadd expects {self._psf_image_size}."
99 )
101 self._cells = cells_builder
102 n_noise_realizations = {len(cell.outer.noise_realizations) for cell in self._cells.values()}
103 self._n_noise_realizations = n_noise_realizations.pop()
104 if n_noise_realizations:
105 n_noise_realizations.add(self._n_noise_realizations)
106 raise ValueError(
107 f"Inconsistent number of noise realizations ({n_noise_realizations}) between cells."
108 )
110 # Finish the construction without relying on the first and last of
111 # self._cells so we can construct an instance with partial list.
112 indices = list(cells_builder.indices())
113 max_inner_bbox = Box2I(
114 grid.bbox_of(indices[0]).getMin(),
115 grid.bbox_of(indices[-1]).getMax(),
116 )
118 if inner_bbox is None:
119 inner_bbox = max_inner_bbox
120 elif not max_inner_bbox.contains(inner_bbox):
121 raise ValueError(
122 f"Requested inner bounding box {inner_bbox} is not fully covered by these "
123 f"cells (bbox is {max_inner_bbox})."
124 )
125 self._inner_bbox = inner_bbox
127 @property
128 def cells(self) -> GridContainer[SingleCellCoadd]:
129 """The grid of single-cell coadds, indexed by (y, x)."""
130 return self._cells
132 @property
133 def n_noise_realizations(self) -> int:
134 """The number of noise realizations cells are guaranteed to have."""
135 return self._n_noise_realizations
137 @property
138 def mask_fraction_names(self) -> Set[str]:
139 """The names of all mask planes whose fractions were propagated in any
140 cell.
142 Cells that do not have a mask fraction for a particular name may be
143 assumed to have the fraction for that mask plane uniformly zero.
144 """
145 return self._mask_fraction_names
147 @property
148 def grid(self) -> UniformGrid:
149 """Object that defines the inner geometry for all cells."""
150 return self._grid
152 @property
153 def outer_cell_size(self) -> Extent2I:
154 """Dimensions of the outer region of each cell."""
155 return self._outer_cell_size
157 @property
158 def psf_image_size(self) -> Extent2I:
159 """Dimensions of PSF model images."""
160 return self._psf_image_size
162 @property
163 def outer_bbox(self) -> Box2I:
164 """The rectangular region fully covered by all cell outer bounding
165 boxes.
166 """
167 return self.grid.bbox_with_padding
169 @property
170 def inner_bbox(self) -> Box2I:
171 """The rectangular region fully covered by all cell inner bounding
172 boxes.
173 """
174 return self._inner_bbox
176 @property
177 def common(self) -> CommonComponents:
178 # Docstring inherited.
179 return self._common
181 def stitch(self, bbox: Box2I | None = None) -> StitchedCoadd:
182 """Return a contiguous (but in general discontinuous) coadd by
183 stitching together inner cells.
185 Parameters
186 ----------
187 bbox : `Box2I`, optional
188 Region for the returned coadd; default is ``self.inner_bbox``.
190 Returns
191 -------
192 stitched : `StitchedCellCoadd`
193 Contiguous coadd covering the given area. Each image plane is
194 actually constructed when first accessed, not when this method
195 is called.
196 """
197 # In the future, stitching algorithms that apply ramps to smooth
198 # discontinuities may also be provided; we'd implement that by having
199 # this return different types (from a common ABC), perhaps dispatched
200 # by an enum.
201 return StitchedCoadd(self, bbox=bbox)
203 def explode(self, pad_psfs_with: float | None = None) -> ExplodedCoadd:
204 """Return a coadd whose image planes stitch together the outer regions
205 of each cell, duplicating pixels in the overlap regions.
207 Parameters
208 ----------
209 pad_psfs_with : `float` or None, optional
210 A floating-point value to pad PSF images with so each PSF-image
211 cell has the same dimensions as the image (outer) cell it
212 corresponds to. If `None`, PSF images will not be padded and the
213 full PSF image will generally be smaller than the exploded image it
214 corresponds to.
216 Returns
217 -------
218 exploded : `ExplodedCoadd`
219 Exploded version of the coadd.
220 """
221 return ExplodedCoadd(self, pad_psfs_with=pad_psfs_with)
223 @classmethod
224 def read_fits(cls, filename: str) -> MultipleCellCoadd:
225 """Read a MultipleCellCoadd from a FITS file.
227 Parameters
228 ----------
229 filename : `str`
230 The path to the FITS file to read.
232 Returns
233 -------
234 cell_coadd : `MultipleCellCoadd`
235 The MultipleCellCoadd object read from the FITS file.
236 """
237 from ._fits import CellCoaddFitsReader # Avoid circular import.
239 reader = CellCoaddFitsReader(filename)
240 return reader.readAsMultipleCellCoadd()
242 @classmethod
243 def readFits(cls, *args, **kwargs) -> MultipleCellCoadd: # type: ignore[no-untyped-def]
244 """Alias to `read_fits` method.
246 Notes
247 -----
248 This method exists for compatability with the rest of the codebase.
249 The presence of this method allows for reading in via
250 `lsst.obs.base.formatters.FitsGenericFormatter`.
251 Whenever possible, use `read_fits` instead, since this method may be
252 deprecated in the near future.
253 """
254 return cls.read_fits(*args, **kwargs)
256 def write_fits(self, filename: str, overwrite: bool = False, metadata: PropertySet | None = None) -> None:
257 """Write the coadd as a FITS file.
259 Parameters
260 ----------
261 filename : `str`
262 The path to the FITS file to write.
263 overwrite : `bool`, optional
264 Whether to overwrite an existing file?
265 metadata : `~lsst.daf.base.PropertySet`, optional
266 Additional metadata to write to the FITS header.
267 """
268 from ._fits import writeMultipleCellCoaddAsFits # Avoid circular import.
270 writeMultipleCellCoaddAsFits(self, filename, overwrite=overwrite, metadata=metadata)
272 def writeFits(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
273 """Alias to `write_fits` method.
275 Notes
276 -----
277 This method exists for compatibility with the rest of the codebase.
278 The presence of this method allows for persistence via
279 `lsst.obs.base.formatters.FitsGenericFormatter`.
280 Whenever possible, use `write_fits` instead, since this method may be
281 deprecated in the near future.
282 """
283 self.write_fits(*args, **kwargs)