Coverage for tests/test_variance.py: 27%

71 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-20 02:36 -0700

1# This file is part of meas_algorithms. 

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# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import itertools 

23import unittest 

24import warnings 

25 

26import lsst.utils.tests 

27import numpy as np 

28from lsst.afw import image as afwImage 

29from lsst.meas.algorithms import ( 

30 ComputeNoiseCorrelationConfig, 

31 ComputeNoiseCorrelationTask, 

32 ScaleVarianceTask, 

33) 

34 

35 

36class NoiseVarianceTestCase(lsst.utils.tests.TestCase): 

37 @classmethod 

38 def setUpClass(cls, seed: int = 12345, size: int = 512) -> None: 

39 """ 

40 Set up a common noise field and a masked image for all test cases. 

41 

42 Parameters 

43 ---------- 

44 size : int, optional 

45 Size of the noise image to generate. 

46 seed : int, optional 

47 Seed for the random number generator. 

48 """ 

49 

50 super().setUpClass() 

51 

52 np.random.seed(seed) 

53 cls.noise = np.random.randn(size, size).astype(np.float32) 

54 

55 # We will clip the edges, so the variance plane will be smaller by 2. 

56 variance_array = np.ones((size - 2, size - 2), dtype=np.float32) 

57 

58 # Randomly set some mask bits to be non-zero. 

59 mask_array = (np.random.geometric(0.85, size=(size - 2, size - 2)) - 1).astype( 

60 np.int32 

61 ) 

62 # Set some masked variance values to zero. 

63 variance_array[mask_array > 1] = 0.0 

64 

65 cls.mi = afwImage.makeMaskedImageFromArrays( 

66 image=cls.noise[1:-1, 1:-1].copy(), variance=variance_array, mask=mask_array 

67 ) 

68 

69 @classmethod 

70 def tearDownClass(cls) -> None: 

71 del cls.mi 

72 del cls.noise 

73 super().tearDownClass() 

74 

75 def _prepareImage(self, rho1: float, rho2: float, background_value: float = 0.0) -> None: 

76 """Create a correlated Gaussian noise field using simple translations. 

77 

78 Y[i,j] = ( X[i,j] + a1 X[i-1,j] + a2 X[i,j-1] )/sqrt(1 + a1**2 + a2**2) 

79 

80 Var( X[i,j] ) = Var( Y[i,j] ) = 1 

81 Cov( Y[i,j], V[i-1,j] ) = a1 

82 Cov( Y[i,j], V[i,j-1] ) = a2 

83 

84 rho_i = a_i/sqrt(1 + a1**2 + a2**2) for i = 1, 2 

85 

86 Parameters 

87 ---------- 

88 rho1 : float 

89 Correlation coefficient along the horizontal (x) direction. 

90 rho2 : float 

91 Correlation coefficient along the vertical (y) direction. 

92 background_value : float, optional 

93 A constant background to add to the image. 

94 """ 

95 # Solve for the kernel parameters (a1, a2) & generate correlated noise. 

96 r2 = rho1**2 + rho2**2 

97 if r2 > 0: 

98 k = 0.5 * (1 + np.sqrt(1 - 4 * r2)) / r2 

99 a1, a2 = k * rho1, k * rho2 

100 self.noise += background_value 

101 try: 

102 corr_noise = ( 

103 self.noise 

104 + a1 * np.roll(self.noise, 1, axis=0) 

105 + a2 * np.roll(self.noise, 1, axis=1) 

106 ) / np.sqrt(1 + a1**2 + a2**2) 

107 finally: 

108 self.noise -= background_value 

109 else: 

110 a1, a2 = 0, 0 

111 corr_noise = self.noise + background_value 

112 

113 self.mi.image.array = corr_noise[1:-1, 1:-1] 

114 

115 @lsst.utils.tests.methodParameters( 

116 rho=((0.0, 0.0), (-0.2, 0.0), (0.0, 0.1), (0.15, 0.25), (0.25, -0.15)) 

117 ) 

118 def testScaleVariance(self, rho): 

119 """Test that the ScaleVarianceTask scales the variance plane correctly.""" 

120 task = ScaleVarianceTask() 

121 rho1, rho2 = rho 

122 self._prepareImage(rho1, rho2) 

123 scaleFactors = task.computeScaleFactors(self.mi) 

124 

125 # Check for consistency between pixelFactor and imageFactor 

126 self.assertFloatsAlmostEqual( 

127 scaleFactors.pixelFactor, scaleFactors.imageFactor, atol=1e-6 

128 ) 

129 

130 # Since the variance is expected to remain unity after introducing the 

131 # correlations, the scaleFactor should be 1.0 within statistical error. 

132 self.assertFloatsAlmostEqual(scaleFactors.pixelFactor, 1.0, rtol=2e-2) 

133 

134 @lsst.utils.tests.methodParametersProduct( 

135 rho=((0.0, 0.0), (0.2, 0.0), (0.0, -0.1), (0.15, 0.25), (-0.25, 0.15)), 

136 scaleEmpircalVariance=(False, True), 

137 subtractEmpiricalMean=(False, True), 

138 background_value=(0.0, 100.0), 

139 ) 

140 def testComputeCorrelation( 

141 self, rho, background_value, scaleEmpircalVariance, subtractEmpiricalMean 

142 ): 

143 """Test that the noise correlation coefficients are computed correctly.""" 

144 corr_matrix_size = 5 

145 config = ComputeNoiseCorrelationConfig(size=corr_matrix_size) 

146 config.scaleEmpiricalVariance = scaleEmpircalVariance 

147 config.subtractEmpiricalMean = subtractEmpiricalMean 

148 task = ComputeNoiseCorrelationTask(config=config) 

149 

150 rho1, rho2 = rho 

151 self._prepareImage(rho1, rho2, background_value=background_value) 

152 

153 # Create a copy of the image before running the task. 

154 mi_copy = afwImage.MaskedImage(self.mi, dtype=self.mi.dtype) 

155 self.assertIsNot(mi_copy, self.mi) # Check that it's a deepcopy. 

156 

157 with warnings.catch_warnings(record=True) as warning_list: 

158 corr_matrix = task.run(self.mi) 

159 

160 # Check that when dividing the background pixels by per-pixel variance 

161 # we did not divide by zero accidentally. 

162 self.assertEqual(sum(w.category is RuntimeWarning for w in warning_list), 0) 

163 

164 # Check that the task did not modify the input in place accidentally. 

165 self.assertIsNot(mi_copy, self.mi) 

166 np.testing.assert_array_equal(mi_copy.image.array, self.mi.image.array) 

167 np.testing.assert_array_equal(mi_copy.mask.array, self.mi.mask.array) 

168 np.testing.assert_array_equal(mi_copy.variance.array, self.mi.variance.array) 

169 

170 # corr_matrix elements should be zero except for (1,0), (0,1) & (0,0). 

171 # Use the other elements to get an estimate of the statistical 

172 # uncertainty in our estimates. 

173 err = np.std( 

174 [ 

175 corr_matrix(i, j) 

176 for i, j in itertools.product( 

177 range(corr_matrix_size), range(corr_matrix_size) 

178 ) 

179 if (i + j > 1) 

180 ] 

181 ) 

182 

183 self.assertLess(abs(corr_matrix(1, 0) / corr_matrix(0, 0) - rho1), 3 * err) 

184 self.assertLess(abs(corr_matrix(0, 1) / corr_matrix(0, 0) - rho2), 3 * err) 

185 self.assertLess(err, 3e-3) # Check that the err is much less than rho. 

186 

187 

188class MemoryTestCase(lsst.utils.tests.MemoryTestCase): 

189 pass 

190 

191 

192def setup_module(module): 

193 lsst.utils.tests.init() 

194 

195 

196if __name__ == "__main__": 196 ↛ 197line 196 didn't jump to line 197, because the condition on line 196 was never true

197 lsst.utils.tests.init() 

198 unittest.main()