Coverage for tests/test_difference_image_extras.py: 38%
82 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 math
16import os
17from typing import Any
19import pytest
21from lsst.images import Box, DetectorFrame, DifferenceImage, DifferenceImageTemplateInfo
22from lsst.images.convolution_kernels import ConvolutionKernel, ImageBasisConvolutionKernel
23from lsst.images.tests import (
24 DP2_TEMPLATE_COADD_DATASETS,
25 DP2_VISIT_DETECTOR_DATA_ID,
26 RoundtripFits,
27 assert_close,
28)
30try:
31 from lsst.afw.image import Exposure as LegacyExposure
32 from lsst.afw.image import ImageD as LegacyImageD
33 from lsst.afw.math import Kernel as LegacyKernel
34 from lsst.daf.base import PropertyList as LegacyPropertyList
35 from lsst.meas.algorithms import CoaddPsf as LegacyCoaddPsf
36except ImportError:
37 type LegacyExposure = Any # type: ignore[no-redef]
38 type LegacyKernel = Any # type: ignore[no-redef]
39 type LegacyPropertyList = Any # type: ignore[no-redef]
40 type LegacyCoaddPsf = Any # type: ignore[no-redef]
43EXTERNAL_DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None)
46@dataclasses.dataclass
47class _LegacyTestData:
48 kernel: LegacyKernel
49 template_metadata: LegacyPropertyList
50 template_psf: LegacyCoaddPsf
51 exposure: LegacyExposure
52 detector_frame: DetectorFrame
55@pytest.fixture(scope="session")
56def legacy_test_data() -> _LegacyTestData:
57 """Return a struct of legacy test objects loaded from EXTERNAL_DATA_DIR.
59 Skips if TESTDATA_IMAGES_DIR is unset or afw is unavailable.
60 """
61 if EXTERNAL_DATA_DIR is None: 61 ↛ 63line 61 didn't jump to line 63 because the condition on line 61 was always true
62 pytest.skip("TESTDATA_IMAGES_DIR is not in the environment.")
63 kernel_filename = os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "difference_kernel.fits")
64 template_filename = os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "template_detector.fits")
65 exposure_filename = os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "difference_image.fits")
66 try:
67 from lsst.afw.image import ExposureFitsReader
68 except ImportError:
69 pytest.skip("afw not available; cannot read legacy difference image or components")
70 kernel = LegacyKernel.readFits(kernel_filename)
71 template_reader = ExposureFitsReader(template_filename)
72 template_metadata = template_reader.readMetadata()
73 template_psf = template_reader.readPsf()
74 exposure = ExposureFitsReader(exposure_filename).read()
75 detector_frame = DetectorFrame(
76 **DP2_VISIT_DETECTOR_DATA_ID, bbox=Box.from_legacy(exposure.getDetector().getBBox())
77 )
78 return _LegacyTestData(
79 kernel=kernel,
80 template_metadata=template_metadata,
81 template_psf=template_psf,
82 exposure=exposure,
83 detector_frame=detector_frame,
84 )
87def compare_kernel_to_legacy(kernel: ConvolutionKernel, legacy_kernel: LegacyKernel) -> None:
88 """Assert that a ConvolutionKernel matches a legacy Kernel at sampled
89 points.
90 """
91 xy_array = kernel.bounds.bbox.meshgrid(3)
92 legacy_im = LegacyImageD(kernel.kernel_bbox.to_legacy())
93 for x, y in zip(xy_array.x.flat, xy_array.y.flat):
94 x = round(x)
95 y = round(y)
96 im = kernel.compute_kernel_image(x=x, y=y)
97 legacy_im.array[...] = 0.0
98 legacy_kernel.computeImage(legacy_im, doNormalize=False, x=x, y=y)
99 assert_close(im.array, legacy_im.array, rtol=1e-15, atol=1e-15)
102def _sanity_check_template_info(
103 template_info: list[DifferenceImageTemplateInfo], detector_frame: DetectorFrame
104) -> None:
105 """Check that a list of DifferenceImageTemplateInfo looks plausible."""
106 assert len(template_info) == 9
107 assert {info.dataset_id for info in template_info} == set(DP2_TEMPLATE_COADD_DATASETS.keys())
108 assert {
109 frozenset({"skymap": info.skymap, "tract": info.tract, "patch": info.patch, "band": "r"}.items())
110 for info in template_info
111 } == {frozenset(v.items()) for v in DP2_TEMPLATE_COADD_DATASETS.values()}
112 assert not any(info.psf_shape_flag for info in template_info)
113 assert not any(math.isnan(info.psf_shape_xx) for info in template_info)
114 assert not any(math.isnan(info.psf_shape_yy) for info in template_info)
115 assert not any(math.isnan(info.psf_shape_xy) for info in template_info)
116 assert all(detector_frame.bbox.contains(info.bounds.bbox) for info in template_info)
117 # Patches overlap, so total area is a bit more than detector area.
118 assert sum(info.bounds.area for info in template_info) < 1.5 * detector_frame.bbox.area
121def test_roundtrip(legacy_test_data: _LegacyTestData) -> None:
122 """Test round-tripping a DifferenceImage with extra components through
123 FITS.
124 """
125 difference_image = DifferenceImage.from_legacy(legacy_test_data.exposure)
126 difference_image.kernel = ImageBasisConvolutionKernel.from_legacy(legacy_test_data.kernel)
127 difference_image.templates = DifferenceImageTemplateInfo.from_legacy(
128 legacy_test_data.detector_frame,
129 legacy_test_data.template_psf,
130 legacy_test_data.template_metadata,
131 DP2_TEMPLATE_COADD_DATASETS,
132 )
133 with RoundtripFits(difference_image, storage_class="DifferenceImage") as roundtrip:
134 compare_kernel_to_legacy(roundtrip.get("kernel"), legacy_test_data.kernel)
135 compare_kernel_to_legacy(roundtrip.result.kernel, legacy_test_data.kernel)
136 _sanity_check_template_info(roundtrip.result.templates, legacy_test_data.detector_frame)
139def test_difference_kernel(legacy_test_data: _LegacyTestData) -> None:
140 """Test converting a legacy difference kernel to and from the new type."""
141 kernel = ImageBasisConvolutionKernel.from_legacy(legacy_test_data.kernel)
142 compare_kernel_to_legacy(kernel, legacy_test_data.kernel)
143 legacy_kernel_2 = kernel.to_legacy()
144 compare_kernel_to_legacy(kernel, legacy_kernel_2)
147def test_template_info(legacy_test_data: _LegacyTestData) -> None:
148 """Test extracting template information from legacy template_detector
149 components.
150 """
151 template_info = DifferenceImageTemplateInfo.from_legacy(
152 legacy_test_data.detector_frame,
153 legacy_test_data.template_psf,
154 legacy_test_data.template_metadata,
155 DP2_TEMPLATE_COADD_DATASETS,
156 )
157 _sanity_check_template_info(template_info, legacy_test_data.detector_frame)