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

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 dataclasses 

15import math 

16import os 

17from typing import Any 

18 

19import pytest 

20 

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) 

29 

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] 

41 

42 

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

44 

45 

46@dataclasses.dataclass 

47class _LegacyTestData: 

48 kernel: LegacyKernel 

49 template_metadata: LegacyPropertyList 

50 template_psf: LegacyCoaddPsf 

51 exposure: LegacyExposure 

52 detector_frame: DetectorFrame 

53 

54 

55@pytest.fixture(scope="session") 

56def legacy_test_data() -> _LegacyTestData: 

57 """Return a struct of legacy test objects loaded from EXTERNAL_DATA_DIR. 

58 

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 ) 

85 

86 

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) 

100 

101 

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 

119 

120 

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) 

137 

138 

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) 

145 

146 

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)