Coverage for tests/test_difference_image_extras.py: 29%
68 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 09:14 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 09:14 +0000
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 math
15import os
16import unittest
18from lsst.images import Box, DetectorFrame, DifferenceImage, DifferenceImageTemplateInfo
19from lsst.images.convolution_kernels import ConvolutionKernel, ImageBasisConvolutionKernel
20from lsst.images.tests import (
21 DP2_TEMPLATE_COADD_DATASETS,
22 DP2_VISIT_DETECTOR_DATA_ID,
23 RoundtripFits,
24 assert_close,
25)
27try:
28 from lsst.afw.image import ImageD as LegacyImageD
29 from lsst.afw.math import Kernel as LegacyKernel
30except ImportError:
31 pass
33EXTERNAL_DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None)
36@unittest.skipUnless(EXTERNAL_DATA_DIR is not None, "TESTDATA_IMAGES_DIR is not in the environment.")
37class DifferenceImageExtraLegacyTestCase(unittest.TestCase):
38 """Tests for DifferenceImage components that were stored externally in
39 separate files in the legacy system.
41 Requires legacy code. Mosts tests for DifferenceImage are in
42 test_visit_image.py, since most of DifferenceImage is inherited from
43 VisitImage.
44 """
46 @classmethod
47 def setUpClass(cls) -> None:
48 assert EXTERNAL_DATA_DIR is not None, "Guaranteed by decorator."
49 cls.kernel_filename = os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "difference_kernel.fits")
50 cls.template_filename = os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "template_detector.fits")
51 cls.exposure_filename = os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "difference_image.fits")
52 try:
53 from lsst.afw.image import ExposureFitsReader
54 from lsst.afw.math import Kernel
56 cls.legacy_kernel = Kernel.readFits(cls.kernel_filename)
57 template_reader = ExposureFitsReader(cls.template_filename)
58 cls.legacy_template_metadata = template_reader.readMetadata()
59 cls.legacy_template_psf = template_reader.readPsf()
60 cls.legacy_exposure = ExposureFitsReader(cls.exposure_filename).read()
61 except ImportError:
62 raise unittest.SkipTest(
63 "afw not available; cannot read legacy difference image or components"
64 ) from None
65 cls.detector_frame = DetectorFrame(
66 **DP2_VISIT_DETECTOR_DATA_ID, bbox=Box.from_legacy(cls.legacy_exposure.getDetector().getBBox())
67 )
69 def test_roundtrip(self) -> None:
70 """Test round-tripping the difference image through FITs with the
71 extra components attached.
72 """
73 difference_image = DifferenceImage.from_legacy(self.legacy_exposure)
74 difference_image.kernel = ImageBasisConvolutionKernel.from_legacy(self.legacy_kernel)
75 difference_image.templates = DifferenceImageTemplateInfo.from_legacy(
76 self.detector_frame,
77 self.legacy_template_psf,
78 self.legacy_template_metadata,
79 DP2_TEMPLATE_COADD_DATASETS,
80 )
81 with RoundtripFits(self, difference_image, storage_class="DifferenceImage") as roundtrip:
82 self.compare_kernel_to_legacy(roundtrip.get("kernel"), self.legacy_kernel)
83 self.compare_kernel_to_legacy(roundtrip.result.kernel, self.legacy_kernel)
84 self.sanity_check_template_info(roundtrip.result.templates)
86 def test_difference_kernel(self) -> None:
87 """Test that we can convert to and from legacy difference kernels."""
88 kernel = ImageBasisConvolutionKernel.from_legacy(self.legacy_kernel)
89 self.compare_kernel_to_legacy(kernel, self.legacy_kernel)
90 legacy_kernel_2 = kernel.to_legacy()
91 self.compare_kernel_to_legacy(kernel, legacy_kernel_2)
93 def compare_kernel_to_legacy(self, kernel: ConvolutionKernel, legacy_kernel: LegacyKernel) -> None:
94 xy_array = kernel.bounds.bbox.meshgrid(3)
95 legacy_im = LegacyImageD(kernel.kernel_bbox.to_legacy())
96 for x, y in zip(xy_array.x.flat, xy_array.y.flat):
97 x = round(x)
98 y = round(y)
99 im = kernel.compute_kernel_image(x=x, y=y)
100 legacy_im.array[...] = 0.0
101 legacy_kernel.computeImage(legacy_im, doNormalize=False, x=x, y=y)
102 assert_close(self, im.array, legacy_im.array, rtol=1e-15, atol=1e-15)
104 def test_template_info(self) -> None:
105 """Test extracting template information from various legacy
106 template_detector components.
107 """
108 template_info = DifferenceImageTemplateInfo.from_legacy(
109 self.detector_frame,
110 self.legacy_template_psf,
111 self.legacy_template_metadata,
112 DP2_TEMPLATE_COADD_DATASETS,
113 )
114 self.sanity_check_template_info(template_info)
116 def sanity_check_template_info(self, template_info: list[DifferenceImageTemplateInfo]) -> None:
117 self.assertEqual(len(template_info), 9)
118 self.assertCountEqual([info.dataset_id for info in template_info], DP2_TEMPLATE_COADD_DATASETS.keys())
119 self.assertCountEqual(
120 [
121 {"skymap": info.skymap, "tract": info.tract, "patch": info.patch, "band": "r"}
122 for info in template_info
123 ],
124 DP2_TEMPLATE_COADD_DATASETS.values(),
125 )
126 self.assertFalse(any(info.psf_shape_flag for info in template_info))
127 self.assertFalse(any(math.isnan(info.psf_shape_xx) for info in template_info))
128 self.assertFalse(any(math.isnan(info.psf_shape_yy) for info in template_info))
129 self.assertFalse(any(math.isnan(info.psf_shape_xy) for info in template_info))
130 self.assertTrue(all(self.detector_frame.bbox.contains(info.bounds.bbox) for info in template_info))
131 # The template bounds will overlap somewhat because patches overlap,
132 # so their total area should be a bit more than the total area of the
133 # detector.
134 self.assertLess(sum(info.bounds.area for info in template_info), 1.5 * self.detector_frame.bbox.area)
137if __name__ == "__main__":
138 unittest.main()