lsst.meas.astrom  16.0-21-gdae7b8c+3
pessimistic_pattern_matcher_b_3D.py
Go to the documentation of this file.
1 
2 
3 import numpy as np
4 from scipy.optimize import least_squares
5 from scipy.spatial import cKDTree
6 
7 import lsst.pipe.base as pipeBase
8 
9 
10 def _rotation_matrix_chi_sq(flattened_rot_matrix,
11  pattern_a,
12  pattern_b,
13  max_dist_rad):
14  """Compute the squared differences for least squares fitting.
15 
16  Given a flattened rotation matrix, one N point pattern and another N point
17  pattern to transform into to, compute the squared differences between the
18  points in the two patterns after the rotation.
19 
20  Parameters
21  ----------
22  flattened_rot_matrix : `numpy.ndarray`, (9, )
23  A flattened array representing a 3x3 rotation matrix. The array is
24  flattened to comply with the API of scipy.optimize.least_squares.
25  Flattened elements are [[0, 0], [0, 1], [0, 2], [1, 0]...]
26  pattern_a : `numpy.ndarray`, (N, 3)
27  A array containing N, 3 vectors representing the objects we would like
28  to transform into the frame of pattern_b.
29  pattern_b : `numpy.ndarray`, (N, 3)
30  A array containing N, 3 vectors representing the reference frame we
31  would like to transform pattern_a into.
32  max_dist_rad : `float`
33  The maximum distance allowed from the pattern matching. This value is
34  used as the standard error for the resultant chi values.
35 
36  Returns
37  -------
38  noralized_diff : `numpy.ndarray`, (9,)
39  Array of differences between the vectors representing of the source
40  pattern rotated into the reference frame and the converse. This is
41  used to minimize in a least squares fitter.
42  """
43  # Unflatten the rotation matrix
44  rot_matrix = flattened_rot_matrix.reshape((3, 3))
45  # Compare the rotated source pattern to the references.
46  rot_pattern_a = np.dot(rot_matrix, pattern_a.transpose()).transpose()
47  diff_pattern_a_to_b = rot_pattern_a - pattern_b
48  # Return the flattened differences and length tolerances for use in a least
49  # squares fitter.
50  return diff_pattern_a_to_b.flatten() / max_dist_rad
51 
52 
54  """Class implementing a pessimistic version of Optimistic Pattern Matcher
55  B (OPMb) from Tabur 2007. See `DMTN-031 <http://ls.st/DMTN-031`_
56 
57  Parameters
58  ----------
59  reference_array : `numpy.ndarray`, (N, 3)
60  spherical points x, y, z of to use as reference objects for
61  pattern matching.
62  log : `lsst.log.Log`
63  Logger for outputting debug info.
64 
65  Notes
66  -----
67  The class loads and stores the reference object
68  in a convenient data structure for matching any set of source objects that
69  are assumed to contain each other. The pessimistic nature of the algorithm
70  comes from requiring that it discovers at least two patterns that agree on
71  the correct shift and rotation for matching before exiting. The original
72  behavior of OPMb can be recovered simply. Patterns matched between the
73  input datasets are n-spoked pinwheels created from n+1 points. Refer to
74  DMTN #031 for more details. http://github.com/lsst-dm/dmtn-031
75  """
76 
77  def __init__(self, reference_array, log):
78  self._reference_array = reference_array
79  self._n_reference = len(self._reference_array)
80  self.log = log
81 
83 
84  def _build_distances_and_angles(self):
85  """Create the data structures we will use to search for our pattern
86  match in.
87 
88  Throughout this function and the rest of the class we use id to
89  reference the position in the input reference catalog and index to
90  'index' into the arrays sorted on distance.
91  """
92 
93  # Initialize the arrays we will need for quick look up of pairs once
94  # have a candidate spoke center.
95  self._pair_id_array = np.empty(
96  (self._n_reference, self._n_reference - 1),
97  dtype=np.uint32)
98  self._pair_delta_array = np.empty(
99  (self._n_reference, self._n_reference - 1, 3),
100  dtype=np.float64)
101  self._pair_dist_array = np.empty(
102  (self._n_reference, self._n_reference - 1),
103  dtype=np.float64)
104 
105  # Create empty lists to temporarily store our pair information per
106  # reference object. These will be concatenated into our final arrays.
107  sub_id_array_list = []
108  sub_delta_array_list = []
109  sub_dist_array_list = []
110 
111  # Loop over reference objects and store pair distances, ids, and
112  # 3 vector deltas.
113  for ref_id, ref_obj in enumerate(self._reference_array):
114 
115  # Reserve and fill the ids of each reference object pair.
116  sub_id_array = np.zeros((self._n_reference - 1 - ref_id, 2),
117  dtype=np.uint32)
118  sub_id_array[:, 0] = ref_id
119  sub_id_array[:, 1] = np.arange(ref_id + 1, self._n_reference,
120  dtype=np.uint32)
121 
122  # Compute the vector deltas for each pair of reference objects
123  # and compute and store the distances.
124  sub_delta_array = (self._reference_array[ref_id + 1:, :] -
125  ref_obj)
126  sub_dist_array = np.sqrt(sub_delta_array[:, 0] ** 2 +
127  sub_delta_array[:, 1] ** 2 +
128  sub_delta_array[:, 2] ** 2)
129 
130  # Append to our arrays to the output lists for later
131  # concatenation.
132  sub_id_array_list.append(sub_id_array)
133  sub_delta_array_list.append(sub_delta_array)
134  sub_dist_array_list.append(sub_dist_array)
135 
136  # Fill the pair look up arrays row wise and then column wise.
137  self._pair_id_array[ref_id, ref_id:] = sub_id_array[:, 1]
138  self._pair_delta_array[ref_id, ref_id:, :] = sub_delta_array
139  self._pair_dist_array[ref_id, ref_id:] = sub_dist_array
140 
141  # Don't fill the array column wise if we are on the last as
142  # these pairs have already been filled by previous
143  # iterations.
144  if ref_id < self._n_reference - 1:
145  self._pair_id_array[ref_id + 1:, ref_id] = ref_id
146  self._pair_delta_array[ref_id + 1:, ref_id, :] = \
147  sub_delta_array
148  self._pair_dist_array[ref_id + 1:, ref_id] = sub_dist_array
149 
150  # Sort each row on distance for fast look up of pairs given
151  # the id of one of the objects in the pair.
152  sorted_pair_dist_args = self._pair_dist_array[ref_id, :].argsort()
153  self._pair_dist_array[ref_id, :] = self._pair_dist_array[
154  ref_id, sorted_pair_dist_args]
155  self._pair_id_array[ref_id, :] = self._pair_id_array[
156  ref_id, sorted_pair_dist_args]
157  self._pair_delta_array[ref_id, :, :] = self._pair_delta_array[
158  ref_id, sorted_pair_dist_args, :]
159 
160  # Concatenate our arrays together.
161  unsorted_id_array = np.concatenate(sub_id_array_list)
162  unsorted_delta_array = np.concatenate(sub_delta_array_list)
163  unsorted_dist_array = np.concatenate(sub_dist_array_list)
164 
165  # Sort each array on the pair distances for the initial
166  # optimistic pattern matcher lookup.
167  sorted_dist_args = unsorted_dist_array.argsort()
168  self._dist_array = unsorted_dist_array[sorted_dist_args]
169  self._id_array = unsorted_id_array[sorted_dist_args]
170  self._delta_array = unsorted_delta_array[sorted_dist_args]
171 
172  return None
173 
174  def match(self, source_array, n_check, n_match, n_agree,
175  max_n_patterns, max_shift, max_rotation, max_dist,
176  min_matches, pattern_skip_array=None):
177  """Match a given source catalog into the loaded reference catalog.
178 
179  Given array of points on the unit sphere and tolerances, we
180  attempt to match a pinwheel like pattern between these input sources
181  and the reference objects this class was created with. This pattern
182  informs of the shift and rotation needed to align the input source
183  objects into the frame of the references.
184 
185  Parameters
186  ----------
187  source_array : `numpy.ndarray`, (N, 3)
188  An array of spherical x,y,z coordinates and a magnitude in units
189  of objects having a lower value for sorting. The array should be
190  of shape (N, 4).
191  n_check : `int`
192  Number of sources to create a pattern from. Not all objects may be
193  checked if n_match criteria is before looping through all n_check
194  objects.
195  n_match : `int`
196  Number of objects to use in constructing a pattern to match.
197  n_agree : `int`
198  Number of found patterns that must agree on their shift and
199  rotation before exiting. Set this value to 1 to recover the
200  expected behavior of Optimistic Pattern Matcher B.
201  max_n_patters : `int`
202  Number of patterns to create from the input source objects to
203  attempt to match into the reference objects.
204  max_shift : `float`
205  Maximum allowed shift to match patterns in arcseconds.
206  max_rotation : `float`
207  Maximum allowed rotation between patterns in degrees.
208  max_dist : `float`
209  Maximum distance in arcseconds allowed between candidate spokes in
210  the source and reference objects. Also sets that maximum distance
211  in the intermediate verify, pattern shift/rotation agreement, and
212  final verify steps.
213  pattern_skip_array : `int`
214  Patterns we would like to skip. This could be due to the pattern
215  being matched on a previous iteration that we now consider invalid.
216  This assumes the ordering of the source objects is the same
217  between different runs of the matcher which, assuming no object
218  has been inserted or the magnitudes have changed, it should be.
219 
220  Returns
221  -------
222  output_struct : `lsst.pipe.base.Struct`
223  Result struct with components
224 
225  - ``matches`` : (N, 2) array of matched ids for pairs. Empty list if no
226  match found (`numpy.ndarray`, (N, 2) or `list`)
227  - ``distances_rad`` : Radian distances between the matched objects.
228  Empty list if no match found (`numpy.ndarray`, (N,))
229  - ``pattern_idx``: Index of matched pattern. None if no match found
230  (`int`).
231  - ``shift`` : Magnitude for the shift between the source and reference
232  objects in arcseconds. None if no match found (`float`).
233  """
234 
235  # Given our input source_array we sort on magnitude.
236  sorted_source_array = source_array[source_array[:, -1].argsort(), :3]
237  n_source = len(sorted_source_array)
238 
239  # Initialize output struct.
240  output_match_struct = pipeBase.Struct(
241  match_ids=[],
242  distances_rad=[],
243  pattern_idx=None,
244  shift=None,)
245 
246  if n_source <= 0:
247  self.log.warn("Source object array is empty. Unable to match. "
248  "Exiting matcher.")
249  return None
250 
251  # To test if the shifts and rotations we find agree with each other,
252  # we first create two test points situated at the top and bottom of
253  # where the z axis on the sphere bisects the source catalog.
254  test_vectors = self._compute_test_vectors(source_array[:, :3])
255 
256  # We now create an empty list of our resultant rotated vectors to
257  # compare the different rotations we find.
258  rot_vect_list = []
259 
260  # Convert the tolerances to values we will use in the code.
261  max_cos_shift = np.cos(np.radians(max_shift / 3600.))
262  max_cos_rot_sq = np.cos(np.radians(max_rotation)) ** 2
263  max_dist_rad = np.radians(max_dist / 3600.)
264 
265  # Loop through the sources from brightest to faintest, grabbing a
266  # chunk of n_check each time.
267  for pattern_idx in range(np.min((max_n_patterns,
268  n_source - n_match))):
269 
270  # If this pattern is one that we matched on the past but we
271  # now want to skip, we do so here.
272  if pattern_skip_array is not None and \
273  np.any(pattern_skip_array == pattern_idx):
274  self.log.debug(
275  "Skipping previously matched bad pattern %i..." %
276  pattern_idx)
277  continue
278  # Grab the sources to attempt to create this pattern.
279  pattern = sorted_source_array[
280  pattern_idx: np.min((pattern_idx + n_check, n_source)), :3]
281 
282  # Construct a pattern given the number of points defining the
283  # pattern complexity. This is the start of the primary tests to
284  # match our source pattern into the reference objects.
285  construct_return_struct = \
287  pattern, n_match, max_cos_shift, max_cos_rot_sq,
288  max_dist_rad)
289 
290  # Our struct is None if we could not match the pattern.
291  if construct_return_struct.ref_candidates is None or \
292  construct_return_struct.shift_rot_matrix is None or \
293  construct_return_struct.cos_shift is None or \
294  construct_return_struct.sin_rot is None:
295  continue
296 
297  # Grab the output data from the Struct object.
298  ref_candidates = construct_return_struct.ref_candidates
299  shift_rot_matrix = construct_return_struct.shift_rot_matrix
300  cos_shift = construct_return_struct.cos_shift
301  sin_rot = construct_return_struct.sin_rot
302 
303  # If we didn't match enough candidates we continue to the next
304  # pattern.
305  if len(ref_candidates) < n_match:
306  continue
307 
308  # Now that we know our pattern and shift/rotation are valid we
309  # store the the rotated versions of our test points for later
310  # use.
311  tmp_rot_vect_list = []
312  for test_vect in test_vectors:
313  tmp_rot_vect_list.append(np.dot(shift_rot_matrix, test_vect))
314  # Since our test point are in the source frame, we can test if
315  # their lengths are mostly preserved under the transform.
316  if not self._test_pattern_lengths(np.array(tmp_rot_vect_list),
317  max_dist_rad):
318  continue
319 
320  tmp_rot_vect_list.append(pattern_idx)
321  rot_vect_list.append(tmp_rot_vect_list)
322 
323  # Test if we have enough rotations, which agree, or if we
324  # are in optimistic mode.
325  if self._test_rotation_agreement(rot_vect_list, max_dist_rad) < \
326  n_agree - 1:
327  continue
328 
329  # Perform final verify.
330  match_sources_struct = self._match_sources(source_array[:, :3],
331  shift_rot_matrix)
332 
333  n_matched = len(match_sources_struct.match_ids[
334  match_sources_struct.distances_rad < max_dist_rad])
335 
336  # Check that we have enough matches.
337  if n_matched >= min_matches:
338  # Convert the observed shift to arcseconds
339  shift = np.degrees(np.arccos(cos_shift)) * 3600.
340  # Print information to the logger.
341  self.log.debug("Succeeded after %i patterns." % pattern_idx)
342  self.log.debug("\tShift %.4f arcsec" % shift)
343  self.log.debug("\tRotation: %.4f deg" %
344  np.degrees(np.arcsin(sin_rot)))
345 
346  # Fill the struct and return.
347  output_match_struct.match_ids = \
348  match_sources_struct.match_ids
349  output_match_struct.distances_rad = \
350  match_sources_struct.distances_rad
351  output_match_struct.pattern_idx = pattern_idx
352  output_match_struct.shift = shift
353  return output_match_struct
354 
355  self.log.debug("Failed after %i patterns." % (pattern_idx + 1))
356  return output_match_struct
357 
358  def _compute_test_vectors(self, source_array):
359  """Compute spherical 3 vectors at the edges of the x, y, z extent
360  of the input source catalog.
361 
362  Parameters
363  ----------
364  source_array : `numpy.ndarray`, (N, 3)
365  array of 3 vectors representing positions on the unit
366  sphere.
367 
368  Returns
369  -------
370  test_vectors : `numpy.ndarray`, (N, 3)
371  Array of vectors representing the maximum extents in x, y, z
372  of the input source array. These are used with the rotations
373  the code finds to test for agreement from different patterns
374  when the code is running in pessimistic mode.
375  """
376 
377  # Get the center of source_array.
378  if np.any(np.logical_not(np.isfinite(source_array))):
379  self.log.warn("Input source objects contain non-finite values. "
380  "This could end badly.")
381  center_vect = np.nanmean(source_array, axis=0)
382 
383  # So that our rotation test works over the full sky we compute
384  # the max extent in each Cartesian direction x,y,z.
385  xbtm_vect = np.array([np.min(source_array[:, 0]), center_vect[1],
386  center_vect[2]], dtype=np.float64)
387  xtop_vect = np.array([np.max(source_array[:, 0]), center_vect[1],
388  center_vect[2]], dtype=np.float64)
389  xbtm_vect /= np.sqrt(np.dot(xbtm_vect, xbtm_vect))
390  xtop_vect /= np.sqrt(np.dot(xtop_vect, xtop_vect))
391 
392  ybtm_vect = np.array([center_vect[0], np.min(source_array[:, 1]),
393  center_vect[2]], dtype=np.float64)
394  ytop_vect = np.array([center_vect[0], np.max(source_array[:, 1]),
395  center_vect[2]], dtype=np.float64)
396  ybtm_vect /= np.sqrt(np.dot(ybtm_vect, ybtm_vect))
397  ytop_vect /= np.sqrt(np.dot(ytop_vect, ytop_vect))
398 
399  zbtm_vect = np.array([center_vect[0], center_vect[1],
400  np.min(source_array[:, 2])], dtype=np.float64)
401  ztop_vect = np.array([center_vect[0], center_vect[1],
402  np.max(source_array[:, 2])], dtype=np.float64)
403  zbtm_vect /= np.sqrt(np.dot(zbtm_vect, zbtm_vect))
404  ztop_vect /= np.sqrt(np.dot(ztop_vect, ztop_vect))
405 
406  # Return our list of vectors for later rotation testing.
407  return np.array([xbtm_vect, xtop_vect, ybtm_vect, ytop_vect,
408  zbtm_vect, ztop_vect])
409 
410  def _construct_pattern_and_shift_rot_matrix(self, src_pattern_array,
411  n_match, max_cos_theta_shift,
412  max_cos_rot_sq, max_dist_rad):
413  """Test an input source pattern against the reference catalog.
414 
415  Returns the candidate matched patterns and their
416  implied rotation matrices or None.
417 
418  Parameters
419  ----------
420  src_pattern_array : `numpy.ndarray`, (N, 3)
421  Sub selection of source 3 vectors to create a pattern from
422  n_match : `int`
423  Number of points to attempt to create a pattern from. Must be
424  >= len(src_pattern_array)
425  max_cos_theta_shift : `float`
426  Maximum shift allowed between two patterns' centers.
427  max_cos_rot_sq : `float`
428  Maximum rotation between two patterns that have been shifted
429  to have their centers on top of each other.
430  max_dist_rad : `float`
431  Maximum delta distance allowed between the source and reference
432  pair distances to consider the reference pair a candidate for
433  the source pair. Also sets the tolerance between the opening
434  angles of the spokes when compared to the reference.
435 
436  Return
437  -------
438  output_matched_pattern : `lsst.pipe.base.Struct`
439  Result struct with components:
440 
441  - ``ref_candidates`` : ids of the matched pattern in the internal
442  reference_array object (`list` of `int`).
443  - ``src_candidates`` : Pattern ids of the sources matched
444  (`list` of `int`).
445  - ``shift_rot_matrix_src_to_ref`` : 3x3 matrix specifying the full
446  shift and rotation between the reference and source objects.
447  Rotates source into reference frame. `None` if match is not
448  found. (`numpy.ndarray`, (3, 3))
449  - ``shift_rot_matrix_ref_to_src`` : 3x3 matrix specifying the full
450  shift and rotation of the reference and source objects. Rotates
451  reference into source frame. None if match is not found
452  (`numpy.ndarray`, (3, 3)).
453  - ``cos_shift`` : Magnitude of the shift found between the two
454  patten centers. `None` if match is not found (`float`).
455  - ``sin_rot`` : float value of the rotation to align the already
456  shifted source pattern to the reference pattern. `None` if no match
457  found (`float`).
458  """
459 
460  # Create our place holder variables for the matched sources and
461  # references. The source list starts with the 0th and first indexed
462  # objects as we are guaranteed to use those and these define both
463  # the shift and rotation of the final pattern.
464  output_matched_pattern = pipeBase.Struct(
465  ref_candidates=[],
466  src_candidates=[],
467  shift_rot_matrix=None,
468  cos_shift=None,
469  sin_rot=None)
470 
471  # Create the delta vectors and distances we will need to assemble the
472  # spokes of the pattern.
473  src_delta_array = np.empty((len(src_pattern_array) - 1, 3))
474  src_delta_array[:, 0] = (src_pattern_array[1:, 0] -
475  src_pattern_array[0, 0])
476  src_delta_array[:, 1] = (src_pattern_array[1:, 1] -
477  src_pattern_array[0, 1])
478  src_delta_array[:, 2] = (src_pattern_array[1:, 2] -
479  src_pattern_array[0, 2])
480  src_dist_array = np.sqrt(src_delta_array[:, 0]**2 +
481  src_delta_array[:, 1]**2 +
482  src_delta_array[:, 2]**2)
483 
484  # Our first test. We search the reference dataset for pairs
485  # that have the same length as our first source pairs to with
486  # plus/minus the max_dist tolerance.
487  ref_dist_index_array = self._find_candidate_reference_pairs(
488  src_dist_array[0], self._dist_array, max_dist_rad)
489 
490  # Start our loop over the candidate reference objects.
491  for ref_dist_idx in ref_dist_index_array:
492  # We have two candidates for which reference object corresponds
493  # with the source at the center of our pattern. As such we loop
494  # over and test both possibilities.
495  tmp_ref_pair_list = self._id_array[ref_dist_idx]
496  for pair_idx, ref_id in enumerate(tmp_ref_pair_list):
497  src_candidates = [0, 1]
498  ref_candidates = []
499  shift_rot_matrix = None
500  cos_shift = None
501  sin_rot = None
502  # Test the angle between our candidate ref center and the
503  # source center of our pattern. This angular distance also
504  # defines the shift we will later use.
505  ref_center = self._reference_array[ref_id]
506  cos_shift = np.dot(src_pattern_array[0], ref_center)
507  if cos_shift < max_cos_theta_shift:
508  continue
509 
510  # We can now append this one as a candidate.
511  ref_candidates.append(ref_id)
512  ref_delta = self._delta_array[ref_dist_idx]
513  # If the candidate reference center we found is second in
514  # this pair we need to reverse the direction of the
515  # corresponding pair's delta vector.
516  if pair_idx == 0:
517  ref_candidates.append(
518  tmp_ref_pair_list[1])
519  else:
520  ref_candidates.append(
521  tmp_ref_pair_list[0])
522  ref_delta *= -1
523 
524  # For dense fields it will be faster to compute the absolute
525  # rotation this pair suggests first rather than saving it
526  # after all the spokes are found. We then compute the cos^2
527  # of the rotation and first part of the rotation matrix from
528  # source to reference frame.
529  test_rot_struct = self._test_rotation(
530  src_pattern_array[0], ref_center, src_delta_array[0],
531  ref_delta, cos_shift, max_cos_rot_sq)
532  if test_rot_struct.cos_rot_sq is None or \
533  test_rot_struct.proj_ref_ctr_delta is None or \
534  test_rot_struct.shift_matrix is None:
535  continue
536 
537  # Get the data from the return struct.
538  cos_rot_sq = test_rot_struct.cos_rot_sq
539  proj_ref_ctr_delta = test_rot_struct.proj_ref_ctr_delta
540  shift_matrix = test_rot_struct.shift_matrix
541 
542  # Now that we have a candidate first spoke and reference
543  # pattern center, we mask our future search to only those
544  # pairs that contain our candidate reference center.
545  tmp_ref_delta_array = self._pair_delta_array[ref_id]
546  tmp_ref_dist_array = self._pair_dist_array[ref_id]
547  tmp_ref_id_array = self._pair_id_array[ref_id]
548 
549  # Now we feed this sub data to match the spokes of
550  # our pattern.
551  pattern_spoke_struct = self._create_pattern_spokes(
552  src_pattern_array[0], src_delta_array, src_dist_array,
553  self._reference_array[ref_id], ref_id, proj_ref_ctr_delta,
554  tmp_ref_delta_array, tmp_ref_dist_array,
555  tmp_ref_id_array, max_dist_rad, n_match)
556 
557  # If we don't find enough candidates we can continue to the
558  # next reference center pair.
559  if len(pattern_spoke_struct.ref_spoke_list) < n_match - 2 or \
560  len(pattern_spoke_struct.src_spoke_list) < n_match - 2:
561  continue
562 
563  # If we have the right number of matched ids we store these.
564  ref_candidates.extend(pattern_spoke_struct.ref_spoke_list)
565  src_candidates.extend(pattern_spoke_struct.src_spoke_list)
566 
567  # We can now create our full rotation matrix for both the
568  # shift and rotation. Reminder shift, aligns the pattern
569  # centers, rotation rotates the spokes on top of each other.
570  shift_rot_struct = self._create_shift_rot_matrix(
571  cos_rot_sq, shift_matrix, src_delta_array[0],
572  self._reference_array[ref_id], ref_delta)
573  # If we fail to create the rotation matrix, continue to the
574  # next objects.
575  if shift_rot_struct.sin_rot is None or \
576  shift_rot_struct.shift_rot_matrix is None:
577  continue
578 
579  # Get the data from the return struct.
580  sin_rot = shift_rot_struct.sin_rot
581  shift_rot_matrix = shift_rot_struct.shift_rot_matrix
582 
583  # Now that we have enough candidates we test to see if it
584  # passes intermediate verify. This shifts and rotates the
585  # source pattern into the reference frame and then tests that
586  # each source/reference object pair is within max_dist. It also
587  # tests the opposite rotation that is reference to source
588  # frame.
589  fit_shift_rot_matrix = self._intermediate_verify(
590  src_pattern_array[src_candidates],
591  self._reference_array[ref_candidates],
592  shift_rot_matrix, max_dist_rad)
593 
594  if fit_shift_rot_matrix is not None:
595  # Fill the struct and return.
596  output_matched_pattern.ref_candidates = ref_candidates
597  output_matched_pattern.src_candidates = src_candidates
598  output_matched_pattern.shift_rot_matrix = \
599  fit_shift_rot_matrix
600  output_matched_pattern.cos_shift = cos_shift
601  output_matched_pattern.sin_rot = sin_rot
602  return output_matched_pattern
603 
604  return output_matched_pattern
605 
606  def _find_candidate_reference_pairs(self, src_dist, ref_dist_array,
607  max_dist_rad):
608  """Wrap numpy.searchsorted to find the range of reference spokes
609  within a spoke distance tolerance of our source spoke.
610 
611  Returns an array sorted from the smallest absolute delta distance
612  between source and reference spoke length. This sorting increases the
613  speed for the pattern search greatly.
614 
615  Parameters
616  ----------
617  src_dist : `float`
618  float value of the distance we would like to search for in
619  the reference array in radians.
620  ref_dist_array : `numpy.ndarray`, (N,)
621  sorted array of distances in radians.
622  max_dist_rad : `float`
623  maximum plus/minus search to find in the reference array in
624  radians.
625 
626  Return
627  ------
628  tmp_diff_array : `numpy.ndarray`, (N,)
629  indices lookup into the input ref_dist_array sorted by the
630  difference in value to the src_dist from absolute value
631  smallest to largest.
632  """
633  # Find the index of the minimum and maximum values that satisfy
634  # the tolerance.
635  start_idx = np.searchsorted(ref_dist_array, src_dist - max_dist_rad)
636  end_idx = np.searchsorted(ref_dist_array, src_dist + max_dist_rad,
637  side='right')
638 
639  # If these are equal there are no candidates and we exit.
640  if start_idx == end_idx:
641  return []
642 
643  # Make sure the endpoints of the input array are respected.
644  if start_idx < 0:
645  start_idx = 0
646  if end_idx > ref_dist_array.shape[0]:
647  end_idx = ref_dist_array.shape[0]
648 
649  # Now we sort the indices from smallest absolute delta dist difference
650  # to the largest and return the vector. This step greatly increases
651  # the speed of the algorithm.
652  tmp_diff_array = np.fabs(ref_dist_array[start_idx:end_idx] - src_dist)
653  return tmp_diff_array.argsort() + start_idx
654 
655  def _test_rotation(self, src_center, ref_center, src_delta, ref_delta,
656  cos_shift, max_cos_rot_sq):
657  """ Test if the rotation implied between the source
658  pattern and reference pattern is within tolerance. To test this
659  we need to create the first part of our spherical rotation matrix
660  which we also return for use later.
661 
662  Parameters
663  ----------
664  src_center : `numpy.ndarray`, (N, 3)
665  pattern.
666  ref_center : `numpy.ndarray`, (N, 3)
667  3 vector defining the center of the candidate reference pinwheel
668  pattern.
669  src_delta : `numpy.ndarray`, (N, 3)
670  3 vector delta between the source pattern center and the end of
671  the pinwheel spoke.
672  ref_delta : `numpy.ndarray`, (N, 3)
673  3 vector delta of the candidate matched reference pair
674  cos_shift : `float`
675  Cosine of the angle between the source and reference candidate
676  centers.
677  max_cos_rot_sq : `float`
678  candidate reference pair after shifting the centers on top of each
679  other. The function will return None if the rotation implied is
680  greater than max_cos_rot_sq.
681 
682  Returns
683  -------
684  result : `lsst.pipe.base.Struct`
685  Result struct with components:
686 
687  - ``cos_rot_sq`` : magnitude of the rotation needed to align the
688  two patterns after their centers are shifted on top of each
689  other. `None` if rotation test fails (`float`).
690  - ``shift_matrix`` : 3x3 rotation matrix describing the shift needed to
691  align the source and candidate reference center. `None` if rotation
692  test fails (`numpy.ndarray`, (N, 3)).
693  """
694 
695  # Make sure the sine is a real number.
696  if cos_shift > 1.0:
697  cos_shift = 1.
698  elif cos_shift < -1.0:
699  cos_shift = -1.
700  sin_shift = np.sqrt(1 - cos_shift ** 2)
701 
702  # If the sine of our shift is zero we only need to use the identity
703  # matrix for the shift. Else we construct the rotation matrix for
704  # shift.
705  if sin_shift > 0:
706  rot_axis = np.cross(src_center, ref_center)
707  rot_axis /= sin_shift
708  shift_matrix = self._create_spherical_rotation_matrix(
709  rot_axis, cos_shift, sin_shift)
710  else:
711  shift_matrix = np.identity(3)
712 
713  # Now that we have our shift we apply it to the src delta vector
714  # and check the rotation.
715  rot_src_delta = np.dot(shift_matrix, src_delta)
716  proj_src_delta = (rot_src_delta -
717  np.dot(rot_src_delta, ref_center) * ref_center)
718  proj_ref_delta = (ref_delta -
719  np.dot(ref_delta, ref_center) * ref_center)
720  cos_rot_sq = (np.dot(proj_src_delta, proj_ref_delta) ** 2 /
721  (np.dot(proj_src_delta, proj_src_delta) *
722  np.dot(proj_ref_delta, proj_ref_delta)))
723  # If the rotation isn't in tolerance return None.
724  if cos_rot_sq < max_cos_rot_sq:
725  return pipeBase.Struct(
726  cos_rot_sq=None,
727  proj_ref_ctr_delta=None,
728  shift_matrix=None,)
729  # Return the rotation angle, the plane projected reference vector,
730  # and the first half of the full shift and rotation matrix.
731  return pipeBase.Struct(
732  cos_rot_sq=cos_rot_sq,
733  proj_ref_ctr_delta=proj_ref_delta,
734  shift_matrix=shift_matrix,)
735 
736  def _create_spherical_rotation_matrix(self, rot_axis, cos_rotation,
737  sin_rotion):
738  """Construct a generalized 3D rotation matrix about a given
739  axis.
740 
741  Parameters
742  ----------
743  rot_axis : `numpy.ndarray`, (3,)
744  3 vector defining the axis to rotate about.
745  cos_rotation : `float`
746  cosine of the rotation angle.
747  sin_rotation : `float`
748  sine of the rotation angle.
749 
750  Return
751  ------
752  shift_matrix : `numpy.ndarray`, (3, 3)
753  3x3 spherical, rotation matrix.
754  """
755 
756  rot_cross_matrix = np.array(
757  [[0., -rot_axis[2], rot_axis[1]],
758  [rot_axis[2], 0., -rot_axis[0]],
759  [-rot_axis[1], rot_axis[0], 0.]], dtype=np.float64)
760  shift_matrix = (cos_rotation*np.identity(3) +
761  sin_rotion*rot_cross_matrix +
762  (1. - cos_rotation)*np.outer(rot_axis, rot_axis))
763 
764  return shift_matrix
765 
766  def _create_pattern_spokes(self, src_ctr, src_delta_array, src_dist_array,
767  ref_ctr, ref_ctr_id, proj_ref_ctr_delta,
768  ref_delta_array, ref_dist_array,
769  ref_id_array, max_dist_rad, n_match):
770  """ Create the individual spokes that make up the pattern now that the
771  shift and rotation are within tolerance.
772 
773  If we can't create a valid pattern we exit early.
774 
775  Parameters
776  ----------
777  src_ctr : `numpy.ndarray`, (3,)
778  3 vector of the source pinwheel center
779  src_delta_array : `numpy.ndarray`, (N, 3)
780  Array of 3 vector deltas between the source center and the pairs
781  that make up the remaining spokes of the pinwheel
782  src_dist_array : `numpy.ndarray`, (N, 3)
783  Array of the distances of each src_delta in the pinwheel
784  ref_ctr : `numpy.ndarray`, (3,)
785  3 vector of the candidate reference center
786  ref_ctr_id : `int`
787  id of the ref_ctr in the master reference array
788  proj_ref_ctr_delta : `numpy.ndarray`, (3,)
789  Plane projected 3 vector formed from the center point of the
790  candidate pin-wheel and the second point in the pattern to create
791  the first spoke pair. This is the candidate pair that was matched
792  in the main _construct_pattern_and_shift_rot_matrix loop
793  ref_delta_array : `numpy.ndarray`, (N,3)
794  Array of 3 vector deltas that are have the current candidate
795  reference center as part of the pair
796  ref_dist_array : `numpy.ndarray`, (N,)
797  Array of vector distances for each of the reference pairs
798  ref_id_array : `numpy.ndarray`, (N,)
799  Array of id lookups into the master reference array that our
800  center id object is paired with.
801  max_dist_rad : `float`
802  Maximum search distance
803  n_match : `int`
804  Number of source deltas that must be matched into the reference
805  deltas in order to consider this a successful pattern match.
806 
807  Returns
808  -------
809  output_spokes : `lsst.pipe.base.Struct`
810  Result struct with components:
811 
812  - ``ref_spoke_list`` : list of ints specifying ids into the master
813  reference array (`list` of `int`).
814  - ``src_spoke_list`` : list of ints specifying indices into the
815  current source pattern that is being tested (`list` of `int`).
816  """
817  # Struct where we will be putting our results.
818  output_spokes = pipeBase.Struct(
819  ref_spoke_list=[],
820  src_spoke_list=[],)
821 
822  # Counter for number of spokes we failed to find a reference
823  # candidate for. We break the loop if we haven't found enough.
824  n_fail = 0
825  ref_spoke_list = []
826  src_spoke_list = []
827 
828  # Plane project the center/first spoke of the source pattern using
829  # the center vector of the pattern as normal.
830  proj_src_ctr_delta = (src_delta_array[0] -
831  np.dot(src_delta_array[0], src_ctr) * src_ctr)
832  proj_src_ctr_dist_sq = np.dot(proj_src_ctr_delta, proj_src_ctr_delta)
833 
834  # Pre-compute the squared length of the projected reference vector.
835  proj_ref_ctr_dist_sq = np.dot(proj_ref_ctr_delta, proj_ref_ctr_delta)
836 
837  # Loop over the source pairs.
838  for src_idx in range(1, len(src_dist_array)):
839  if n_fail > len(src_dist_array) - (n_match - 1):
840  break
841 
842  # Given our length tolerance we can use it to compute a tolerance
843  # on the angle between our spoke.
844  src_sin_tol = (max_dist_rad /
845  (src_dist_array[src_idx] + max_dist_rad))
846 
847  # Test if the small angle approximation will still hold. This is
848  # defined as when sin(theta) ~= theta to within 0.1% of each
849  # other. If the implied opening angle is too large we set it to
850  # the 0.1% threshold.
851  max_sin_tol = 0.0447
852  if src_sin_tol > max_sin_tol:
853  src_sin_tol = max_sin_tol
854 
855  # Plane project the candidate source spoke and compute the cosine
856  # and sine of the opening angle.
857  proj_src_delta = (
858  src_delta_array[src_idx] -
859  np.dot(src_delta_array[src_idx], src_ctr) * src_ctr)
860  geom_dist_src = np.sqrt(
861  np.dot(proj_src_delta, proj_src_delta) *
862  proj_src_ctr_dist_sq)
863 
864  # Compute cosine and sine of the delta vector opening angle.
865  cos_theta_src = (np.dot(proj_src_delta, proj_src_ctr_delta) /
866  geom_dist_src)
867  cross_src = (np.cross(proj_src_delta, proj_src_ctr_delta) /
868  geom_dist_src)
869  sin_theta_src = np.dot(cross_src, src_ctr)
870 
871  # Find the reference pairs that include our candidate pattern
872  # center and sort them in increasing delta
873  ref_dist_idx_array = self._find_candidate_reference_pairs(
874  src_dist_array[src_idx], ref_dist_array, max_dist_rad)
875 
876  # Test the spokes and return the id of the reference object.
877  # Return None if no match is found.
878  ref_id = self._test_spoke(
879  cos_theta_src,
880  sin_theta_src,
881  ref_ctr,
882  ref_ctr_id,
883  proj_ref_ctr_delta,
884  proj_ref_ctr_dist_sq,
885  ref_dist_idx_array,
886  ref_delta_array,
887  ref_id_array,
888  src_sin_tol)
889  if ref_id is None:
890  n_fail += 1
891  continue
892 
893  # Append the successful indices to our list. The src_idx needs
894  # an extra iteration to skip the first and second source objects.
895  ref_spoke_list.append(ref_id)
896  src_spoke_list.append(src_idx + 1)
897  # If we found enough reference objects we can return early. This is
898  # n_match - 2 as we already have 2 source objects matched into the
899  # reference data.
900  if len(ref_spoke_list) >= n_match - 2:
901  # Set the struct data and return the struct.
902  output_spokes.ref_spoke_list = ref_spoke_list
903  output_spokes.src_spoke_list = src_spoke_list
904  return output_spokes
905 
906  return output_spokes
907 
908  def _test_spoke(self, cos_theta_src, sin_theta_src, ref_ctr, ref_ctr_id,
909  proj_ref_ctr_delta, proj_ref_ctr_dist_sq,
910  ref_dist_idx_array, ref_delta_array,
911  ref_id_array, src_sin_tol):
912  """Test the opening angle between the first spoke of our pattern
913  for the source object against the reference object.
914 
915  This method makes heavy use of the small angle approximation to perform
916  the comparison.
917 
918  Parameters
919  ----------
920  cos_theta_src : `float`
921  Cosine of the angle between the current candidate source spoke and
922  the first spoke.
923  sin_theta_src : `float`
924  Sine of the angle between the current candidate source spoke and
925  the first spoke.
926  ref_ctr : `numpy.ndarray`, (3,)
927  3 vector of the candidate reference center
928  ref_ctr_id : `int`
929  id lookup of the ref_ctr into the master reference array
930  proj_ref_ctr_delta : `float`
931  Plane projected first spoke in the reference pattern using the
932  pattern center as normal.
933  proj_ref_ctr_dist_sq : `float`
934  Squared length of the projected vector.
935  ref_dist_idx_array : `numpy.ndarray`, (N,)
936  Indices sorted by the delta distance between the source
937  spoke we are trying to test and the candidate reference
938  spokes.
939  ref_delta_array : `numpy.ndarray`, (N, 3)
940  Array of 3 vector deltas that are have the current candidate
941  reference center as part of the pair
942  ref_id_array : `numpy.ndarray`, (N,)
943  Array of id lookups into the master reference array that our
944  center id object is paired with.
945  src_sin_tol : `float`
946  Sine of tolerance allowed between source and reference spoke
947  opening angles.
948 
949  Returns
950  -------
951  id : `int`
952  If we can not find a candidate spoke we return `None` else we
953  return an int id into the master reference array.
954  """
955 
956  # Loop over our candidate reference objects.
957  for ref_dist_idx in ref_dist_idx_array:
958  # Check the direction of the delta vector.
959  ref_sign = 1
960  if ref_id_array[ref_dist_idx] < ref_ctr_id:
961  ref_sign = -1
962 
963  # Compute the cos between our "center" reference vector and the
964  # current reference candidate.
965  proj_ref_delta = (
966  ref_delta_array[ref_dist_idx] -
967  np.dot(ref_delta_array[ref_dist_idx], ref_ctr) * ref_ctr)
968  geom_dist_ref = np.sqrt(proj_ref_ctr_dist_sq *
969  np.dot(proj_ref_delta, proj_ref_delta))
970  cos_theta_ref = ref_sign * (
971  np.dot(proj_ref_delta, proj_ref_ctr_delta) /
972  geom_dist_ref)
973 
974  # Make sure we can safely make the comparison in case
975  # our "center" and candidate vectors are mostly aligned.
976  if cos_theta_ref ** 2 < (1 - src_sin_tol ** 2):
977  cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
978  (1 - cos_theta_ref ** 2))
979  else:
980  cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
981  src_sin_tol ** 2)
982  # Test the difference of the cosine of the reference angle against
983  # the source angle. Assumes that the delta between the two is
984  # small.
985  if cos_sq_comparison > src_sin_tol ** 2:
986  continue
987 
988  # The cosine tests the magnitude of the angle but not
989  # its direction. To do that we need to know the sine as well.
990  # This cross product calculation does that.
991  cross_ref = ref_sign * (
992  np.cross(proj_ref_delta, proj_ref_ctr_delta) /
993  geom_dist_ref)
994  sin_theta_ref = np.dot(cross_ref, ref_ctr)
995 
996  # Check the value of the cos again to make sure that it is not
997  # near zero.
998  if abs(cos_theta_src) < src_sin_tol:
999  sin_comparison = (sin_theta_src - sin_theta_ref) / src_sin_tol
1000  else:
1001  sin_comparison = \
1002  (sin_theta_src - sin_theta_ref) / cos_theta_ref
1003 
1004  if abs(sin_comparison) > src_sin_tol:
1005  continue
1006 
1007  # Return the correct id of the candidate we found.
1008  return ref_id_array[ref_dist_idx]
1009 
1010  return None
1011 
1012  def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta,
1013  ref_ctr, ref_delta):
1014  """ Create the final part of our spherical rotation matrix.
1015 
1016  Parameters
1017  ----------
1018  cos_rot_sq : `float`
1019  cosine of the rotation needed to align our source and reference
1020  candidate patterns.
1021  shift_matrix : `numpy.ndarray`, (3, 3)
1022  3x3 rotation matrix for shifting the source pattern center on top
1023  of the candidate reference pattern center.
1024  src_delta : `numpy.ndarray`, (3,)
1025  3 vector delta of representing the first spoke of the source
1026  pattern
1027  ref_ctr : `numpy.ndarray`, (3,)
1028  3 vector on the unit-sphere representing the center of our
1029  reference pattern.
1030  ref_delta : `numpy.ndarray`, (3,)
1031  3 vector delta made by the first pair of the reference pattern.
1032 
1033  Returns
1034  -------
1035  result : `lsst.pipe.base.Struct`
1036  Result struct with components:
1037 
1038  - ``sin_rot`` : float sine of the amount of rotation between the
1039  source and reference pattern. We use sine here as it is
1040  signed and tells us the chirality of the rotation (`float`).
1041  - ``shift_rot_matrix`` : float array representing the 3x3 rotation
1042  matrix that takes the source pattern and shifts and rotates
1043  it to align with the reference pattern (`numpy.ndarray`, (3,3)).
1044  """
1045  cos_rot = np.sqrt(cos_rot_sq)
1046  rot_src_delta = np.dot(shift_matrix, src_delta)
1047  delta_dot_cross = np.dot(np.cross(rot_src_delta, ref_delta), ref_ctr)
1048 
1049  sin_rot = np.sign(delta_dot_cross) * np.sqrt(1 - cos_rot_sq)
1050  rot_matrix = self._create_spherical_rotation_matrix(
1051  ref_ctr, cos_rot, sin_rot)
1052 
1053  shift_rot_matrix = np.dot(rot_matrix, shift_matrix)
1054 
1055  return pipeBase.Struct(
1056  sin_rot=sin_rot,
1057  shift_rot_matrix=shift_rot_matrix,)
1058 
1059  def _intermediate_verify(self, src_pattern, ref_pattern, shift_rot_matrix,
1060  max_dist_rad):
1061  """ Perform an intermediate verify step.
1062 
1063  Rotate the matches references into the source frame and test their
1064  distances against tolerance. Only return true if all points are within
1065  tolerance.
1066 
1067  Parameters
1068  ----------
1069  src_pattern : `numpy.ndarray`, (N,3)
1070  Array of 3 vectors representing the points that make up our source
1071  pinwheel pattern.
1072  ref_pattern : `numpy.ndarray`, (N,3)
1073  Array of 3 vectors representing our candidate reference pinwheel
1074  pattern.
1075  shift_rot_matrix : `numpy.ndarray`, (3,3)
1076  3x3 rotation matrix that takes the source objects and rotates them
1077  onto the frame of the reference objects
1078  max_dist_rad : `float`
1079  Maximum distance allowed to consider two objects the same.
1080 
1081  Returns
1082  -------
1083  fit_shift_rot_matrix : `numpy.ndarray`, (3,3)
1084  Return the fitted shift/rotation matrix if all of the points in our
1085  source pattern are within max_dist_rad of their matched reference
1086  objects. Returns None if this criteria is not satisfied.
1087  """
1088  if len(src_pattern) != len(ref_pattern):
1089  raise ValueError(
1090  "Source pattern length does not match ref pattern.\n"
1091  "\t source pattern len=%i, reference pattern len=%i" %
1092  (len(src_pattern), len(ref_pattern)))
1093 
1095  src_pattern, ref_pattern, shift_rot_matrix, max_dist_rad):
1096  # Now that we know our initial shift and rot matrix is valid we
1097  # want to fit the implied transform using all points from
1098  # our pattern. This is a more robust rotation matrix as our
1099  # initial matrix only used the first 2 points from the source
1100  # pattern to estimate the shift and rotation. The matrix we fit
1101  # are allowed to be non unitary but need to preserve the length of
1102  # the rotated vectors to within the match tolerance.
1103  fit_shift_rot_matrix = least_squares(
1104  _rotation_matrix_chi_sq,
1105  x0=shift_rot_matrix.flatten(),
1106  args=(src_pattern, ref_pattern, max_dist_rad)
1107  ).x.reshape((3, 3))
1108  # Do another verify in case the fits have wondered off.
1110  src_pattern, ref_pattern, fit_shift_rot_matrix,
1111  max_dist_rad):
1112  return fit_shift_rot_matrix
1113 
1114  return None
1115 
1116  def _intermediate_verify_comparison(self, pattern_a, pattern_b,
1117  shift_rot_matrix, max_dist_rad):
1118  """Test the input rotation matrix against one input pattern and
1119  a second one.
1120 
1121  If every point in the pattern after rotation is within a distance of
1122  max_dist_rad to its candidate point in the other pattern, we return
1123  True.
1124 
1125  Parameters
1126  ----------
1127  pattern_a : `numpy.ndarray`, (N,3)
1128  Array of 3 vectors representing the points that make up our source
1129  pinwheel pattern.
1130  pattern_b : `numpy.ndarray`, (N,3)
1131  Array of 3 vectors representing our candidate reference pinwheel
1132  pattern.
1133  shift_rot_matrix : `numpy.ndarray`, (3,3)
1134  3x3 rotation matrix that takes the source objects and rotates them
1135  onto the frame of the reference objects
1136  max_dist_rad : `float`
1137  Maximum distance allowed to consider two objects the same.
1138 
1139 
1140  Returns
1141  -------
1142  bool
1143  True if all rotated source points are within max_dist_rad of
1144  the candidate references matches.
1145  """
1146  shifted_pattern_a = np.dot(shift_rot_matrix,
1147  pattern_a.transpose()).transpose()
1148  tmp_delta_array = shifted_pattern_a - pattern_b
1149  tmp_dist_array = (tmp_delta_array[:, 0] ** 2 +
1150  tmp_delta_array[:, 1] ** 2 +
1151  tmp_delta_array[:, 2] ** 2)
1152  return np.all(tmp_dist_array < max_dist_rad ** 2)
1153 
1154  def _test_pattern_lengths(self, test_pattern, max_dist_rad):
1155  """ Test that the all vectors in a pattern are unit length within
1156  tolerance.
1157 
1158  This is useful for assuring the non unitary transforms do not contain
1159  too much distortion.
1160 
1161  Parameters
1162  ----------
1163  test_pattern : `numpy.ndarray`, (N, 3)
1164  Test vectors at the maximum and minimum x, y, z extents.
1165  max_dist_rad : `float`
1166  maximum distance in radians to consider two points "agreeing" on
1167  a rotation
1168 
1169  Returns
1170  -------
1171  test : `bool`
1172  Length tests pass.
1173  """
1174  dists = (test_pattern[:, 0] ** 2 +
1175  test_pattern[:, 1] ** 2 +
1176  test_pattern[:, 2] ** 2)
1177  return np.all(
1178  np.logical_and((1 - max_dist_rad) ** 2 < dists,
1179  dists < (1 + max_dist_rad) ** 2))
1180 
1181  def _test_rotation_agreement(self, rot_vects, max_dist_rad):
1182  """ Test this rotation against the previous N found and return
1183  the number that a agree within tolerance to where our test
1184  points are.
1185 
1186  Parameters
1187  ----------
1188  rot_vects : `numpy.ndarray`, (N, 3)
1189  Arrays of rotated 3 vectors representing the maximum x, y,
1190  z extent on the unit sphere of the input source objects rotated by
1191  the candidate rotations into the reference frame.
1192  max_dist_rad : `float`
1193  maximum distance in radians to consider two points "agreeing" on
1194  a rotation
1195 
1196  Returns
1197  -------
1198  tot_consent : `int`
1199  Number of candidate rotations that agree for all of the rotated
1200  test 3 vectors.
1201  """
1202 
1203  self.log.debug("Comparing pattern %i to previous %i rotations..." %
1204  (rot_vects[-1][-1], len(rot_vects) - 1))
1205 
1206  tot_consent = 0
1207  for rot_idx in range(max((len(rot_vects) - 1), 0)):
1208  tmp_dist_list = []
1209  for vect_idx in range(len(rot_vects[rot_idx]) - 1):
1210  tmp_delta_vect = (rot_vects[rot_idx][vect_idx] -
1211  rot_vects[-1][vect_idx])
1212  tmp_dist_list.append(
1213  np.dot(tmp_delta_vect, tmp_delta_vect))
1214  if np.all(np.array(tmp_dist_list) < max_dist_rad ** 2):
1215  tot_consent += 1
1216  return tot_consent
1217 
1218  def _match_sources(self,
1219  source_array,
1220  shift_rot_matrix):
1221  """ Shift both the reference and source catalog to the the respective
1222  frames and find their nearest neighbor using a kdTree.
1223 
1224  Removes all matches who do not agree when either the reference or
1225  source catalog is rotated. Cuts on a maximum distance are left to an
1226  external function.
1227 
1228  Parameters
1229  ----------
1230  source_array : `numpy.ndarray`, (N, 3)
1231  array of 3 vectors representing the source objects we are trying
1232  to match into the source catalog.
1233  shift_rot_matrix : `numpy.ndarray`, (3, 3)
1234  3x3 rotation matrix that performs the spherical rotation from the
1235  source frame into the reference frame.
1236 
1237  Returns
1238  -------
1239  results : `lsst.pipe.base.Struct`
1240  Result struct with components:
1241 
1242  - ``matches`` : array of integer ids into the source and
1243  reference arrays. Matches are only returned for those that
1244  satisfy the distance and handshake criteria
1245  (`numpy.ndarray`, (N, 2)).
1246  - ``distances`` : Distances between each match in radians after
1247  the shift and rotation is applied (`numpy.ndarray`, (N)).
1248  """
1249  shifted_references = np.dot(
1250  np.linalg.inv(shift_rot_matrix),
1251  self._reference_array.transpose()).transpose()
1252  shifted_sources = np.dot(
1253  shift_rot_matrix,
1254  source_array.transpose()).transpose()
1255 
1256  ref_matches = np.empty((len(shifted_references), 2),
1257  dtype=np.uint32)
1258  src_matches = np.empty((len(shifted_sources), 2),
1259  dtype=np.uint32)
1260 
1261  ref_matches[:, 1] = np.arange(len(shifted_references),
1262  dtype=np.uint32)
1263  src_matches[:, 0] = np.arange(len(shifted_sources),
1264  dtype=np.uint32)
1265 
1266  ref_kdtree = cKDTree(self._reference_array)
1267  src_kdtree = cKDTree(source_array)
1268 
1269  ref_to_src_dist, tmp_ref_to_src_idx = \
1270  src_kdtree.query(shifted_references)
1271  src_to_ref_dist, tmp_src_to_ref_idx = \
1272  ref_kdtree.query(shifted_sources)
1273 
1274  ref_matches[:, 0] = tmp_ref_to_src_idx
1275  src_matches[:, 1] = tmp_src_to_ref_idx
1276 
1277  handshake_mask = self._handshake_match(src_matches, ref_matches)
1278  return pipeBase.Struct(
1279  match_ids=src_matches[handshake_mask],
1280  distances_rad=src_to_ref_dist[handshake_mask],)
1281 
1282  def _handshake_match(self, matches_src, matches_ref):
1283  """Return only those matches where both the source
1284  and reference objects agree they they are each others'
1285  nearest neighbor.
1286 
1287  Parameters
1288  ----------
1289  matches_src : `numpy.ndarray`, (N, 2)
1290  int array of nearest neighbor matches between shifted and
1291  rotated reference objects matched into the sources.
1292  matches_ref : `numpy.ndarray`, (N, 2)
1293  int array of nearest neighbor matches between shifted and
1294  rotated source objects matched into the references.
1295  Return
1296  ------
1297  handshake_mask_array : `numpy.ndarray`, (N,)
1298  Return the array positions where the two match catalogs agree.
1299  """
1300  handshake_mask_array = np.zeros(len(matches_src), dtype=np.bool)
1301 
1302  for src_match_idx, match in enumerate(matches_src):
1303  ref_match_idx = np.searchsorted(matches_ref[:, 1], match[1])
1304  if match[0] == matches_ref[ref_match_idx, 0]:
1305  handshake_mask_array[src_match_idx] = True
1306  return handshake_mask_array
def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta, ref_ctr, ref_delta)
def _construct_pattern_and_shift_rot_matrix(self, src_pattern_array, n_match, max_cos_theta_shift, max_cos_rot_sq, max_dist_rad)
def _test_spoke(self, cos_theta_src, sin_theta_src, ref_ctr, ref_ctr_id, proj_ref_ctr_delta, proj_ref_ctr_dist_sq, ref_dist_idx_array, ref_delta_array, ref_id_array, src_sin_tol)
def _test_rotation(self, src_center, ref_center, src_delta, ref_delta, cos_shift, max_cos_rot_sq)
def _intermediate_verify_comparison(self, pattern_a, pattern_b, shift_rot_matrix, max_dist_rad)
def _create_pattern_spokes(self, src_ctr, src_delta_array, src_dist_array, ref_ctr, ref_ctr_id, proj_ref_ctr_delta, ref_delta_array, ref_dist_array, ref_id_array, max_dist_rad, n_match)
def match(self, source_array, n_check, n_match, n_agree, max_n_patterns, max_shift, max_rotation, max_dist, min_matches, pattern_skip_array=None)
def _intermediate_verify(self, src_pattern, ref_pattern, shift_rot_matrix, max_dist_rad)