Coverage for tests/test_pessimisticPatternMatcherB3D.py: 16%
126 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-08 09:34 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-08 09:34 +0000
1# This file is part of meas_astrom.
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/>.
22from copy import copy
23import unittest
24import logging
26import numpy as np
28from lsst.meas.astrom.pessimistic_pattern_matcher_b_3D \
29 import PessimisticPatternMatcherB
31__deg_to_rad__ = np.pi/180
34class TestPessimisticPatternMatcherB(unittest.TestCase):
36 """Unittest suite for the Pessimistic Pattern Matcher B.
37 """
39 def setUp(self):
40 np.random.seed(12345)
42 n_points = 1000
43 # reference_obj_array is a number array representing
44 # 3D points randomly draw on a 1 sq deg patch.
45 self.reference_obj_array = np.empty((n_points, 4))
46 cos_theta_array = np.random.uniform(
47 np.cos(np.pi/2 + 0.5*__deg_to_rad__),
48 np.cos(np.pi/2 - 0.5*__deg_to_rad__), size=n_points)
49 sin_theta_array = np.sqrt(1 - cos_theta_array**2)
50 phi_array = np.random.uniform(-0.5, 0.5, size=n_points)*__deg_to_rad__
51 self.reference_obj_array[:, 0] = sin_theta_array*np.cos(phi_array)
52 self.reference_obj_array[:, 1] = sin_theta_array*np.sin(phi_array)
53 self.reference_obj_array[:, 2] = cos_theta_array
54 self.reference_obj_array[:, 3] = (
55 np.random.power(1.2, size=n_points)*4 + 20)
57 # Our initial source catalog is a straight copy of the reference
58 # array at first. In some of the tests we will add rotations and
59 # shifts to the data in order to test the input and outputs of our
60 # matcher.
61 self.source_obj_array = copy(self.reference_obj_array)
62 self.log = logging.getLogger(__name__)
64 def testConstructPattern(self):
65 """ Test that a specified pattern can be found in the reference
66 data and that the explicit ids match.
67 """
68 self.pyPPMb = PessimisticPatternMatcherB(
69 reference_array=self.reference_obj_array[:, :3],
70 log=self.log)
72 pattern_struct = self.pyPPMb._construct_pattern_and_shift_rot_matrix(
73 self.source_obj_array[:6, :3], 6, np.cos(np.radians(60. / 3600.)),
74 np.cos(np.radians(1.0)) ** 2, np.radians(5./3600.))
75 pattern_list = pattern_struct.ref_candidates
76 self.assertGreater(len(pattern_list), 0)
77 self.assertEqual(pattern_list[0], 0)
78 self.assertEqual(pattern_list[1], 1)
79 self.assertEqual(pattern_list[2], 2)
80 self.assertEqual(pattern_list[3], 3)
81 self.assertEqual(pattern_list[4], 4)
82 self.assertEqual(pattern_list[5], 5)
84 pattern_struct = self.pyPPMb._construct_pattern_and_shift_rot_matrix(
85 self.source_obj_array[:9, :3], 6, np.cos(np.radians(60. / 3600.)),
86 np.cos(np.radians(1.0)) ** 2, np.radians(5./3600.))
87 pattern_list = pattern_struct.ref_candidates
88 self.assertGreater(len(pattern_list), 0)
89 self.assertEqual(pattern_list[0], 0)
90 self.assertEqual(pattern_list[1], 1)
91 self.assertEqual(pattern_list[2], 2)
92 self.assertEqual(pattern_list[3], 3)
93 self.assertEqual(pattern_list[4], 4)
94 self.assertEqual(pattern_list[5], 5)
96 pattern_struct = self.pyPPMb._construct_pattern_and_shift_rot_matrix(
97 self.source_obj_array[[2, 4, 8, 16, 32, 64], :3], 6,
98 np.cos(np.radians(60. / 3600.)), np.cos(np.radians(1.0)) ** 2,
99 np.radians(5./3600.))
100 pattern_list = pattern_struct.ref_candidates
101 self.assertEqual(pattern_list[0], 2)
102 self.assertEqual(pattern_list[1], 4)
103 self.assertEqual(pattern_list[2], 8)
104 self.assertEqual(pattern_list[3], 16)
105 self.assertEqual(pattern_list[4], 32)
106 self.assertEqual(pattern_list[5], 64)
108 def testMatchPerfect(self):
109 """ Input objects that have no shift or rotation to the matcher
110 and test that we return a match.
111 """
112 self.pyPPMb = PessimisticPatternMatcherB(
113 reference_array=self.reference_obj_array[:, :3],
114 log=self.log)
116 match_struct = self.pyPPMb.match(
117 source_array=self.source_obj_array, n_check=9, n_match=6,
118 n_agree=2, max_n_patterns=100, max_shift=60., max_rotation=5.0,
119 max_dist=5., min_matches=30, pattern_skip_array=None)
120 self.assertEqual(len(match_struct.match_ids),
121 len(self.reference_obj_array))
122 self.assertTrue(
123 np.all(match_struct.distances_rad < 0.01/3600.0 * __deg_to_rad__))
125 def testOptimisticMatch(self):
126 """ Test the optimistic mode of the pattern matcher. That is
127 the algorithm with the early exit strategy as described in
128 Tabur 2007.
129 """
130 self.pyPPMb = PessimisticPatternMatcherB(
131 reference_array=self.reference_obj_array[:, :3],
132 log=self.log)
134 match_struct = self.pyPPMb.match(
135 source_array=self.source_obj_array, n_check=9, n_match=6,
136 n_agree=1, max_n_patterns=100, max_shift=60., max_rotation=6.0,
137 max_dist=5., min_matches=30, pattern_skip_array=None)
138 self.assertEqual(len(match_struct.match_ids),
139 len(self.reference_obj_array))
140 self.assertTrue(
141 np.all(match_struct.distances_rad < 0.01/3600.0 * __deg_to_rad__))
143 def testMatchSkip(self):
144 """ Test the ability to skip specified patterns in the matching
145 process.
146 """
147 self.pyPPMb = PessimisticPatternMatcherB(
148 reference_array=self.reference_obj_array[:, :3],
149 log=self.log)
151 match_struct = self.pyPPMb.match(
152 source_array=self.source_obj_array, n_check=9, n_match=6,
153 n_agree=2, max_n_patterns=100, max_shift=60., max_rotation=5.0,
154 max_dist=5., min_matches=30, pattern_skip_array=np.array([0]))
155 self.assertEqual(len(match_struct.match_ids),
156 len(self.reference_obj_array))
157 self.assertTrue(
158 np.all(match_struct.distances_rad < 0.01/3600.0 * __deg_to_rad__))
160 def testMatchMoreSources(self):
161 """ Test the case where we have more sources than references
162 but no rotation or shift.
163 """
164 self.pyPPMb = PessimisticPatternMatcherB(
165 reference_array=self.reference_obj_array[:500, :3],
166 log=self.log)
168 match_struct = self.pyPPMb.match(
169 source_array=self.source_obj_array, n_check=9, n_match=6,
170 n_agree=2, max_n_patterns=100, max_shift=60.0, max_rotation=5.0,
171 max_dist=5., min_matches=30, pattern_skip_array=None)
172 self.assertEqual(len(match_struct.match_ids),
173 len(self.reference_obj_array[:500]))
174 self.assertTrue(
175 np.all(match_struct.distances_rad < 0.01/3600.0 * __deg_to_rad__))
177 def testMatchMoreReferences(self):
178 """ Test the case where we have more references than sources
179 but no rotation or shift.
180 """
181 self.pyPPMb = PessimisticPatternMatcherB(
182 reference_array=self.reference_obj_array[:, :3],
183 log=self.log)
185 match_struct = self.pyPPMb.match(
186 source_array=self.source_obj_array[:500], n_check=9, n_match=6,
187 n_agree=2, max_n_patterns=100, max_shift=60., max_rotation=1.0,
188 max_dist=5., min_matches=30, pattern_skip_array=None)
189 self.assertEqual(len(match_struct.match_ids),
190 len(self.reference_obj_array[:500]))
191 self.assertTrue(
192 np.all(match_struct.distances_rad < 0.01/3600.0 * __deg_to_rad__))
194 def testShift(self):
195 """ Test the matcher when a shift is applied to the data.
197 We say shift here as while we are rotating the unit-sphere in 3D, on
198 our 'focal plane' this will appear as a shift.
199 """
200 self.pyPPMb = PessimisticPatternMatcherB(
201 reference_array=self.reference_obj_array[:, :3],
202 log=self.log)
203 theta = np.radians(45.0 / 3600.)
204 cos_theta = np.cos(theta)
205 sin_theta = np.sin(theta)
206 theta_rotation = self.pyPPMb._create_spherical_rotation_matrix(
207 np.array([0, 0, 1]), cos_theta, sin_theta)
209 self.source_obj_array[:, :3] = np.dot(
210 theta_rotation,
211 self.source_obj_array[:, :3].transpose()).transpose()
213 match_struct = self.pyPPMb.match(
214 source_array=self.source_obj_array, n_check=9, n_match=6,
215 n_agree=2, max_n_patterns=100, max_shift=60, max_rotation=5.0,
216 max_dist=5., min_matches=30, pattern_skip_array=None)
218 self.assertEqual(len(match_struct.match_ids),
219 len(self.reference_obj_array))
220 self.assertTrue(
221 np.all(match_struct.distances_rad < 0.01/3600.0 * __deg_to_rad__))
223 def testRotation(self):
224 """ Test the matcher for when a roation is applied to the data.
225 """
226 self.pyPPMb = PessimisticPatternMatcherB(
227 reference_array=self.reference_obj_array[:, :3],
228 log=self.log)
229 phi = 2.5*__deg_to_rad__
230 cos_phi = np.cos(phi)
231 sin_phi = np.sin(phi)
232 phi_rotation = self.pyPPMb._create_spherical_rotation_matrix(
233 np.array([1, 0, 0]), cos_phi, sin_phi)
235 self.source_obj_array[:, :3] = np.dot(
236 phi_rotation, self.source_obj_array[:, :3].transpose()).transpose()
238 match_struct = self.pyPPMb.match(
239 source_array=self.source_obj_array, n_check=9, n_match=6,
240 n_agree=2, max_n_patterns=100, max_shift=60, max_rotation=5.0,
241 max_dist=5., min_matches=30, pattern_skip_array=None)
243 self.assertEqual(len(match_struct.match_ids),
244 len(self.reference_obj_array))
245 self.assertTrue(
246 np.all(match_struct.distances_rad < 0.01/3600.0 * __deg_to_rad__))
248 def testShiftRotation(self):
249 """ Test both a shift and rotation being applied to the data.
250 """
251 self.pyPPMb = PessimisticPatternMatcherB(
252 reference_array=self.reference_obj_array[:, :3],
253 log=self.log)
254 theta = np.radians(45.0 / 3600.)
255 cos_theta = np.cos(theta)
256 sin_theta = np.sin(theta)
257 theta_rotation = self.pyPPMb._create_spherical_rotation_matrix(
258 np.array([0, 0, 1]), cos_theta, sin_theta)
260 phi = 2.5 * __deg_to_rad__
261 cos_phi = np.cos(phi)
262 sin_phi = np.sin(phi)
263 phi_rotation = self.pyPPMb._create_spherical_rotation_matrix(
264 np.array([1, 0, 0]), cos_phi, sin_phi)
266 shift_rot_matrix = np.dot(theta_rotation, phi_rotation)
268 self.source_obj_array[:, :3] = np.dot(
269 shift_rot_matrix,
270 self.source_obj_array[:, :3].transpose()).transpose()
272 match_struct = self.pyPPMb.match(
273 source_array=self.source_obj_array, n_check=9, n_match=6,
274 n_agree=2, max_n_patterns=100, max_shift=60., max_rotation=5.0,
275 max_dist=5., min_matches=30, pattern_skip_array=None)
277 self.assertEqual(len(match_struct.match_ids),
278 len(self.reference_obj_array))
279 self.assertTrue(
280 np.all(match_struct.distances_rad < 0.01/3600.0 * __deg_to_rad__))
282 def testLinearDistortion(self):
283 """ Create a simple linear distortion and test that the correct
284 references are still matched.
285 """
287 self.pyPPMb = PessimisticPatternMatcherB(
288 reference_array=self.reference_obj_array[:, :3],
289 log=self.log)
291 max_z = np.cos(np.pi/2 + 0.5 * __deg_to_rad__)
292 min_z = np.cos(np.pi/2 - 0.5 * __deg_to_rad__)
293 # The max shift in position we add to the position will be 25
294 # arcseconds.
295 max_distort = 25.0 / 3600. * __deg_to_rad__
296 self.source_obj_array[:, 2] = (
297 self.source_obj_array[:, 2]
298 - max_distort * (self.source_obj_array[:, 2] - min_z)
299 / (max_z - min_z))
300 # Renomalize the 3 vectors to be unit length.
301 distorted_dists = np.sqrt(self.source_obj_array[:, 0] ** 2
302 + self.source_obj_array[:, 1] ** 2
303 + self.source_obj_array[:, 2] ** 2)
304 self.source_obj_array[:, 0] /= distorted_dists
305 self.source_obj_array[:, 1] /= distorted_dists
306 self.source_obj_array[:, 2] /= distorted_dists
308 match_struct = self.pyPPMb.match(
309 source_array=self.source_obj_array, n_check=9, n_match=6,
310 n_agree=2, max_n_patterns=100, max_shift=60., max_rotation=5.0,
311 max_dist=5., min_matches=30, pattern_skip_array=None)
313 self.assertEqual(len(match_struct.match_ids),
314 len(self.reference_obj_array))
315 self.assertTrue(
316 np.all(match_struct.distances_rad < 10 / 3600.0 * __deg_to_rad__))
318 def testNoReferenceSources(self):
319 """Check that we get a helpful error when no reference objects are
320 supplied.
321 """
322 with self.assertRaisesRegex(ValueError, "No reference objects supplied"):
323 PessimisticPatternMatcherB(np.ndarray((0, 3)), self.log)
326if __name__ == '__main__': 326 ↛ 327line 326 didn't jump to line 327, because the condition on line 326 was never true
327 unittest.main()