2 from __future__
import division, print_function, absolute_import
5 from scipy.optimize
import least_squares
6 from scipy.spatial
import cKDTree
8 import lsst.pipe.base
as pipeBase
11 def _rotation_matrix_chi_sq(flattened_rot_matrix,
15 """Compute the squared differences for least squares fitting. 17 Given a flattened rotation matrix, one N point pattern and another N point 18 pattern to transform into to, compute the squared differences between the 19 points in the two patterns after the rotation. 23 flattened_rot_matrix : float array 24 A flattened array representing a 3x3 rotation matrix. The array is 25 flattened to comply with the API of scipy.optimize.least_squares. 26 Flattened elements are [[0, 0], [0, 1], [0, 2], [1, 0]...] 27 pattern_a : float array of 3 vectors 28 A array containing N, 3 vectors representing the objects we would like 29 to transform into the frame of pattern_b. 30 pattern_b : float array of 3 vectors 31 A array containing N, 3 vectors representing the reference frame we 32 would like to transform pattern_a into. 34 The maximum distance allowed from the pattern matching. This value is 35 used as the standard error for the resultant chi values. 40 Array of differences between the vectors representing of the source 41 pattern rotated into the reference frame and the converse. This is 42 used to minimize in a least squares fitter. 45 rot_matrix = flattened_rot_matrix.reshape((3, 3))
47 rot_pattern_a = np.dot(rot_matrix, pattern_a.transpose()).transpose()
48 diff_pattern_a_to_b = rot_pattern_a - pattern_b
51 rot_pattern_b = np.dot(np.linalg.inv(rot_matrix),
52 pattern_b.transpose()).transpose()
53 diff_pattern_b_to_a = rot_pattern_b - pattern_a
56 return np.concatenate(
57 (diff_pattern_a_to_b.flatten() / max_dist_rad,
58 diff_pattern_b_to_a.flatten() / max_dist_rad))
62 """ Class implementing a pessimistic version of Optimistic Pattern Matcher 63 B (OPMb) from Tabur 2007. The class loads and stores the reference object 64 in a convenient data structure for matching any set of source objects that 65 are assumed to contain each other. The pessimistic nature of the algorithm 66 comes from requiring that it discovers at least two patterns that agree on 67 the correct shift and rotation for matching before exiting. The original 68 behavior of OPMb can be recovered simply. Patterns matched between the 69 input datasets are n-spoked pinwheels created from n+1 points. Refer to 70 DMTN #031 for more details. http://github.com/lsst-dm/dmtn-031 71 -------------------------------------------------------------------------- 73 reference_array : float array 74 spherical points x, y, z of to use as reference objects for 76 log : an lsst.log instance 77 pair_id_array : int array 78 Internal lookup table. Given an id in the reference array, return 79 an array of the id pair that contains this object's id sorted on 80 the distance to the pairs. 81 pair_delta_array : float array 82 Internal lookup table. Given an id in the reference array, return 83 an array of the 3 vector deltas of all other pairs sorted on their 85 pair_dist_array : float array 86 Internal lookup table. Given an id in the reference return an array 87 of pair distances of all other pairs sorted on distance. 88 dist_array : float array 89 Array of all pairs of objects in the reference array sorted on 92 Array of id pairs that lookup into the reference array sorted 94 delta_array : float array 95 Array of 3 vector deltas for each pair in the reference array 96 sorted on pair distance. 103 reference_array : float array 104 Array of spherical points x, y, z to use as reference objects. 106 logger object for reporting warnings and failures. 114 def _build_distances_and_angles(self):
115 """ Create the data structures we will use to search for our pattern 118 Throughout this function and the rest of the 120 class we use id to reference the position in the input reference 121 catalog and index to 'index' into the arrays sorted on distance. 146 sub_id_array_list = []
147 sub_delta_array_list = []
148 sub_dist_array_list = []
155 sub_id_array = np.zeros((self.
_n_reference - 1 - ref_id, 2),
157 sub_id_array[:, 0] = ref_id
158 sub_id_array[:, 1] = np.arange(ref_id + 1, self.
_n_reference,
165 sub_dist_array = np.sqrt(sub_delta_array[:, 0] ** 2 +
166 sub_delta_array[:, 1] ** 2 +
167 sub_delta_array[:, 2] ** 2)
171 sub_id_array_list.append(sub_id_array)
172 sub_delta_array_list.append(sub_delta_array)
173 sub_dist_array_list.append(sub_dist_array)
193 ref_id, sorted_pair_dist_args]
195 ref_id, sorted_pair_dist_args]
197 ref_id, sorted_pair_dist_args, :]
200 unsorted_id_array = np.concatenate(sub_id_array_list)
201 unsorted_delta_array = np.concatenate(sub_delta_array_list)
202 unsorted_dist_array = np.concatenate(sub_dist_array_list)
206 sorted_dist_args = unsorted_dist_array.argsort()
207 self.
_dist_array = unsorted_dist_array[sorted_dist_args]
208 self.
_id_array = unsorted_id_array[sorted_dist_args]
209 self.
_delta_array = unsorted_delta_array[sorted_dist_args]
213 def match(self, source_array, n_check, n_match, n_agree,
214 max_n_patterns, max_shift, max_rotation, max_dist,
215 min_matches, pattern_skip_array=None):
216 """Match a given source catalog into the loaded reference catalog. 218 Given array of points on the unit sphere and tolerances, we 219 attempt to match a pinwheel like pattern between these input sources 220 and the reference objects this class was created with. This pattern 221 informs of the shift and rotation needed to align the input source 222 objects into the frame of the references. 226 source_array: float array 227 An array of spherical x,y,z coordinates and a magnitude in units 228 of objects having a lower value for sorting. The array should be 231 Number of sources to create a pattern from. Not all objects may be 232 checked if n_match criteria is before looping through all n_check 235 Number of objects to use in constructing a pattern to match. 237 Number of found patterns that must agree on their shift and 238 rotation before exiting. Set this value to 1 to recover the 239 expected behavior of Optimistic Pattern Matcher B. 240 max_n_patters : int value 241 Number of patterns to create from the input source objects to 242 attempt to match into the reference objects. 243 max_shift: float value 244 Maximum allowed shift to match patterns in arcseconds. 245 max_rotation: float value 246 Maximum allowed rotation between patterns in degrees. 247 max_dist: float value 248 Maximum distance in arcseconds allowed between candidate spokes in 249 the source and reference objects. Also sets that maximum distance 250 in the intermediate verify, pattern shift/rotation agreement, and 252 pattern_skip_array: int array 253 Patterns we would like to skip. This could be due to the pattern 254 being matched on a previous iteration that we now consider invalid. 255 This assumes the ordering of the source objects is the same 256 between different runs of the matcher which, assuming no object 257 has been inserted or the magnitudes have changed, it should be. 261 output_struct : pipe.base.struct 262 A lsst.pipe.base struct containing the following outputs. 265 (N, 2) array of matched ids for pairs. Empty list if no 267 distances_rad : float array 268 Radian distances between the matched objects. Empty list 271 Index of matched pattern. None if no match found. 273 Magnitude for the shift between the source and 274 reference objects in arcseconds. None if no match found. 278 sorted_source_array = source_array[source_array[:, -1].argsort(), :3]
279 n_source = len(sorted_source_array)
282 output_match_struct = pipeBase.Struct(
289 self.
log.warn(
"Source object array is empty. Unable to match. " 303 max_cos_shift = np.cos(np.radians(max_shift / 3600.))
304 max_cos_rot_sq = np.cos(np.radians(max_rotation)) ** 2
305 max_dist_rad = np.radians(max_dist / 3600.)
309 for pattern_idx
in range(np.min((max_n_patterns,
310 n_source - n_match))):
314 if pattern_skip_array
is not None and \
315 np.any(pattern_skip_array == pattern_idx):
317 "Skipping previously matched bad pattern %i..." %
321 pattern = sorted_source_array[
322 pattern_idx: np.min((pattern_idx + n_check, n_source)), :3]
327 construct_return_struct = \
329 pattern, n_match, max_cos_shift, max_cos_rot_sq,
333 if construct_return_struct.ref_candidates
is None or \
334 construct_return_struct.shift_rot_matrix
is None or \
335 construct_return_struct.cos_shift
is None or \
336 construct_return_struct.sin_rot
is None:
340 ref_candidates = construct_return_struct.ref_candidates
341 shift_rot_matrix = construct_return_struct.shift_rot_matrix
342 cos_shift = construct_return_struct.cos_shift
343 sin_rot = construct_return_struct.sin_rot
347 if len(ref_candidates) < n_match:
353 tmp_rot_vect_list = []
354 for test_vect
in test_vectors:
355 tmp_rot_vect_list.append(np.dot(shift_rot_matrix, test_vect))
362 tmp_rot_vect_list.append(pattern_idx)
363 rot_vect_list.append(tmp_rot_vect_list)
375 n_matched = len(match_sources_struct.match_ids[
376 match_sources_struct.distances_rad < max_dist_rad])
379 if n_matched >= min_matches:
381 shift = np.degrees(np.arccos(cos_shift)) * 3600.
383 self.
log.debug(
"Succeeded after %i patterns." % pattern_idx)
384 self.
log.debug(
"\tShift %.4f arcsec" % shift)
385 self.
log.debug(
"\tRotation: %.4f deg" %
386 np.degrees(np.arcsin(sin_rot)))
389 output_match_struct.match_ids = \
390 match_sources_struct.match_ids
391 output_match_struct.distances_rad = \
392 match_sources_struct.distances_rad
393 output_match_struct.pattern_idx = pattern_idx
394 output_match_struct.shift = shift
395 return output_match_struct
397 self.
log.warn(
"Failed after %i patterns." % (pattern_idx + 1))
398 return output_match_struct
400 def _compute_test_vectors(self, source_array):
401 """Compute spherical 3 vectors at the edges of the x, y, z extent 402 of the input source catalog. 406 source_array : float array (N, 3) 407 array of 3 vectors representing positions on the unit 412 float array of 3 vectors 413 Array of vectors representing the maximum extents in x, y, z 414 of the input source array. These are used with the rotations 415 the code finds to test for agreement from different patterns 416 when the code is running in pessimistic mode. 420 if np.any(np.logical_not(np.isfinite(source_array))):
421 self.
log.warn(
"Input source objects contain non-finite values. " 422 "This could end badly.")
423 center_vect = np.nanmean(source_array, axis=0)
427 xbtm_vect = np.array([np.min(source_array[:, 0]), center_vect[1],
428 center_vect[2]], dtype=np.float64)
429 xtop_vect = np.array([np.max(source_array[:, 0]), center_vect[1],
430 center_vect[2]], dtype=np.float64)
431 xbtm_vect /= np.sqrt(np.dot(xbtm_vect, xbtm_vect))
432 xtop_vect /= np.sqrt(np.dot(xtop_vect, xtop_vect))
434 ybtm_vect = np.array([center_vect[0], np.min(source_array[:, 1]),
435 center_vect[2]], dtype=np.float64)
436 ytop_vect = np.array([center_vect[0], np.max(source_array[:, 1]),
437 center_vect[2]], dtype=np.float64)
438 ybtm_vect /= np.sqrt(np.dot(ybtm_vect, ybtm_vect))
439 ytop_vect /= np.sqrt(np.dot(ytop_vect, ytop_vect))
441 zbtm_vect = np.array([center_vect[0], center_vect[1],
442 np.min(source_array[:, 2])], dtype=np.float64)
443 ztop_vect = np.array([center_vect[0], center_vect[1],
444 np.max(source_array[:, 2])], dtype=np.float64)
445 zbtm_vect /= np.sqrt(np.dot(zbtm_vect, zbtm_vect))
446 ztop_vect /= np.sqrt(np.dot(ztop_vect, ztop_vect))
449 return np.array([xbtm_vect, xtop_vect, ybtm_vect, ytop_vect,
450 zbtm_vect, ztop_vect])
452 def _construct_pattern_and_shift_rot_matrix(self, src_pattern_array,
453 n_match, max_cos_theta_shift,
454 max_cos_rot_sq, max_dist_rad):
455 """Test an input source pattern against the reference catalog. 457 Returns the candidate matched patterns and their 458 implied rotation matrices or None. 462 src_pattern_array : float array 463 Sub selection of source 3 vectors to create a pattern from 465 Number of points to attempt to create a pattern from. Must be 466 >= len(src_pattern_array) 467 max_cos_theta_shift : float 468 Maximum shift allowed between two patterns' centers. 469 max_cos_rot_sq : float 470 Maximum rotation between two patterns that have been shifted 471 to have their centers on top of each other. 473 Maximum delta distance allowed between the source and reference 474 pair distances to consider the reference pair a candidate for 475 the source pair. Also sets the tolerance between the opening 476 angles of the spokes when compared to the reference. 480 lsst.pipe.base.Struct 481 Return a Struct containing the following data: 483 ref_candidates : list of ints 484 ids of the matched pattern in the internal reference_array 486 src_candidates : list of ints 487 Pattern ids of the sources matched. 488 shift_rot_matrix_src_to_ref : float array 489 3x3 matrix specifying the full shift and rotation between the 490 reference and source objects. Rotates 491 source into reference frame. None if match is not found. 492 shift_rot_matrix_ref_to_src : float array 493 3x3 matrix specifying the full shift 494 and rotation of the reference and source objects. Rotates 495 reference into source frame. None if match is not found. 497 Magnitude of the shift found between the two patten 498 centers. None if match is not found. 499 sin_rot : float value of the rotation to align the already shifted 500 source pattern to the reference pattern. None if no match 508 output_matched_pattern = pipeBase.Struct(
511 shift_rot_matrix=
None,
517 src_delta_array = np.empty((len(src_pattern_array) - 1, 3))
518 src_delta_array[:, 0] = (src_pattern_array[1:, 0] -
519 src_pattern_array[0, 0])
520 src_delta_array[:, 1] = (src_pattern_array[1:, 1] -
521 src_pattern_array[0, 1])
522 src_delta_array[:, 2] = (src_pattern_array[1:, 2] -
523 src_pattern_array[0, 2])
524 src_dist_array = np.sqrt(src_delta_array[:, 0]**2 +
525 src_delta_array[:, 1]**2 +
526 src_delta_array[:, 2]**2)
535 for ref_dist_idx
in ref_dist_index_array:
539 tmp_ref_pair_list = self.
_id_array[ref_dist_idx]
540 for pair_idx, ref_id
in enumerate(tmp_ref_pair_list):
541 src_candidates = [0, 1]
543 shift_rot_matrix =
None 550 cos_shift = np.dot(src_pattern_array[0], ref_center)
551 if cos_shift < max_cos_theta_shift:
555 ref_candidates.append(ref_id)
561 ref_candidates.append(
562 tmp_ref_pair_list[1])
564 ref_candidates.append(
565 tmp_ref_pair_list[0])
574 src_pattern_array[0], ref_center, src_delta_array[0],
575 ref_delta, cos_shift, max_cos_rot_sq)
576 if test_rot_struct.cos_rot_sq
is None or \
577 test_rot_struct.shift_matrix
is None:
581 cos_rot_sq = test_rot_struct.cos_rot_sq
582 shift_matrix = test_rot_struct.shift_matrix
595 src_pattern_array[0], src_delta_array, src_dist_array,
597 ref_dist, tmp_ref_delta_array, tmp_ref_dist_arary,
598 tmp_ref_id_array, max_dist_rad,
603 if len(pattern_spoke_struct.ref_spoke_list) < n_match - 2
or \
604 len(pattern_spoke_struct.src_spoke_list) < n_match - 2:
608 ref_candidates.extend(pattern_spoke_struct.ref_spoke_list)
609 src_candidates.extend(pattern_spoke_struct.src_spoke_list)
615 cos_rot_sq, shift_matrix, src_delta_array[0],
619 if shift_rot_struct.sin_rot
is None or \
620 shift_rot_struct.shift_rot_matrix
is None:
624 sin_rot = shift_rot_struct.sin_rot
625 shift_rot_matrix = shift_rot_struct.shift_rot_matrix
634 src_pattern_array[src_candidates],
636 shift_rot_matrix, max_dist_rad)
638 if fit_shift_rot_matrix
is not None:
640 output_matched_pattern.ref_candidates = ref_candidates
641 output_matched_pattern.src_candidates = src_candidates
642 output_matched_pattern.shift_rot_matrix = \
644 output_matched_pattern.cos_shift = cos_shift
645 output_matched_pattern.sin_rot = sin_rot
646 return output_matched_pattern
648 return output_matched_pattern
650 def _find_candidate_reference_pairs(self, src_dist, ref_dist_array,
652 """Wrap numpy.searchsorted to find the range of reference spokes 653 within a spoke distance tolerance of our source spoke. 655 Returns an array sorted from the smallest absolute delta distance 656 between source and reference spoke length. This sorting increases the 657 speed for the pattern search greatly. 661 src_dist : float radians 662 float value of the distance we would like to search for in 663 the reference array in radians. 664 ref_dist_array : float array 665 sorted array of distances in radians. 667 maximum plus/minus search to find in the reference array in 673 indices lookup into the input ref_dist_array sorted by the 674 difference in value to the src_dist from absolute value 679 start_idx = np.searchsorted(ref_dist_array, src_dist - max_dist_rad)
680 end_idx = np.searchsorted(ref_dist_array, src_dist + max_dist_rad,
684 if start_idx == end_idx:
690 if end_idx > ref_dist_array.shape[0]:
691 end_idx = ref_dist_array.shape[0]
696 tmp_diff_array = np.fabs(ref_dist_array[start_idx:end_idx] - src_dist)
697 return tmp_diff_array.argsort() + start_idx
699 def _test_rotation(self, src_center, ref_center, src_delta, ref_delta,
700 cos_shift, max_cos_rot_sq):
701 """ Test if the rotation implied between the source 702 pattern and reference pattern is within tolerance. To test this 703 we need to create the first part of our spherical rotation matrix 704 which we also return for use later. 708 src_center : float array3 710 ref_center : float array 711 3 vector defining the center of the candidate reference pinwheel 713 src_delta : float array 714 3 vector delta between the source pattern center and the end of 716 ref_delta : float array 717 3 vector delta of the candidate matched reference pair 719 Cosine of the angle between the source and reference candidate 721 max_cos_rot_sq : float 722 candidate reference pair after shifting the centers on top of each 723 other. The function will return None if the rotation implied is 724 greater than max_cos_rot_sq. 728 lsst.pipe.base.Struct 729 Return a pipe.base.Struct containing the following data. 732 magnitude of the rotation needed to align the two patterns 733 after their centers are shifted on top of each other. 734 None if rotation test fails. 735 shift_matrix : float array 736 3x3 rotation matrix describing the shift needed to align 737 the source and candidate reference center. 738 None if rotation test fails. 744 elif cos_shift < -1.0:
746 sin_shift = np.sqrt(1 - cos_shift ** 2)
752 rot_axis = np.cross(src_center, ref_center)
753 rot_axis /= sin_shift
755 rot_axis, cos_shift, sin_shift)
757 shift_matrix = np.identity(3)
761 rot_src_delta = np.dot(shift_matrix, src_delta)
762 cos_rot_sq = (np.dot(rot_src_delta, ref_delta) ** 2 /
763 (np.dot(rot_src_delta, rot_src_delta) *
764 np.dot(ref_delta, ref_delta)))
766 if cos_rot_sq < max_cos_rot_sq:
767 return pipeBase.Struct(
770 return pipeBase.Struct(
771 cos_rot_sq=cos_rot_sq,
772 shift_matrix=shift_matrix,)
774 def _create_spherical_rotation_matrix(self, rot_axis, cos_rotation,
776 """Construct a generalized 3D rotation matrix about a given 781 rot_axis : float array 782 3 vector defining the axis to rotate about. 784 cosine of the rotation angle. 786 sine of the rotation angle. 791 3x3 spherical, rotation matrix. 794 rot_cross_matrix = np.array(
795 [[0., -rot_axis[2], rot_axis[1]],
796 [rot_axis[2], 0., -rot_axis[0]],
797 [-rot_axis[1], rot_axis[0], 0.]], dtype=np.float64)
798 shift_matrix = (cos_rotation*np.identity(3) +
799 sin_rotion*rot_cross_matrix +
800 (1. - cos_rotation)*np.outer(rot_axis, rot_axis))
804 def _create_pattern_spokes(self, src_ctr, src_delta_array, src_dist_array,
805 ref_ctr, ref_ctr_id, ref_delta, ref_dist,
806 ref_delta_array, ref_dist_array,
807 ref_id_array, max_dist_rad, n_match):
808 """ Create the individual spokes that make up the pattern now that the 809 shift and rotation are within tolerance. 811 If we can't create a valid pattern we exit early. 815 src_ctr : float array 816 3 vector of the source pinwheel center 817 src_delta_array : float array 818 Array of 3 vector deltas between the source center and the pairs 819 that make up the remaining spokes of the pinwheel 820 src_dist_array : float array 821 Array of the distances of each src_delta in the pinwheel 822 ref_ctr : float array 823 3 vector of the candidate reference center 825 id of the ref_ctr in the master reference array 826 ref_delta : float array 827 3 vector of the first candidate pair of the pinwheel. This is 828 the candidate pair that was matched in the 829 main _construct_pattern_and_shift_rot_matrix loop 831 Radian distance of the first candidate reference pair 832 ref_delta_array : float array 833 Array of 3 vector deltas that are have the current candidate 834 reference center as part of the pair 835 ref_dist_array : float array 836 Array of vector distances for each of the reference pairs 837 ref_id_array : int array 838 Array of id lookups into the master reference array that our 839 center id object is paired with. 841 Maximum search distance 843 Number of source deltas that must be matched into the reference 844 deltas in order to consider this a successful pattern match. 848 lsst.pipe.base.Struct 849 The Struct contains the following data: 851 ref_spoke_list : list of ints specifying ids into the master 853 src_spoke_list : list of ints specifying indices into the current 854 source pattern that is being tested. 857 output_spokes = pipeBase.Struct(
867 for src_idx
in range(1, len(src_dist_array)):
868 if n_fail > len(src_dist_array) - (n_match - 1):
873 src_sin_tol = (max_dist_rad /
874 (src_dist_array[src_idx] + max_dist_rad))
879 if src_sin_tol > 0.0447:
886 src_dist_array[src_idx], ref_dist_array, max_dist_rad)
891 src_ctr, src_delta_array[src_idx], src_dist_array[src_idx],
892 src_delta_array[0], src_dist_array[0], ref_ctr, ref_ctr_id,
893 ref_delta, ref_dist, ref_dist_idx_array, ref_delta_array,
895 ref_id_array, src_sin_tol)
902 ref_spoke_list.append(ref_id)
903 src_spoke_list.append(src_idx + 1)
907 if len(ref_spoke_list) >= n_match - 2:
909 output_spokes.ref_spoke_list = ref_spoke_list
910 output_spokes.src_spoke_list = src_spoke_list
915 def _test_spoke(self, src_ctr, src_delta, src_dist, src_ctr_delta,
916 src_ctr_dist, ref_ctr, ref_ctr_id, ref_delta, ref_dist,
917 ref_dist_idx_array, ref_delta_array, ref_dist_array,
918 ref_id_array, src_sin_tol):
919 """Test the opening angle between the first spoke of our pattern 920 for the source object against the reference object. 922 This method makes heavy use of the small angle approximation to perform 927 src_ctr : float array 928 3 vector of the source pinwheel center 929 src_delta : float array 930 3 vector delta from the source center and the source object that 931 makes up the current spoke of the pinwheel we are testing. 932 src_dist : float array 933 Distance of the current spoke we are testing 934 src_ctr_delta : float array 935 3 vector delta between the center of the pattern and the first 936 spoke of the pattern. Used to test compute the opening angle 937 between the current spoke and the first spoke. 939 Distance between the pairs that make up src_ctr_delta 940 ref_ctr : float array 941 3 vector of the candidate reference center 943 id lookup of the ref_ctr into the master reference array 944 ref_delta : float array 945 3 vector of the first candidate pair of the pinwheel. That is 946 the candidate pair that was matched in the 947 main _construct_pattern_and_shift_rot_matrix loop 949 Radian distance of the first candidate reference pair 950 ref_dist_idx_array : int array 951 Indices sorted by the delta distance between the source 952 spoke we are trying to test and the candidate reference 954 ref_delta_array : float array 955 Array of 3 vector deltas that are have the current candidate 956 reference center as part of the pair 957 ref_dist_array : float array 958 Array of vector distances for each of the reference pairs 959 ref_id_array : int array 960 Array of id lookups into the master reference array that our 961 center id object is paired with. 963 Sine of tolerance allowed between source and reference spoke 969 If we can not find a candidate spoke we return None else we 970 return an int id into the master reference array. 975 cos_theta_src = (np.dot(src_delta, src_ctr_delta) /
976 (src_dist * src_ctr_dist))
977 cross_src = (np.cross(src_delta, src_ctr_delta) /
978 (src_dist * src_ctr_dist))
979 dot_cross_src = np.dot(cross_src, src_ctr)
982 for ref_dist_idx
in ref_dist_idx_array:
985 if ref_id_array[ref_dist_idx] < ref_ctr_id:
990 cos_theta_ref = ref_sign * (
991 np.dot(ref_delta_array[ref_dist_idx], ref_delta) /
992 (ref_dist_array[ref_dist_idx] * ref_dist))
996 if cos_theta_ref ** 2 < (1 - src_sin_tol ** 2):
997 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
998 (1 - cos_theta_ref ** 2))
1000 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
1005 if cos_sq_comparison > src_sin_tol ** 2:
1011 cross_ref = ref_sign * (
1012 np.cross(ref_delta_array[ref_dist_idx], ref_delta) /
1013 (ref_dist_array[ref_dist_idx] * ref_dist))
1014 dot_cross_ref = np.dot(cross_ref, ref_ctr)
1018 if abs(cos_theta_src) < src_sin_tol:
1019 sin_comparison = (dot_cross_src - dot_cross_ref) / src_sin_tol
1022 (dot_cross_src - dot_cross_ref) / cos_theta_ref
1024 if abs(sin_comparison) > src_sin_tol:
1028 return ref_id_array[ref_dist_idx]
1032 def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta,
1033 ref_ctr, ref_delta):
1034 """ Create the final part of our spherical rotation matrix. 1039 cosine of the rotation needed to align our source and reference 1041 shift_matrix : float array 1042 3x3 rotation matrix for shifting the source pattern center on top 1043 of the candidate reference pattern center. 1044 src_delta : float array 1045 3 vector delta of representing the first spoke of the source 1047 ref_ctr : float array 1048 3 vector on the unit-sphere representing the center of our 1050 ref_delta : float array 1051 3 vector delta made by the first pair of the reference pattern. 1055 lsst.pipe.base.Struct 1056 Struct object containing the following data: 1058 sin_rot : float sine of the amount of rotation between the 1059 source and reference pattern. We use sine here as it is 1060 signed and tells us the chirality of the rotation. 1061 shift_rot_matrix : float array representing the 3x3 rotation 1062 matrix that takes the source pattern and shifts and rotates 1063 it to align with the reference pattern. 1065 cos_rot = np.sqrt(cos_rot_sq)
1066 rot_src_delta = np.dot(shift_matrix, src_delta)
1067 delta_dot_cross = np.dot(np.cross(rot_src_delta, ref_delta), ref_ctr)
1069 sin_rot = np.sign(delta_dot_cross) * np.sqrt(1 - cos_rot_sq)
1071 ref_ctr, cos_rot, sin_rot)
1073 shift_rot_matrix = np.dot(rot_matrix, shift_matrix)
1075 return pipeBase.Struct(
1077 shift_rot_matrix=shift_rot_matrix,)
1079 def _intermediate_verify(self, src_pattern, ref_pattern, shift_rot_matrix,
1081 """ Perform an intermediate verify step. 1083 Rotate the matches references into the source frame and test their 1084 distances against tolerance. Only return true if all points are within 1089 src_pattern : float array 1090 Array of 3 vectors representing the points that make up our source 1092 ref_pattern : float array 1093 Array of 3 vectors representing our candidate reference pinwheel 1095 shift_rot_matrix : float array 1096 3x3 rotation matrix that takes the source objects and rotates them 1097 onto the frame of the reference objects 1098 max_dist_rad : float 1099 Maximum distance allowed to consider two objects the same. 1104 Return the fitted shift/rotation matrix if all of the points in our 1105 source pattern are within max_dist_rad of their matched reference 1106 objects. Returns None if this criteria is not satisfied. 1108 if len(src_pattern) != len(ref_pattern):
1110 "Source pattern length does not match ref pattern.\n" 1111 "\t source pattern len=%i, reference pattern len=%i" %
1112 (len(src_pattern), len(ref_pattern)))
1115 src_pattern, ref_pattern, shift_rot_matrix, max_dist_rad):
1123 fit_shift_rot_matrix = least_squares(
1124 _rotation_matrix_chi_sq,
1125 x0=shift_rot_matrix.flatten(),
1132 src_pattern, ref_pattern, fit_shift_rot_matrix,
1134 return fit_shift_rot_matrix
1138 def _intermediate_verify_comparison(self, pattern_a, pattern_b,
1139 shift_rot_matrix, max_dist_rad):
1140 """Test the input rotation matrix against one input pattern and 1143 If every point in the pattern after rotation is within a distance of 1144 max_dist_rad to its candidate point in the other pattern, we return 1149 pattern_a : float array 1150 Array of 3 vectors representing the points that make up our source 1152 pattern_b : float array 1153 Array of 3 vectors representing our candidate reference pinwheel 1155 shift_rot_matrix : float array 1156 3x3 rotation matrix that takes the source objects and rotates them 1157 onto the frame of the reference objects 1158 max_dist_rad : float 1159 Maximum distance allowed to consider two objects the same. 1165 True if all rotated source points are within max_dist_rad of 1166 the candidate references matches. 1168 shifted_pattern_a = np.dot(shift_rot_matrix,
1169 pattern_a.transpose()).transpose()
1170 tmp_delta_array = shifted_pattern_a - pattern_b
1171 tmp_dist_array = (tmp_delta_array[:, 0] ** 2 +
1172 tmp_delta_array[:, 1] ** 2 +
1173 tmp_delta_array[:, 2] ** 2)
1174 return np.all(tmp_dist_array < max_dist_rad ** 2)
1176 def _test_pattern_lengths(self, test_pattern, max_dist_rad):
1177 """ Test that the all vectors in a pattern are unit length within 1180 This is useful for assuring the non unitary transforms do not contain 1181 too much distortion. 1183 dists = (test_pattern[:, 0] ** 2 +
1184 test_pattern[:, 1] ** 2 +
1185 test_pattern[:, 2] ** 2)
1187 np.logical_and((1 - max_dist_rad) ** 2 < dists,
1188 dists < (1 + max_dist_rad) ** 2))
1190 def _test_rotation_agreement(self, rot_vects, max_dist_rad):
1191 """ Test this rotation against the previous N found and return 1192 the number that a agree within tolerance to where our test 1197 rot_vects : float array 1198 Arrays of rotated 3 vectors representing the maximum x, y, 1199 z extent on the unit sphere of the input source objects rotated by 1200 the candidate rotations into the reference frame. 1201 max_dist_rad : float 1202 maximum distance in radians to consider two points "agreeing" on 1208 Number of candidate rotations that agree for all of the rotated 1212 self.
log.debug(
"Comparing pattern %i to previous %i rotations..." %
1213 (rot_vects[-1][-1], len(rot_vects) - 1))
1216 for rot_idx
in range(max((len(rot_vects) - 1), 0)):
1218 for vect_idx
in range(len(rot_vects[rot_idx]) - 1):
1219 tmp_delta_vect = (rot_vects[rot_idx][vect_idx] -
1220 rot_vects[-1][vect_idx])
1221 tmp_dist_list.append(
1222 np.dot(tmp_delta_vect, tmp_delta_vect))
1223 if np.all(np.array(tmp_dist_list) < max_dist_rad ** 2):
1227 def _match_sources(self,
1230 """ Shift both the reference and source catalog to the the respective 1231 frames and find their nearest neighbor using a kdTree. 1233 Removes all matches who do not agree when either the reference or 1234 source catalog is rotated. Cuts on a maximum distance are left to an 1239 source_array : float array 1240 array of 3 vectors representing the source objects we are trying 1241 to match into the source catalog. 1242 shift_rot_matrix : float array 1243 3x3 rotation matrix that performs the spherical rotation from the 1244 source frame into the reference frame. 1248 lsst.pipe.base.Struct 1249 A Struct object containing the following data 1250 matches : a (N, 2) array of integer ids into the source and 1251 reference arrays. Matches are only returned for those that 1252 satisfy the distance and handshake criteria. 1253 distances : float array of the distance between each match in 1254 radians after the shift and rotation is applied. 1256 shifted_references = np.dot(
1257 np.linalg.inv(shift_rot_matrix),
1259 shifted_sources = np.dot(
1261 source_array.transpose()).transpose()
1263 ref_matches = np.empty((len(shifted_references), 2),
1265 src_matches = np.empty((len(shifted_sources), 2),
1268 ref_matches[:, 1] = np.arange(len(shifted_references),
1270 src_matches[:, 0] = np.arange(len(shifted_sources),
1274 src_kdtree = cKDTree(source_array)
1276 ref_to_src_dist, tmp_ref_to_src_idx = \
1277 src_kdtree.query(shifted_references)
1278 src_to_ref_dist, tmp_src_to_ref_idx = \
1279 ref_kdtree.query(shifted_sources)
1281 ref_matches[:, 0] = tmp_ref_to_src_idx
1282 src_matches[:, 1] = tmp_src_to_ref_idx
1285 return pipeBase.Struct(
1286 match_ids=src_matches[handshake_mask],
1287 distances_rad=src_to_ref_dist[handshake_mask],)
1289 def _handshake_match(self, matches_src, matches_ref):
1290 """Return only those matches where both the source 1291 and reference objects agree they they are each others' 1295 matches_src : int array 1296 (N, 2) int array of nearest neighbor matches between shifted and 1297 rotated reference objects matched into the sources. 1298 matches_ref : int array 1299 (M, 2) int array of nearest neighbor matches between shifted and 1300 rotated source objects matched into the references. 1304 Return the array positions where the two match catalogs agree. 1306 handshake_mask_array = np.zeros(len(matches_src), dtype=np.bool)
1308 for src_match_idx, match
in enumerate(matches_src):
1309 ref_match_idx = np.searchsorted(matches_ref[:, 1], match[1])
1310 if match[0] == matches_ref[ref_match_idx, 0]:
1311 handshake_mask_array[src_match_idx] =
True 1312 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, src_ctr, src_delta, src_dist, src_ctr_delta, src_ctr_dist, ref_ctr, ref_ctr_id, ref_delta, ref_dist, ref_dist_idx_array, ref_delta_array, ref_dist_array, ref_id_array, src_sin_tol)
def _test_pattern_lengths(self, test_pattern, max_dist_rad)
def _match_sources(self, source_array, shift_rot_matrix)
def __init__(self, reference_array, log)
def _handshake_match(self, matches_src, matches_ref)
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 _build_distances_and_angles(self)
def _test_rotation_agreement(self, rot_vects, max_dist_rad)
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 _compute_test_vectors(self, source_array)
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 _create_pattern_spokes(self, src_ctr, src_delta_array, src_dist_array, ref_ctr, ref_ctr_id, ref_delta, ref_dist, ref_delta_array, ref_dist_array, ref_id_array, max_dist_rad, n_match)
def _find_candidate_reference_pairs(self, src_dist, ref_dist_array, max_dist_rad)
def _create_spherical_rotation_matrix(self, rot_axis, cos_rotation, sin_rotion)
def _intermediate_verify(self, src_pattern, ref_pattern, shift_rot_matrix, max_dist_rad)