Coverage for tests/test_cell_coadd.py: 40%
136 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 15:24 -0700
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 15:24 -0700
1# This file is part of lsst-images.
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# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14import dataclasses
15import os
16import pickle
17from typing import Any
19import numpy as np
20import pytest
22from lsst.images import YX, Box, Interval, MaskPlane, get_legacy_deep_coadd_mask_planes
23from lsst.images.cells import CellCoadd, CellIJ
24from lsst.images.fits import FitsCompressionOptions
25from lsst.images.serialization import read_archive
26from lsst.images.tests import (
27 DP2_COADD_DATA_ID,
28 DP2_COADD_MISSING_CELL,
29 RoundtripFits,
30 RoundtripJson,
31 RoundtripNdf,
32 assert_cell_coadds_equal,
33 assert_images_equal,
34 assert_masked_images_equal,
35 assert_psfs_equal,
36 check_bounds_contains_broadcasting,
37 compare_cell_coadd_to_legacy,
38 compare_masked_image_to_legacy,
39 compare_psf_to_legacy,
40 compare_sky_projection_to_legacy_wcs,
41)
43try:
44 import h5py # noqa: F401
46 HAVE_H5PY = True
47except ImportError:
48 HAVE_H5PY = False
50try:
51 import lsst.afw.image # noqa: F401
52 from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd
54 HAVE_LEGACY = True
55except ImportError:
56 HAVE_LEGACY = False
57 type LegacyMultipleCellCoadd = Any # type: ignore[no-redef]
59EXTERNAL_DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None)
60LOCAL_DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
62skip_no_h5py = pytest.mark.skipif(not HAVE_H5PY, reason="h5py is not installed")
63skip_no_legacy = pytest.mark.skipif(not HAVE_LEGACY, reason="lsst.afw (etc) could not be imported.")
66@dataclasses.dataclass
67class _LegacyTestData:
68 """A struct holding test data loaded from EXTERNAL_DATA_DIR."""
70 filename: str
71 tract_bbox: Box
72 legacy_cell_coadd: LegacyMultipleCellCoadd
73 cell_coadd: CellCoadd
74 plane_map: dict[str, MaskPlane] = dataclasses.field(default_factory=get_legacy_deep_coadd_mask_planes)
76 def make_psf_points(self, bbox: Box | None = None) -> YX[np.ndarray]:
77 """Create random PSF sample points within the given bbox."""
78 if bbox is None:
79 bbox = self.cell_coadd.bbox
80 rng = np.random.default_rng(44)
81 xc, yc = np.meshgrid(
82 np.arange(
83 bbox.x.start + self.cell_coadd.grid.cell_shape.x * 0.5,
84 bbox.x.stop,
85 self.cell_coadd.grid.cell_shape.x,
86 ),
87 np.arange(
88 bbox.y.start + self.cell_coadd.grid.cell_shape.y * 0.5,
89 bbox.y.stop,
90 self.cell_coadd.grid.cell_shape.y,
91 ),
92 )
93 return YX(
94 y=yc.ravel() + rng.uniform(-0.4, 0.4, size=yc.size),
95 x=xc.ravel() + rng.uniform(-0.4, 0.4, size=xc.size),
96 )
99@pytest.fixture(scope="session")
100def legacy_test_data() -> _LegacyTestData:
101 """Return a struct of CellCoadd loaded from legacy test data.
103 Skips if ``TESTDATA_IMAGES_DIR`` is not set or if ``lsst.cell_coadds``
104 cannot be imported.
105 """
106 if EXTERNAL_DATA_DIR is None: 106 ↛ 108line 106 didn't jump to line 108 because the condition on line 106 was always true
107 pytest.skip("TESTDATA_IMAGES_DIR is not in the environment.")
108 try:
109 from lsst.cell_coadds import MultipleCellCoadd
110 except ImportError:
111 pytest.skip("lsst.cell_coadds could not be imported.")
112 filename = os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "deep_coadd_cell_predetection.fits")
113 plane_map = get_legacy_deep_coadd_mask_planes()
114 legacy_cell_coadd = MultipleCellCoadd.read_fits(filename)
115 with open(os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "skyMap.pickle"), "rb") as stream:
116 skymap = pickle.load(stream)
117 cell_coadd = CellCoadd.from_legacy(
118 legacy_cell_coadd,
119 plane_map=plane_map,
120 tract_info=skymap[DP2_COADD_DATA_ID["tract"]],
121 )
122 return _LegacyTestData(
123 filename=filename,
124 tract_bbox=Box.from_legacy(skymap[DP2_COADD_DATA_ID["tract"]].getBBox()),
125 legacy_cell_coadd=legacy_cell_coadd,
126 cell_coadd=cell_coadd,
127 )
130@pytest.fixture
131def minified_cell_coadd() -> CellCoadd:
132 """Return a tiny CellCoadd from JSON data stored in this package."""
133 path = os.path.join(LOCAL_DATA_DIR, "schema_v1", "legacy", "cell_coadd.json")
134 return read_archive(path, CellCoadd)
137def make_subbox(full_bbox: Box) -> Box:
138 """Make a box that's useful for nontrivial subimage tests.
140 This box only overlaps (but does not fully cover) the middle 2 (of 4)
141 cells in y, while covering exactly the last column of cells in x. It does
142 not cover the missing cell.
143 """
144 return Box.factory[
145 full_bbox.y.start + 252 : full_bbox.y.stop - 175,
146 full_bbox.x.stop - 150 : full_bbox.x.stop,
147 ]
150def test_from_legacy(legacy_test_data: _LegacyTestData) -> None:
151 """Test constructing a CellCoadd by converting a legacy
152 ``MultipleCellCoadd``.
153 """
154 assert legacy_test_data.cell_coadd.bounds.missing == {CellIJ(**DP2_COADD_MISSING_CELL)}
155 assert legacy_test_data.cell_coadd.bbox == Box.factory[12900:13500, 9600:10050]
156 compare_cell_coadd_to_legacy(
157 legacy_test_data.cell_coadd,
158 legacy_test_data.legacy_cell_coadd,
159 tract_bbox=legacy_test_data.tract_bbox,
160 plane_map=legacy_test_data.plane_map,
161 psf_points=legacy_test_data.make_psf_points(),
162 )
165def test_roundtrip(legacy_test_data: _LegacyTestData) -> None:
166 """Test that a CellCoadd roundtrips through FITS."""
167 with RoundtripFits(legacy_test_data.cell_coadd, "CellCoadd") as roundtrip:
168 # Check a subimage read (no component arg — does not trigger a skip).
169 subbox = Box.factory[
170 legacy_test_data.cell_coadd.bbox.y.start + 252 : legacy_test_data.cell_coadd.bbox.y.stop - 175,
171 legacy_test_data.cell_coadd.bbox.x.stop - 150 : legacy_test_data.cell_coadd.bbox.x.stop,
172 ]
173 subimage = roundtrip.get(bbox=subbox)
174 assert_masked_images_equal(subimage, legacy_test_data.cell_coadd[subbox], expect_view=False)
175 with roundtrip.inspect() as fits:
176 for extname in ["IMAGE", "MASK", "VARIANCE", "MASK_FRACTIONS/REJECTED"] + [
177 f"NOISE_REALIZATIONS/{n}" for n in range(len(legacy_test_data.cell_coadd.noise_realizations))
178 ]:
179 assert fits[extname].header["ZTILE1"] == legacy_test_data.cell_coadd.grid.cell_shape.x
180 assert fits[extname].header["ZTILE2"] == legacy_test_data.cell_coadd.grid.cell_shape.y
181 # Fixture self-consistency: bbox and missing-cell set are as expected.
182 assert legacy_test_data.cell_coadd.bounds.missing == {CellIJ(**DP2_COADD_MISSING_CELL)}
183 assert legacy_test_data.cell_coadd.bbox == Box.factory[12900:13500, 9600:10050]
184 # Full round-trip fidelity.
185 assert_cell_coadds_equal(roundtrip.result, legacy_test_data.cell_coadd, expect_view=False)
186 compare_cell_coadd_to_legacy(
187 roundtrip.result,
188 legacy_test_data.legacy_cell_coadd,
189 tract_bbox=legacy_test_data.tract_bbox,
190 plane_map=legacy_test_data.plane_map,
191 psf_points=legacy_test_data.make_psf_points(),
192 )
195def test_roundtrip_components(legacy_test_data: _LegacyTestData) -> None:
196 """Test component and subimage reads.
198 This test will be skipped if `lsst.daf.butler` is not available instead of
199 falling back to non-butler I/O, which is why we don't want to merge it
200 with `test_roundtrip`.
201 """
202 with RoundtripFits(legacy_test_data.cell_coadd, "CellCoadd") as roundtrip:
203 subbox = make_subbox(legacy_test_data.cell_coadd.bbox)
204 subpsf = roundtrip.get("psf", bbox=subbox)
205 assert subpsf.bounds.bbox == Box(
206 y=Interval.factory[
207 legacy_test_data.cell_coadd.bbox.y.start + 150 : legacy_test_data.cell_coadd.bbox.y.stop - 150
208 ],
209 x=subbox.x,
210 )
211 assert_psfs_equal(
212 subpsf,
213 legacy_test_data.cell_coadd.psf,
214 points=legacy_test_data.make_psf_points(subbox),
215 )
216 assert roundtrip.get("bbox") == legacy_test_data.cell_coadd.bbox
217 alternates = {
218 k: roundtrip.get(k)
219 for k in [
220 "sky_projection",
221 "image",
222 "mask",
223 "variance",
224 "masked_image",
225 "psf",
226 "aperture_corrections",
227 "provenance",
228 "backgrounds",
229 "bbox",
230 ]
231 }
232 # Read all the components at once.
233 all_components = roundtrip.get("components")
234 assert set(all_components) == set(alternates) - {"masked_image"}
235 assert all_components["bbox"] == alternates["bbox"]
236 assert_psfs_equal(all_components["psf"], alternates["psf"])
237 assert_images_equal(all_components["image"], alternates["image"])
239 backgrounds = roundtrip.get("backgrounds")
240 assert backgrounds.keys() == set()
241 assert backgrounds.subtracted is None
243 compare_cell_coadd_to_legacy(
244 roundtrip.result,
245 legacy_test_data.legacy_cell_coadd,
246 tract_bbox=legacy_test_data.tract_bbox,
247 plane_map=legacy_test_data.plane_map,
248 alternates=alternates,
249 psf_points=legacy_test_data.make_psf_points(),
250 )
253def test_fits_compression(legacy_test_data: _LegacyTestData) -> None:
254 """Test lossy FITS compression produces the expected headers."""
255 with RoundtripFits(
256 legacy_test_data.cell_coadd,
257 storage_class="CellCoadd",
258 recipe="lossy16",
259 compression_options={
260 "image": FitsCompressionOptions.LOSSY,
261 "variance": FitsCompressionOptions.LOSSY,
262 },
263 ) as roundtrip:
264 with roundtrip.inspect() as fits:
265 for extname in ["IMAGE", "MASK", "VARIANCE", "MASK_FRACTIONS/REJECTED"] + [
266 f"NOISE_REALIZATIONS/{n}" for n in range(len(legacy_test_data.cell_coadd.noise_realizations))
267 ]:
268 assert fits[extname].header["ZTILE1"] == legacy_test_data.cell_coadd.grid.cell_shape.x
269 assert fits[extname].header["ZTILE2"] == legacy_test_data.cell_coadd.grid.cell_shape.y
270 if extname == "MASK" or extname.startswith("MASK_FRACTIONS"):
271 assert fits[extname].header["ZCMPTYPE"] == "GZIP_2"
272 else:
273 assert fits[extname].header["ZCMPTYPE"] == "RICE_1"
274 assert fits[extname].header["ZQUANTIZ"] == "SUBTRACTIVE_DITHER_2"
277def test_json_roundtrip(legacy_test_data: _LegacyTestData) -> None:
278 """Verify a CellCoadd round-trips correctly through the JSON archive."""
279 with RoundtripJson(legacy_test_data.cell_coadd) as roundtrip:
280 pass
281 assert_cell_coadds_equal(roundtrip.result, legacy_test_data.cell_coadd, expect_view=False)
284def test_to_legacy_cell_coadd(legacy_test_data: _LegacyTestData) -> None:
285 """Verify converting a CellCoadd back into a legacy MultipleCellCoadd."""
286 legacy_cell_coadd = legacy_test_data.cell_coadd.to_legacy_cell_coadd()
287 compare_cell_coadd_to_legacy(
288 legacy_test_data.cell_coadd,
289 legacy_cell_coadd,
290 tract_bbox=legacy_test_data.tract_bbox,
291 plane_map=legacy_test_data.plane_map,
292 psf_points=legacy_test_data.make_psf_points(),
293 )
294 with pytest.raises(
295 ValueError, match="MultipleCellCoadd requires its bounding box to lie on the cell grid."
296 ):
297 legacy_test_data.cell_coadd[make_subbox(legacy_test_data.cell_coadd.bbox)].to_legacy_cell_coadd()
300@skip_no_legacy
301def test_to_legacy(legacy_test_data: _LegacyTestData) -> None:
302 """Test converting a CellCoadd back into a legacy Exposure."""
303 legacy_exposure = legacy_test_data.cell_coadd.to_legacy()
304 assert legacy_exposure.getFilter().bandLabel == legacy_test_data.cell_coadd.band
305 assert Box.from_legacy(legacy_exposure.getBBox()) == legacy_test_data.cell_coadd.bbox
306 compare_masked_image_to_legacy(
307 legacy_test_data.cell_coadd,
308 legacy_exposure.maskedImage,
309 plane_map=legacy_test_data.plane_map,
310 expect_view=True,
311 )
312 compare_psf_to_legacy(
313 legacy_test_data.cell_coadd.psf,
314 legacy_exposure.getPsf(),
315 points=legacy_test_data.make_psf_points(),
316 expect_legacy_raise_on_out_of_bounds=True,
317 )
318 compare_sky_projection_to_legacy_wcs(
319 legacy_test_data.cell_coadd.sky_projection,
320 legacy_exposure.getWcs(),
321 legacy_test_data.cell_coadd.sky_projection.pixel_frame,
322 subimage_bbox=legacy_test_data.cell_coadd.bbox,
323 is_fits=True,
324 )
325 subbox = make_subbox(legacy_test_data.cell_coadd.bbox)
326 compare_masked_image_to_legacy(
327 legacy_test_data.cell_coadd[subbox],
328 legacy_test_data.cell_coadd[subbox].to_legacy().maskedImage,
329 plane_map=legacy_test_data.plane_map,
330 expect_view=True,
331 )
334@skip_no_h5py
335def test_ndf_roundtrip(legacy_test_data: _LegacyTestData) -> None:
336 """Test that CellCoadd round-trips through NDF."""
337 with RoundtripNdf(legacy_test_data.cell_coadd, "CellCoadd") as roundtrip:
338 assert_cell_coadds_equal(roundtrip.result, legacy_test_data.cell_coadd, expect_view=False)
341def test_cell_grid_bounds_contains_broadcasting(minified_cell_coadd: CellCoadd) -> None:
342 """Test that CellGridBounds.contains broadcasts like a numpy ufunc."""
343 assert minified_cell_coadd.bounds.missing, "fixture should retain a missing cell"
344 check_bounds_contains_broadcasting(minified_cell_coadd.bounds)
347def test_intersection_bounds_contains_broadcasting(minified_cell_coadd: CellCoadd) -> None:
348 """Test that IntersectionBounds.contains broadcasts like a numpy ufunc."""
349 # Clip the CellGridBounds with a Box offset by 1 pixel on each side so it
350 # does not snap to any cell boundary, forcing a lazy IntersectionBounds.
351 bounds = minified_cell_coadd.bounds
352 clip = Box.factory[
353 bounds.bbox.y.start + 1 : bounds.bbox.y.stop - 1,
354 bounds.bbox.x.start + 1 : bounds.bbox.x.stop - 1,
355 ]
356 check_bounds_contains_broadcasting(bounds.intersection(clip))