Coverage for tests/test_variance.py: 34%
71 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-30 10:20 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-30 10:20 +0000
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/>.
22import itertools
23import unittest
24import warnings
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)
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.
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 """
50 super().setUpClass()
52 np.random.seed(seed)
53 cls.noise = np.random.randn(size, size).astype(np.float32)
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)
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
65 cls.mi = afwImage.makeMaskedImageFromArrays(
66 image=cls.noise[1:-1, 1:-1].copy(), variance=variance_array, mask=mask_array
67 )
69 @classmethod
70 def tearDownClass(cls) -> None:
71 del cls.mi
72 del cls.noise
73 super().tearDownClass()
75 def _prepareImage(self, rho1: float, rho2: float, background_value: float = 0.0) -> None:
76 """Create a correlated Gaussian noise field using simple translations.
78 Y[i,j] = ( X[i,j] + a1 X[i-1,j] + a2 X[i,j-1] )/sqrt(1 + a1**2 + a2**2)
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
84 rho_i = a_i/sqrt(1 + a1**2 + a2**2) for i = 1, 2
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
113 self.mi.image.array = corr_noise[1:-1, 1:-1]
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)
125 # Check for consistency between pixelFactor and imageFactor
126 self.assertFloatsAlmostEqual(
127 scaleFactors.pixelFactor, scaleFactors.imageFactor, atol=1e-6
128 )
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)
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)
150 rho1, rho2 = rho
151 self._prepareImage(rho1, rho2, background_value=background_value)
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.
157 with warnings.catch_warnings(record=True) as warning_list:
158 corr_matrix = task.run(self.mi)
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)
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)
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 )
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.
188class MemoryTestCase(lsst.utils.tests.MemoryTestCase):
189 pass
192def setup_module(module):
193 lsst.utils.tests.init()
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()