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

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. 

11 

12from __future__ import annotations 

13 

14import math 

15import os 

16import unittest 

17 

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) 

26 

27try: 

28 from lsst.afw.image import ImageD as LegacyImageD 

29 from lsst.afw.math import Kernel as LegacyKernel 

30except ImportError: 

31 pass 

32 

33EXTERNAL_DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None) 

34 

35 

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. 

40 

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 """ 

45 

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 

55 

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 ) 

68 

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) 

85 

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) 

92 

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) 

103 

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) 

115 

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) 

135 

136 

137if __name__ == "__main__": 

138 unittest.main()