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