4 from scipy.optimize
import least_squares
5 from scipy.spatial
import cKDTree
7 import lsst.pipe.base
as pipeBase
10 def _rotation_matrix_chi_sq(flattened_rot_matrix,
14 """Compute the squared differences for least squares fitting. 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. 22 flattened_rot_matrix : float array 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 : float array of 3 vectors 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 : float array of 3 vectors 30 A array containing N, 3 vectors representing the reference frame we 31 would like to transform pattern_a into. 33 The maximum distance allowed from the pattern matching. This value is 34 used as the standard error for the resultant chi values. 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. 44 rot_matrix = flattened_rot_matrix.reshape((3, 3))
46 rot_pattern_a = np.dot(rot_matrix, pattern_a.transpose()).transpose()
47 diff_pattern_a_to_b = rot_pattern_a - pattern_b
50 rot_pattern_b = np.dot(np.linalg.inv(rot_matrix),
51 pattern_b.transpose()).transpose()
52 diff_pattern_b_to_a = rot_pattern_b - pattern_a
55 return np.concatenate(
56 (diff_pattern_a_to_b.flatten() / max_dist_rad,
57 diff_pattern_b_to_a.flatten() / max_dist_rad))
61 """ Class implementing a pessimistic version of Optimistic Pattern Matcher 62 B (OPMb) from Tabur 2007. The class loads and stores the reference object 63 in a convenient data structure for matching any set of source objects that 64 are assumed to contain each other. The pessimistic nature of the algorithm 65 comes from requiring that it discovers at least two patterns that agree on 66 the correct shift and rotation for matching before exiting. The original 67 behavior of OPMb can be recovered simply. Patterns matched between the 68 input datasets are n-spoked pinwheels created from n+1 points. Refer to 69 DMTN #031 for more details. http://github.com/lsst-dm/dmtn-031 70 -------------------------------------------------------------------------- 72 reference_array : float array 73 spherical points x, y, z of to use as reference objects for 75 log : an lsst.log instance 76 pair_id_array : int array 77 Internal lookup table. Given an id in the reference array, return 78 an array of the id pair that contains this object's id sorted on 79 the distance to the pairs. 80 pair_delta_array : float array 81 Internal lookup table. Given an id in the reference array, return 82 an array of the 3 vector deltas of all other pairs sorted on their 84 pair_dist_array : float array 85 Internal lookup table. Given an id in the reference return an array 86 of pair distances of all other pairs sorted on distance. 87 dist_array : float array 88 Array of all pairs of objects in the reference array sorted on 91 Array of id pairs that lookup into the reference array sorted 93 delta_array : float array 94 Array of 3 vector deltas for each pair in the reference array 95 sorted on pair distance. 102 reference_array : float array 103 Array of spherical points x, y, z to use as reference objects. 105 logger object for reporting warnings and failures. 113 def _build_distances_and_angles(self):
114 """ Create the data structures we will use to search for our pattern 117 Throughout this function and the rest of the 119 class we use id to reference the position in the input reference 120 catalog and index to 'index' into the arrays sorted on distance. 145 sub_id_array_list = []
146 sub_delta_array_list = []
147 sub_dist_array_list = []
154 sub_id_array = np.zeros((self.
_n_reference - 1 - ref_id, 2),
156 sub_id_array[:, 0] = ref_id
157 sub_id_array[:, 1] = np.arange(ref_id + 1, self.
_n_reference,
164 sub_dist_array = np.sqrt(sub_delta_array[:, 0] ** 2 +
165 sub_delta_array[:, 1] ** 2 +
166 sub_delta_array[:, 2] ** 2)
170 sub_id_array_list.append(sub_id_array)
171 sub_delta_array_list.append(sub_delta_array)
172 sub_dist_array_list.append(sub_dist_array)
192 ref_id, sorted_pair_dist_args]
194 ref_id, sorted_pair_dist_args]
196 ref_id, sorted_pair_dist_args, :]
199 unsorted_id_array = np.concatenate(sub_id_array_list)
200 unsorted_delta_array = np.concatenate(sub_delta_array_list)
201 unsorted_dist_array = np.concatenate(sub_dist_array_list)
205 sorted_dist_args = unsorted_dist_array.argsort()
206 self.
_dist_array = unsorted_dist_array[sorted_dist_args]
207 self.
_id_array = unsorted_id_array[sorted_dist_args]
208 self.
_delta_array = unsorted_delta_array[sorted_dist_args]
212 def match(self, source_array, n_check, n_match, n_agree,
213 max_n_patterns, max_shift, max_rotation, max_dist,
214 min_matches, pattern_skip_array=None):
215 """Match a given source catalog into the loaded reference catalog. 217 Given array of points on the unit sphere and tolerances, we 218 attempt to match a pinwheel like pattern between these input sources 219 and the reference objects this class was created with. This pattern 220 informs of the shift and rotation needed to align the input source 221 objects into the frame of the references. 225 source_array: float array 226 An array of spherical x,y,z coordinates and a magnitude in units 227 of objects having a lower value for sorting. The array should be 230 Number of sources to create a pattern from. Not all objects may be 231 checked if n_match criteria is before looping through all n_check 234 Number of objects to use in constructing a pattern to match. 236 Number of found patterns that must agree on their shift and 237 rotation before exiting. Set this value to 1 to recover the 238 expected behavior of Optimistic Pattern Matcher B. 239 max_n_patters : int value 240 Number of patterns to create from the input source objects to 241 attempt to match into the reference objects. 242 max_shift: float value 243 Maximum allowed shift to match patterns in arcseconds. 244 max_rotation: float value 245 Maximum allowed rotation between patterns in degrees. 246 max_dist: float value 247 Maximum distance in arcseconds allowed between candidate spokes in 248 the source and reference objects. Also sets that maximum distance 249 in the intermediate verify, pattern shift/rotation agreement, and 251 pattern_skip_array: int array 252 Patterns we would like to skip. This could be due to the pattern 253 being matched on a previous iteration that we now consider invalid. 254 This assumes the ordering of the source objects is the same 255 between different runs of the matcher which, assuming no object 256 has been inserted or the magnitudes have changed, it should be. 260 output_struct : pipe.base.struct 261 A lsst.pipe.base struct containing the following outputs. 264 (N, 2) array of matched ids for pairs. Empty list if no 266 distances_rad : float array 267 Radian distances between the matched objects. Empty list 270 Index of matched pattern. None if no match found. 272 Magnitude for the shift between the source and 273 reference objects in arcseconds. None if no match found. 277 sorted_source_array = source_array[source_array[:, -1].argsort(), :3]
278 n_source = len(sorted_source_array)
281 output_match_struct = pipeBase.Struct(
288 self.
log.warn(
"Source object array is empty. Unable to match. " 302 max_cos_shift = np.cos(np.radians(max_shift / 3600.))
303 max_cos_rot_sq = np.cos(np.radians(max_rotation)) ** 2
304 max_dist_rad = np.radians(max_dist / 3600.)
308 for pattern_idx
in range(np.min((max_n_patterns,
309 n_source - n_match))):
313 if pattern_skip_array
is not None and \
314 np.any(pattern_skip_array == pattern_idx):
316 "Skipping previously matched bad pattern %i..." %
320 pattern = sorted_source_array[
321 pattern_idx: np.min((pattern_idx + n_check, n_source)), :3]
326 construct_return_struct = \
328 pattern, n_match, max_cos_shift, max_cos_rot_sq,
332 if construct_return_struct.ref_candidates
is None or \
333 construct_return_struct.shift_rot_matrix
is None or \
334 construct_return_struct.cos_shift
is None or \
335 construct_return_struct.sin_rot
is None:
339 ref_candidates = construct_return_struct.ref_candidates
340 shift_rot_matrix = construct_return_struct.shift_rot_matrix
341 cos_shift = construct_return_struct.cos_shift
342 sin_rot = construct_return_struct.sin_rot
346 if len(ref_candidates) < n_match:
352 tmp_rot_vect_list = []
353 for test_vect
in test_vectors:
354 tmp_rot_vect_list.append(np.dot(shift_rot_matrix, test_vect))
361 tmp_rot_vect_list.append(pattern_idx)
362 rot_vect_list.append(tmp_rot_vect_list)
374 n_matched = len(match_sources_struct.match_ids[
375 match_sources_struct.distances_rad < max_dist_rad])
378 if n_matched >= min_matches:
380 shift = np.degrees(np.arccos(cos_shift)) * 3600.
382 self.
log.debug(
"Succeeded after %i patterns." % pattern_idx)
383 self.
log.debug(
"\tShift %.4f arcsec" % shift)
384 self.
log.debug(
"\tRotation: %.4f deg" %
385 np.degrees(np.arcsin(sin_rot)))
388 output_match_struct.match_ids = \
389 match_sources_struct.match_ids
390 output_match_struct.distances_rad = \
391 match_sources_struct.distances_rad
392 output_match_struct.pattern_idx = pattern_idx
393 output_match_struct.shift = shift
394 return output_match_struct
396 self.
log.warn(
"Failed after %i patterns." % (pattern_idx + 1))
397 return output_match_struct
399 def _compute_test_vectors(self, source_array):
400 """Compute spherical 3 vectors at the edges of the x, y, z extent 401 of the input source catalog. 405 source_array : float array (N, 3) 406 array of 3 vectors representing positions on the unit 411 float array of 3 vectors 412 Array of vectors representing the maximum extents in x, y, z 413 of the input source array. These are used with the rotations 414 the code finds to test for agreement from different patterns 415 when the code is running in pessimistic mode. 419 if np.any(np.logical_not(np.isfinite(source_array))):
420 self.
log.warn(
"Input source objects contain non-finite values. " 421 "This could end badly.")
422 center_vect = np.nanmean(source_array, axis=0)
426 xbtm_vect = np.array([np.min(source_array[:, 0]), center_vect[1],
427 center_vect[2]], dtype=np.float64)
428 xtop_vect = np.array([np.max(source_array[:, 0]), center_vect[1],
429 center_vect[2]], dtype=np.float64)
430 xbtm_vect /= np.sqrt(np.dot(xbtm_vect, xbtm_vect))
431 xtop_vect /= np.sqrt(np.dot(xtop_vect, xtop_vect))
433 ybtm_vect = np.array([center_vect[0], np.min(source_array[:, 1]),
434 center_vect[2]], dtype=np.float64)
435 ytop_vect = np.array([center_vect[0], np.max(source_array[:, 1]),
436 center_vect[2]], dtype=np.float64)
437 ybtm_vect /= np.sqrt(np.dot(ybtm_vect, ybtm_vect))
438 ytop_vect /= np.sqrt(np.dot(ytop_vect, ytop_vect))
440 zbtm_vect = np.array([center_vect[0], center_vect[1],
441 np.min(source_array[:, 2])], dtype=np.float64)
442 ztop_vect = np.array([center_vect[0], center_vect[1],
443 np.max(source_array[:, 2])], dtype=np.float64)
444 zbtm_vect /= np.sqrt(np.dot(zbtm_vect, zbtm_vect))
445 ztop_vect /= np.sqrt(np.dot(ztop_vect, ztop_vect))
448 return np.array([xbtm_vect, xtop_vect, ybtm_vect, ytop_vect,
449 zbtm_vect, ztop_vect])
451 def _construct_pattern_and_shift_rot_matrix(self, src_pattern_array,
452 n_match, max_cos_theta_shift,
453 max_cos_rot_sq, max_dist_rad):
454 """Test an input source pattern against the reference catalog. 456 Returns the candidate matched patterns and their 457 implied rotation matrices or None. 461 src_pattern_array : float array 462 Sub selection of source 3 vectors to create a pattern from 464 Number of points to attempt to create a pattern from. Must be 465 >= len(src_pattern_array) 466 max_cos_theta_shift : float 467 Maximum shift allowed between two patterns' centers. 468 max_cos_rot_sq : float 469 Maximum rotation between two patterns that have been shifted 470 to have their centers on top of each other. 472 Maximum delta distance allowed between the source and reference 473 pair distances to consider the reference pair a candidate for 474 the source pair. Also sets the tolerance between the opening 475 angles of the spokes when compared to the reference. 479 lsst.pipe.base.Struct 480 Return a Struct containing the following data: 482 ref_candidates : list of ints 483 ids of the matched pattern in the internal reference_array 485 src_candidates : list of ints 486 Pattern ids of the sources matched. 487 shift_rot_matrix_src_to_ref : float array 488 3x3 matrix specifying the full shift and rotation between the 489 reference and source objects. Rotates 490 source into reference frame. None if match is not found. 491 shift_rot_matrix_ref_to_src : float array 492 3x3 matrix specifying the full shift 493 and rotation of the reference and source objects. Rotates 494 reference into source frame. None if match is not found. 496 Magnitude of the shift found between the two patten 497 centers. None if match is not found. 498 sin_rot : float value of the rotation to align the already shifted 499 source pattern to the reference pattern. None if no match 507 output_matched_pattern = pipeBase.Struct(
510 shift_rot_matrix=
None,
516 src_delta_array = np.empty((len(src_pattern_array) - 1, 3))
517 src_delta_array[:, 0] = (src_pattern_array[1:, 0] -
518 src_pattern_array[0, 0])
519 src_delta_array[:, 1] = (src_pattern_array[1:, 1] -
520 src_pattern_array[0, 1])
521 src_delta_array[:, 2] = (src_pattern_array[1:, 2] -
522 src_pattern_array[0, 2])
523 src_dist_array = np.sqrt(src_delta_array[:, 0]**2 +
524 src_delta_array[:, 1]**2 +
525 src_delta_array[:, 2]**2)
534 for ref_dist_idx
in ref_dist_index_array:
538 tmp_ref_pair_list = self.
_id_array[ref_dist_idx]
539 for pair_idx, ref_id
in enumerate(tmp_ref_pair_list):
540 src_candidates = [0, 1]
542 shift_rot_matrix =
None 549 cos_shift = np.dot(src_pattern_array[0], ref_center)
550 if cos_shift < max_cos_theta_shift:
554 ref_candidates.append(ref_id)
560 ref_candidates.append(
561 tmp_ref_pair_list[1])
563 ref_candidates.append(
564 tmp_ref_pair_list[0])
573 src_pattern_array[0], ref_center, src_delta_array[0],
574 ref_delta, cos_shift, max_cos_rot_sq)
575 if test_rot_struct.cos_rot_sq
is None or \
576 test_rot_struct.proj_ref_ctr_delta
is None or \
577 test_rot_struct.shift_matrix
is None:
581 cos_rot_sq = test_rot_struct.cos_rot_sq
582 proj_ref_ctr_delta = test_rot_struct.proj_ref_ctr_delta
583 shift_matrix = test_rot_struct.shift_matrix
595 src_pattern_array[0], src_delta_array, src_dist_array,
597 tmp_ref_delta_array, tmp_ref_dist_array,
598 tmp_ref_id_array, max_dist_rad, n_match)
602 if len(pattern_spoke_struct.ref_spoke_list) < n_match - 2
or \
603 len(pattern_spoke_struct.src_spoke_list) < n_match - 2:
607 ref_candidates.extend(pattern_spoke_struct.ref_spoke_list)
608 src_candidates.extend(pattern_spoke_struct.src_spoke_list)
614 cos_rot_sq, shift_matrix, src_delta_array[0],
618 if shift_rot_struct.sin_rot
is None or \
619 shift_rot_struct.shift_rot_matrix
is None:
623 sin_rot = shift_rot_struct.sin_rot
624 shift_rot_matrix = shift_rot_struct.shift_rot_matrix
633 src_pattern_array[src_candidates],
635 shift_rot_matrix, max_dist_rad)
637 if fit_shift_rot_matrix
is not None:
639 output_matched_pattern.ref_candidates = ref_candidates
640 output_matched_pattern.src_candidates = src_candidates
641 output_matched_pattern.shift_rot_matrix = \
643 output_matched_pattern.cos_shift = cos_shift
644 output_matched_pattern.sin_rot = sin_rot
645 return output_matched_pattern
647 return output_matched_pattern
649 def _find_candidate_reference_pairs(self, src_dist, ref_dist_array,
651 """Wrap numpy.searchsorted to find the range of reference spokes 652 within a spoke distance tolerance of our source spoke. 654 Returns an array sorted from the smallest absolute delta distance 655 between source and reference spoke length. This sorting increases the 656 speed for the pattern search greatly. 660 src_dist : float radians 661 float value of the distance we would like to search for in 662 the reference array in radians. 663 ref_dist_array : float array 664 sorted array of distances in radians. 666 maximum plus/minus search to find in the reference array in 672 indices lookup into the input ref_dist_array sorted by the 673 difference in value to the src_dist from absolute value 678 start_idx = np.searchsorted(ref_dist_array, src_dist - max_dist_rad)
679 end_idx = np.searchsorted(ref_dist_array, src_dist + max_dist_rad,
683 if start_idx == end_idx:
689 if end_idx > ref_dist_array.shape[0]:
690 end_idx = ref_dist_array.shape[0]
695 tmp_diff_array = np.fabs(ref_dist_array[start_idx:end_idx] - src_dist)
696 return tmp_diff_array.argsort() + start_idx
698 def _test_rotation(self, src_center, ref_center, src_delta, ref_delta,
699 cos_shift, max_cos_rot_sq):
700 """ Test if the rotation implied between the source 701 pattern and reference pattern is within tolerance. To test this 702 we need to create the first part of our spherical rotation matrix 703 which we also return for use later. 707 src_center : float array3 709 ref_center : float array 710 3 vector defining the center of the candidate reference pinwheel 712 src_delta : float array 713 3 vector delta between the source pattern center and the end of 715 ref_delta : float array 716 3 vector delta of the candidate matched reference pair 718 Cosine of the angle between the source and reference candidate 720 max_cos_rot_sq : float 721 candidate reference pair after shifting the centers on top of each 722 other. The function will return None if the rotation implied is 723 greater than max_cos_rot_sq. 727 lsst.pipe.base.Struct 728 Return a pipe.base.Struct containing the following data. 731 magnitude of the rotation needed to align the two patterns 732 after their centers are shifted on top of each other. 733 None if rotation test fails. 734 shift_matrix : float array 735 3x3 rotation matrix describing the shift needed to align 736 the source and candidate reference center. 737 None if rotation test fails. 743 elif cos_shift < -1.0:
745 sin_shift = np.sqrt(1 - cos_shift ** 2)
751 rot_axis = np.cross(src_center, ref_center)
752 rot_axis /= sin_shift
754 rot_axis, cos_shift, sin_shift)
756 shift_matrix = np.identity(3)
760 rot_src_delta = np.dot(shift_matrix, src_delta)
761 proj_src_delta = (rot_src_delta -
762 np.dot(rot_src_delta, ref_center) * ref_center)
763 proj_ref_delta = (ref_delta -
764 np.dot(ref_delta, ref_center) * ref_center)
765 cos_rot_sq = (np.dot(proj_src_delta, proj_ref_delta) ** 2 /
766 (np.dot(proj_src_delta, proj_src_delta) *
767 np.dot(proj_ref_delta, proj_ref_delta)))
769 if cos_rot_sq < max_cos_rot_sq:
770 return pipeBase.Struct(
772 proj_ref_ctr_delta=
None,
776 return pipeBase.Struct(
777 cos_rot_sq=cos_rot_sq,
778 proj_ref_ctr_delta=proj_ref_delta,
779 shift_matrix=shift_matrix,)
781 def _create_spherical_rotation_matrix(self, rot_axis, cos_rotation,
783 """Construct a generalized 3D rotation matrix about a given 788 rot_axis : float array 789 3 vector defining the axis to rotate about. 791 cosine of the rotation angle. 793 sine of the rotation angle. 798 3x3 spherical, rotation matrix. 801 rot_cross_matrix = np.array(
802 [[0., -rot_axis[2], rot_axis[1]],
803 [rot_axis[2], 0., -rot_axis[0]],
804 [-rot_axis[1], rot_axis[0], 0.]], dtype=np.float64)
805 shift_matrix = (cos_rotation*np.identity(3) +
806 sin_rotion*rot_cross_matrix +
807 (1. - cos_rotation)*np.outer(rot_axis, rot_axis))
811 def _create_pattern_spokes(self, src_ctr, src_delta_array, src_dist_array,
812 ref_ctr, ref_ctr_id, proj_ref_ctr_delta,
813 ref_delta_array, ref_dist_array,
814 ref_id_array, max_dist_rad, n_match):
815 """ Create the individual spokes that make up the pattern now that the 816 shift and rotation are within tolerance. 818 If we can't create a valid pattern we exit early. 822 src_ctr : float array 823 3 vector of the source pinwheel center 824 src_delta_array : float array 825 Array of 3 vector deltas between the source center and the pairs 826 that make up the remaining spokes of the pinwheel 827 src_dist_array : float array 828 Array of the distances of each src_delta in the pinwheel 829 ref_ctr : float array 830 3 vector of the candidate reference center 832 id of the ref_ctr in the master reference array 833 proj_ref_ctr_delta : `float` array-like 834 Plane projected 3 vector formed from the center point of the 835 candidate pin-wheel and the second point in the pattern to create 836 the first spoke pair. This is the candidate pair that was matched 837 in the main _construct_pattern_and_shift_rot_matrix loop 838 ref_delta_array : float array 839 Array of 3 vector deltas that are have the current candidate 840 reference center as part of the pair 841 ref_dist_array : float array 842 Array of vector distances for each of the reference pairs 843 ref_id_array : int array 844 Array of id lookups into the master reference array that our 845 center id object is paired with. 847 Maximum search distance 849 Number of source deltas that must be matched into the reference 850 deltas in order to consider this a successful pattern match. 854 lsst.pipe.base.Struct 855 The Struct contains the following data: 857 ref_spoke_list : list of ints specifying ids into the master 859 src_spoke_list : list of ints specifying indices into the current 860 source pattern that is being tested. 863 output_spokes = pipeBase.Struct(
875 proj_src_ctr_delta = (src_delta_array[0] -
876 np.dot(src_delta_array[0], src_ctr) * src_ctr)
877 proj_src_ctr_dist_sq = np.dot(proj_src_ctr_delta, proj_src_ctr_delta)
880 proj_ref_ctr_dist_sq = np.dot(proj_ref_ctr_delta, proj_ref_ctr_delta)
883 for src_idx
in range(1, len(src_dist_array)):
884 if n_fail > len(src_dist_array) - (n_match - 1):
889 src_sin_tol = (max_dist_rad /
890 (src_dist_array[src_idx] + max_dist_rad))
896 if src_sin_tol > 0.0447:
903 src_delta_array[src_idx] -
904 np.dot(src_delta_array[src_idx], src_ctr) * src_ctr)
905 geom_dist_src = np.sqrt(
906 np.dot(proj_src_delta, proj_src_delta) *
907 proj_src_ctr_dist_sq)
910 cos_theta_src = (np.dot(proj_src_delta, proj_src_ctr_delta) /
912 cross_src = (np.cross(proj_src_delta, proj_src_ctr_delta) /
914 sin_theta_src = np.dot(cross_src, src_ctr)
919 src_dist_array[src_idx], ref_dist_array, max_dist_rad)
929 proj_ref_ctr_dist_sq,
940 ref_spoke_list.append(ref_id)
941 src_spoke_list.append(src_idx + 1)
945 if len(ref_spoke_list) >= n_match - 2:
947 output_spokes.ref_spoke_list = ref_spoke_list
948 output_spokes.src_spoke_list = src_spoke_list
953 def _test_spoke(self, cos_theta_src, sin_theta_src, ref_ctr, ref_ctr_id,
954 proj_ref_ctr_delta, proj_ref_ctr_dist_sq,
955 ref_dist_idx_array, ref_delta_array,
956 ref_id_array, src_sin_tol):
957 """Test the opening angle between the first spoke of our pattern 958 for the source object against the reference object. 960 This method makes heavy use of the small angle approximation to perform 965 cos_theta_src : `float` 966 Cosine of the angle between the current candidate source spoke and 968 sin_theta_src : `float` 969 Sine of the angle between the current candidate source spoke and 971 ref_ctr : float array 972 3 vector of the candidate reference center 974 id lookup of the ref_ctr into the master reference array 975 proj_ref_ctr_delta : `float` 976 Plane projected first spoke in the reference pattern using the 977 pattern center as normal. 978 proj_ref_ctr_dist_sq : `float` 979 Squared length of the projected vector. 980 ref_dist_idx_array : int array 981 Indices sorted by the delta distance between the source 982 spoke we are trying to test and the candidate reference 984 ref_delta_array : float array 985 Array of 3 vector deltas that are have the current candidate 986 reference center as part of the pair 987 ref_id_array : int array 988 Array of id lookups into the master reference array that our 989 center id object is paired with. 991 Sine of tolerance allowed between source and reference spoke 997 If we can not find a candidate spoke we return None else we 998 return an int id into the master reference array. 1002 for ref_dist_idx
in ref_dist_idx_array:
1005 if ref_id_array[ref_dist_idx] < ref_ctr_id:
1011 ref_delta_array[ref_dist_idx] -
1012 np.dot(ref_delta_array[ref_dist_idx], ref_ctr) * ref_ctr)
1013 geom_dist_ref = np.sqrt(proj_ref_ctr_dist_sq *
1014 np.dot(proj_ref_delta, proj_ref_delta))
1015 cos_theta_ref = ref_sign * (
1016 np.dot(proj_ref_delta, proj_ref_ctr_delta) /
1021 if cos_theta_ref ** 2 < (1 - src_sin_tol ** 2):
1022 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
1023 (1 - cos_theta_ref ** 2))
1025 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
1030 if cos_sq_comparison > src_sin_tol ** 2:
1036 cross_ref = ref_sign * (
1037 np.cross(proj_ref_delta, proj_ref_ctr_delta) /
1039 sin_theta_ref = np.dot(cross_ref, ref_ctr)
1043 if abs(cos_theta_src) < src_sin_tol:
1044 sin_comparison = (sin_theta_src - sin_theta_ref) / src_sin_tol
1047 (sin_theta_src - sin_theta_ref) / cos_theta_ref
1049 if abs(sin_comparison) > src_sin_tol:
1053 return ref_id_array[ref_dist_idx]
1057 def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta,
1058 ref_ctr, ref_delta):
1059 """ Create the final part of our spherical rotation matrix. 1064 cosine of the rotation needed to align our source and reference 1066 shift_matrix : float array 1067 3x3 rotation matrix for shifting the source pattern center on top 1068 of the candidate reference pattern center. 1069 src_delta : float array 1070 3 vector delta of representing the first spoke of the source 1072 ref_ctr : float array 1073 3 vector on the unit-sphere representing the center of our 1075 ref_delta : float array 1076 3 vector delta made by the first pair of the reference pattern. 1080 lsst.pipe.base.Struct 1081 Struct object containing the following data: 1083 sin_rot : float sine of the amount of rotation between the 1084 source and reference pattern. We use sine here as it is 1085 signed and tells us the chirality of the rotation. 1086 shift_rot_matrix : float array representing the 3x3 rotation 1087 matrix that takes the source pattern and shifts and rotates 1088 it to align with the reference pattern. 1090 cos_rot = np.sqrt(cos_rot_sq)
1091 rot_src_delta = np.dot(shift_matrix, src_delta)
1092 delta_dot_cross = np.dot(np.cross(rot_src_delta, ref_delta), ref_ctr)
1094 sin_rot = np.sign(delta_dot_cross) * np.sqrt(1 - cos_rot_sq)
1096 ref_ctr, cos_rot, sin_rot)
1098 shift_rot_matrix = np.dot(rot_matrix, shift_matrix)
1100 return pipeBase.Struct(
1102 shift_rot_matrix=shift_rot_matrix,)
1104 def _intermediate_verify(self, src_pattern, ref_pattern, shift_rot_matrix,
1106 """ Perform an intermediate verify step. 1108 Rotate the matches references into the source frame and test their 1109 distances against tolerance. Only return true if all points are within 1114 src_pattern : float array 1115 Array of 3 vectors representing the points that make up our source 1117 ref_pattern : float array 1118 Array of 3 vectors representing our candidate reference pinwheel 1120 shift_rot_matrix : float array 1121 3x3 rotation matrix that takes the source objects and rotates them 1122 onto the frame of the reference objects 1123 max_dist_rad : float 1124 Maximum distance allowed to consider two objects the same. 1129 Return the fitted shift/rotation matrix if all of the points in our 1130 source pattern are within max_dist_rad of their matched reference 1131 objects. Returns None if this criteria is not satisfied. 1133 if len(src_pattern) != len(ref_pattern):
1135 "Source pattern length does not match ref pattern.\n" 1136 "\t source pattern len=%i, reference pattern len=%i" %
1137 (len(src_pattern), len(ref_pattern)))
1140 src_pattern, ref_pattern, shift_rot_matrix, max_dist_rad):
1148 fit_shift_rot_matrix = least_squares(
1149 _rotation_matrix_chi_sq,
1150 x0=shift_rot_matrix.flatten(),
1151 args=(src_pattern, ref_pattern, max_dist_rad)
1155 src_pattern, ref_pattern, fit_shift_rot_matrix,
1157 return fit_shift_rot_matrix
1161 def _intermediate_verify_comparison(self, pattern_a, pattern_b,
1162 shift_rot_matrix, max_dist_rad):
1163 """Test the input rotation matrix against one input pattern and 1166 If every point in the pattern after rotation is within a distance of 1167 max_dist_rad to its candidate point in the other pattern, we return 1172 pattern_a : float array 1173 Array of 3 vectors representing the points that make up our source 1175 pattern_b : float array 1176 Array of 3 vectors representing our candidate reference pinwheel 1178 shift_rot_matrix : float array 1179 3x3 rotation matrix that takes the source objects and rotates them 1180 onto the frame of the reference objects 1181 max_dist_rad : float 1182 Maximum distance allowed to consider two objects the same. 1188 True if all rotated source points are within max_dist_rad of 1189 the candidate references matches. 1191 shifted_pattern_a = np.dot(shift_rot_matrix,
1192 pattern_a.transpose()).transpose()
1193 tmp_delta_array = shifted_pattern_a - pattern_b
1194 tmp_dist_array = (tmp_delta_array[:, 0] ** 2 +
1195 tmp_delta_array[:, 1] ** 2 +
1196 tmp_delta_array[:, 2] ** 2)
1197 return np.all(tmp_dist_array < max_dist_rad ** 2)
1199 def _test_pattern_lengths(self, test_pattern, max_dist_rad):
1200 """ Test that the all vectors in a pattern are unit length within 1203 This is useful for assuring the non unitary transforms do not contain 1204 too much distortion. 1206 dists = (test_pattern[:, 0] ** 2 +
1207 test_pattern[:, 1] ** 2 +
1208 test_pattern[:, 2] ** 2)
1210 np.logical_and((1 - max_dist_rad) ** 2 < dists,
1211 dists < (1 + max_dist_rad) ** 2))
1213 def _test_rotation_agreement(self, rot_vects, max_dist_rad):
1214 """ Test this rotation against the previous N found and return 1215 the number that a agree within tolerance to where our test 1220 rot_vects : float array 1221 Arrays of rotated 3 vectors representing the maximum x, y, 1222 z extent on the unit sphere of the input source objects rotated by 1223 the candidate rotations into the reference frame. 1224 max_dist_rad : float 1225 maximum distance in radians to consider two points "agreeing" on 1231 Number of candidate rotations that agree for all of the rotated 1235 self.
log.debug(
"Comparing pattern %i to previous %i rotations..." %
1236 (rot_vects[-1][-1], len(rot_vects) - 1))
1239 for rot_idx
in range(max((len(rot_vects) - 1), 0)):
1241 for vect_idx
in range(len(rot_vects[rot_idx]) - 1):
1242 tmp_delta_vect = (rot_vects[rot_idx][vect_idx] -
1243 rot_vects[-1][vect_idx])
1244 tmp_dist_list.append(
1245 np.dot(tmp_delta_vect, tmp_delta_vect))
1246 if np.all(np.array(tmp_dist_list) < max_dist_rad ** 2):
1250 def _match_sources(self,
1253 """ Shift both the reference and source catalog to the the respective 1254 frames and find their nearest neighbor using a kdTree. 1256 Removes all matches who do not agree when either the reference or 1257 source catalog is rotated. Cuts on a maximum distance are left to an 1262 source_array : float array 1263 array of 3 vectors representing the source objects we are trying 1264 to match into the source catalog. 1265 shift_rot_matrix : float array 1266 3x3 rotation matrix that performs the spherical rotation from the 1267 source frame into the reference frame. 1271 lsst.pipe.base.Struct 1272 A Struct object containing the following data 1273 matches : a (N, 2) array of integer ids into the source and 1274 reference arrays. Matches are only returned for those that 1275 satisfy the distance and handshake criteria. 1276 distances : float array of the distance between each match in 1277 radians after the shift and rotation is applied. 1279 shifted_references = np.dot(
1280 np.linalg.inv(shift_rot_matrix),
1282 shifted_sources = np.dot(
1284 source_array.transpose()).transpose()
1286 ref_matches = np.empty((len(shifted_references), 2),
1288 src_matches = np.empty((len(shifted_sources), 2),
1291 ref_matches[:, 1] = np.arange(len(shifted_references),
1293 src_matches[:, 0] = np.arange(len(shifted_sources),
1297 src_kdtree = cKDTree(source_array)
1299 ref_to_src_dist, tmp_ref_to_src_idx = \
1300 src_kdtree.query(shifted_references)
1301 src_to_ref_dist, tmp_src_to_ref_idx = \
1302 ref_kdtree.query(shifted_sources)
1304 ref_matches[:, 0] = tmp_ref_to_src_idx
1305 src_matches[:, 1] = tmp_src_to_ref_idx
1308 return pipeBase.Struct(
1309 match_ids=src_matches[handshake_mask],
1310 distances_rad=src_to_ref_dist[handshake_mask],)
1312 def _handshake_match(self, matches_src, matches_ref):
1313 """Return only those matches where both the source 1314 and reference objects agree they they are each others' 1318 matches_src : int array 1319 (N, 2) int array of nearest neighbor matches between shifted and 1320 rotated reference objects matched into the sources. 1321 matches_ref : int array 1322 (M, 2) int array of nearest neighbor matches between shifted and 1323 rotated source objects matched into the references. 1327 Return the array positions where the two match catalogs agree. 1329 handshake_mask_array = np.zeros(len(matches_src), dtype=np.bool)
1331 for src_match_idx, match
in enumerate(matches_src):
1332 ref_match_idx = np.searchsorted(matches_ref[:, 1], match[1])
1333 if match[0] == matches_ref[ref_match_idx, 0]:
1334 handshake_mask_array[src_match_idx] =
True 1335 return handshake_mask_array
def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta, ref_ctr, ref_delta)
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_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_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 _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 _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)