Coverage for tests/test_operators.py: 10%
136 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-01 11:54 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-01 11:54 +0000
1# This file is part of lsst.scarlet.lite.
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 os
24import numpy as np
25from lsst.scarlet.lite import Image
26from lsst.scarlet.lite.operators import (
27 Monotonicity,
28 prox_connected,
29 prox_monotonic_mask,
30 prox_sdss_symmetry,
31 prox_uncentered_symmetry,
32)
33from numpy.testing import assert_array_equal
34from utils import ScarletTestCase
37class TestOperators(ScarletTestCase):
38 def setUp(self) -> None:
39 filename = os.path.join(__file__, "..", "..", "data", "hsc_cosmos_35.npz")
40 filename = os.path.abspath(filename)
41 data = np.load(filename)
42 self.detect = np.sum(data["images"], axis=0)
43 self.centers = np.array([data["catalog"]["y"], data["catalog"]["x"]]).T
45 def test_prox_connected(self):
46 image1 = Image(np.full((5, 10), 1), yx0=(5, 2))
47 image2 = Image(np.full((3, 9), 2), yx0=(20, 16))
48 image3 = Image(np.full((5, 12), 3), yx0=(10, 30))
49 image4 = Image(np.full((7, 3), 4), yx0=(29, 5))
50 image5 = Image(np.full((11, 15), 5), yx0=(30, 30))
52 image = Image(np.zeros((50, 50), dtype=float))
53 image += image1 + image2 + image3
54 full_image = image + image4 + image5
56 # Include 3 of the 5 box centers
57 centers = [(7, 7), (21, 20), (12, 36)]
59 result = prox_connected(full_image.data, centers)
60 assert_array_equal(result, image.data)
62 def test_monotonicity(self):
63 shape = (201, 201)
64 cx = (shape[1] - 1) >> 1
65 cy = (shape[0] - 1) >> 1
66 x = np.arange(shape[1], dtype=float) - cx
67 y = np.arange(shape[0], dtype=float) - cy
68 x, y = np.meshgrid(x, y)
69 distance = np.sqrt(x**2 + y**2)
71 neighbor_dist = np.zeros((9,) + distance.shape, dtype=float)
72 neighbor_dist[0, 1:, 1:] = distance[1:, 1:] - distance[:-1, :-1]
73 neighbor_dist[1, 1:, :] = distance[1:, :] - distance[:-1, :]
74 neighbor_dist[2, 1:, :-1] = distance[1:, :-1] - distance[:-1, 1:]
75 neighbor_dist[3, :, 1:] = distance[:, 1:] - distance[:, :-1]
76 # For the center pixel, set the distance to 1 just so that it is
77 # non-zero
78 neighbor_dist[4, cy, cx] = 1
79 neighbor_dist[5, :, :-1] = distance[:, :-1] - distance[:, 1:]
80 neighbor_dist[6, :-1, 1:] = distance[:-1, 1:] - distance[1:, :-1]
81 neighbor_dist[7, :-1, :] = distance[:-1, :] - distance[1:, :]
82 neighbor_dist[8, :-1, :-1] = distance[:-1, :-1] - distance[1:, 1:]
84 monotonicity = Monotonicity(shape)
85 assert_array_equal(monotonicity.distance, distance)
86 assert_array_equal(monotonicity.weights > 0, neighbor_dist > 0)
87 self.assertTupleEqual(monotonicity.shape, (201, 201))
88 self.assertTupleEqual(monotonicity.center, (100, 100))
90 # Since the monotonicity operators _are_ the test for monotonicity,
91 # we just check that the two different monotonicty operators run,
92 # and that the weighted monotonicity operator is still monotonic
93 # according to the monotonicity mask operator.
94 morph = self.detect.copy()
95 cy, cx = self.centers[1].astype(int)
96 morph = monotonicity(morph, (cy, cx))
97 # Add zero threshold
98 morph[morph < 0] = 0
99 # Get the monotonic mask soluton
100 _, masked, _ = prox_monotonic_mask(
101 morph.copy(),
102 (cy, cx),
103 0,
104 0,
105 0,
106 )
107 # The operators are not exactly equal, since the weighted monotonicity
108 # uses diagonal pixels and the monotonic mask does not take those
109 # into account. So we allow for known pixels to be different.
110 diff = np.abs(morph - masked)
111 self.assertEqual(np.sum(diff > 0), 198)
113 # Test that interpolating edge pixels is working
114 _, interpolated, _ = prox_monotonic_mask(
115 morph.copy(),
116 (cy, cx),
117 0,
118 0,
119 3,
120 )
122 # Remove all of the diagonal weights and check that the
123 # weighted monotonic solution agrees with the monotonic mask solution.
124 weights = monotonicity.weights
125 weights[0] = 0
126 weights[2] = 0
127 weights[6] = 0
128 weights[8] = 0
130 morph = self.detect.copy()
131 cy, cx = self.centers[1].astype(int)
132 morph = monotonicity(morph, (cy, cx))
133 # Add zero threshold
134 morph[morph < 0] = 0
135 # Get the monotonic mask soluton
136 _, masked, _ = prox_monotonic_mask(
137 morph.copy(),
138 (cy, cx),
139 0,
140 0,
141 0,
142 )
143 assert_array_equal(morph, masked)
145 def test_resize_monotonicity(self):
146 monotonicity = Monotonicity((101, 101))
147 morph = self.detect.copy()
148 cy, cx = self.centers[1].astype(int)
149 morph = monotonicity(morph, (cy, cx))
150 self.assertTupleEqual(monotonicity.shape, (101, 101))
152 monotonicity.update((201, 201))
153 morph2 = monotonicity(morph, (cy, cx))
154 self.assertTupleEqual(monotonicity.shape, (201, 201))
155 assert_array_equal(morph, morph2)
157 with self.assertRaises(ValueError):
158 # Even shapes not allowed
159 Monotonicity((100, 100))
161 with self.assertRaises(ValueError):
162 # The shape should only have 2 dimensions
163 Monotonicity((101, 101, 101)) # type: ignore
165 with self.assertRaises(ValueError):
166 # The shape should have exactly 2 dimensions
167 Monotonicity((101,)) # type: ignore
169 def test_check_size(self):
170 monotonicity = Monotonicity((11, 11))
171 self.assertTupleEqual(monotonicity.shape, (11, 11))
172 self.assertTupleEqual(monotonicity.sizes, (5, 5, 6, 6))
173 morph = self.detect.copy()
174 cy, cx = self.centers[1].astype(int)
175 monotonicity(morph, (cy, cx))
176 self.assertTupleEqual(monotonicity.shape, (73, 73))
177 self.assertTupleEqual(monotonicity.sizes, (36, 36, 37, 37))
179 monotonicity = Monotonicity((11, 11), auto_update=False)
180 with self.assertRaises(ValueError):
181 monotonicity(morph, (cy, cx))
183 def test_off_center_monotonicity(self):
184 monotonicity = Monotonicity((101, 101))
185 morph = self.detect.copy()
186 cy, cx = self.centers[1].astype(int)
187 truth = monotonicity(morph.copy(), (cy, cx))
188 morph = monotonicity(morph, (cy + 1, cx))
189 assert_array_equal(morph, truth)
191 # Shift by 2 pixels and confirm that the morphologies are not equal
192 morph = self.detect.copy()
193 morph = monotonicity(morph, (cy + 2, cx))
194 with self.assertRaises(AssertionError):
195 assert_array_equal(morph, truth)
197 # Now increase the search radius and try again
198 monotonicity = Monotonicity((101, 101), fit_radius=2)
199 morph = self.detect.copy()
200 morph = monotonicity(morph, (cy + 2, cx))
201 assert_array_equal(morph, truth)
203 monotonicity = Monotonicity((101, 101), fit_radius=2)
204 morph = self.detect.copy()
205 morph = monotonicity(morph, (cy + 1, cx + 1))
206 assert_array_equal(morph, truth)
208 def test_symmetry(self):
209 # Test simple symmetry
210 morph = np.arange(27).reshape(3, 9)
211 truth = np.array(list(range(14)) + list(range(13)[::-1])).reshape(3, 9)
212 morph = prox_sdss_symmetry(morph)
213 assert_array_equal(morph, truth)
215 # Test uncentered symmetry
216 morph = np.arange(50).reshape(5, 10)
218 symmetric = [
219 [25, 26, 27, 28, 29],
220 [35, 36, 37, 36, 35],
221 [29, 28, 27, 26, 25],
222 ]
224 # Test leaving the non-symmetric part of the morphology
225 truth = morph.copy()
226 truth[2:, 5:] = symmetric
227 center = (3, 7)
228 symmetric_morph = prox_uncentered_symmetry(morph.copy(), center)
229 assert_array_equal(symmetric_morph, truth)
231 # Test setting the non-symmetric part of the morphology to zero
232 truth = np.zeros(morph.shape, dtype=int)
233 truth[2:, 5:] = symmetric
234 symmetric_morph = prox_uncentered_symmetry(morph.copy(), center, 0)
235 assert_array_equal(symmetric_morph, truth)
237 # Test skipping re-centering if the center of the source
238 # is the center of the image
239 _morph = morph[2:, 5:]
240 symmetric_morph = prox_uncentered_symmetry(_morph.copy(), (1, 2))
241 assert_array_equal(symmetric_morph, symmetric)
243 # Test using the default center of the image
244 _morph = morph[2:, 5:]
245 symmetric_morph = prox_uncentered_symmetry(_morph.copy())
246 assert_array_equal(symmetric_morph, _morph)