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 : `numpy.ndarray`, (9, ) 23 A flattened array representing a 3x3 rotation matrix. The array is 24 flattened to comply with the API of scipy.optimize.least_squares. 25 Flattened elements are [[0, 0], [0, 1], [0, 2], [1, 0]...] 26 pattern_a : `numpy.ndarray`, (N, 3) 27 A array containing N, 3 vectors representing the objects we would like 28 to transform into the frame of pattern_b. 29 pattern_b : `numpy.ndarray`, (N, 3) 30 A array containing N, 3 vectors representing the reference frame we 31 would like to transform pattern_a into. 32 max_dist_rad : `float` 33 The maximum distance allowed from the pattern matching. This value is 34 used as the standard error for the resultant chi values. 38 noralized_diff : `numpy.ndarray`, (9,) 39 Array of differences between the vectors representing of the source 40 pattern rotated into the reference frame and the converse. This is 41 used to minimize in a least squares fitter. 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 return diff_pattern_a_to_b.flatten() / max_dist_rad
54 """Class implementing a pessimistic version of Optimistic Pattern Matcher 55 B (OPMb) from Tabur 2007. See `DMTN-031 <http://ls.st/DMTN-031`_ 59 reference_array : `numpy.ndarray`, (N, 3) 60 spherical points x, y, z of to use as reference objects for 63 Logger for outputting debug info. 67 The class loads and stores the reference object 68 in a convenient data structure for matching any set of source objects that 69 are assumed to contain each other. The pessimistic nature of the algorithm 70 comes from requiring that it discovers at least two patterns that agree on 71 the correct shift and rotation for matching before exiting. The original 72 behavior of OPMb can be recovered simply. Patterns matched between the 73 input datasets are n-spoked pinwheels created from n+1 points. Refer to 74 DMTN #031 for more details. http://github.com/lsst-dm/dmtn-031 84 def _build_distances_and_angles(self):
85 """Create the data structures we will use to search for our pattern 88 Throughout this function and the rest of the class we use id to 89 reference the position in the input reference catalog and index to 90 'index' into the arrays sorted on distance. 107 sub_id_array_list = []
108 sub_delta_array_list = []
109 sub_dist_array_list = []
116 sub_id_array = np.zeros((self.
_n_reference - 1 - ref_id, 2),
118 sub_id_array[:, 0] = ref_id
119 sub_id_array[:, 1] = np.arange(ref_id + 1, self.
_n_reference,
126 sub_dist_array = np.sqrt(sub_delta_array[:, 0] ** 2 +
127 sub_delta_array[:, 1] ** 2 +
128 sub_delta_array[:, 2] ** 2)
132 sub_id_array_list.append(sub_id_array)
133 sub_delta_array_list.append(sub_delta_array)
134 sub_dist_array_list.append(sub_dist_array)
154 ref_id, sorted_pair_dist_args]
156 ref_id, sorted_pair_dist_args]
158 ref_id, sorted_pair_dist_args, :]
161 unsorted_id_array = np.concatenate(sub_id_array_list)
162 unsorted_delta_array = np.concatenate(sub_delta_array_list)
163 unsorted_dist_array = np.concatenate(sub_dist_array_list)
167 sorted_dist_args = unsorted_dist_array.argsort()
168 self.
_dist_array = unsorted_dist_array[sorted_dist_args]
169 self.
_id_array = unsorted_id_array[sorted_dist_args]
170 self.
_delta_array = unsorted_delta_array[sorted_dist_args]
174 def match(self, source_array, n_check, n_match, n_agree,
175 max_n_patterns, max_shift, max_rotation, max_dist,
176 min_matches, pattern_skip_array=None):
177 """Match a given source catalog into the loaded reference catalog. 179 Given array of points on the unit sphere and tolerances, we 180 attempt to match a pinwheel like pattern between these input sources 181 and the reference objects this class was created with. This pattern 182 informs of the shift and rotation needed to align the input source 183 objects into the frame of the references. 187 source_array : `numpy.ndarray`, (N, 3) 188 An array of spherical x,y,z coordinates and a magnitude in units 189 of objects having a lower value for sorting. The array should be 192 Number of sources to create a pattern from. Not all objects may be 193 checked if n_match criteria is before looping through all n_check 196 Number of objects to use in constructing a pattern to match. 198 Number of found patterns that must agree on their shift and 199 rotation before exiting. Set this value to 1 to recover the 200 expected behavior of Optimistic Pattern Matcher B. 201 max_n_patters : `int` 202 Number of patterns to create from the input source objects to 203 attempt to match into the reference objects. 205 Maximum allowed shift to match patterns in arcseconds. 206 max_rotation : `float` 207 Maximum allowed rotation between patterns in degrees. 209 Maximum distance in arcseconds allowed between candidate spokes in 210 the source and reference objects. Also sets that maximum distance 211 in the intermediate verify, pattern shift/rotation agreement, and 213 pattern_skip_array : `int` 214 Patterns we would like to skip. This could be due to the pattern 215 being matched on a previous iteration that we now consider invalid. 216 This assumes the ordering of the source objects is the same 217 between different runs of the matcher which, assuming no object 218 has been inserted or the magnitudes have changed, it should be. 222 output_struct : `lsst.pipe.base.Struct` 223 Result struct with components 225 - ``matches`` : (N, 2) array of matched ids for pairs. Empty list if no 226 match found (`numpy.ndarray`, (N, 2) or `list`) 227 - ``distances_rad`` : Radian distances between the matched objects. 228 Empty list if no match found (`numpy.ndarray`, (N,)) 229 - ``pattern_idx``: Index of matched pattern. None if no match found 231 - ``shift`` : Magnitude for the shift between the source and reference 232 objects in arcseconds. None if no match found (`float`). 236 sorted_source_array = source_array[source_array[:, -1].argsort(), :3]
237 n_source = len(sorted_source_array)
240 output_match_struct = pipeBase.Struct(
247 self.
log.warn(
"Source object array is empty. Unable to match. " 261 max_cos_shift = np.cos(np.radians(max_shift / 3600.))
262 max_cos_rot_sq = np.cos(np.radians(max_rotation)) ** 2
263 max_dist_rad = np.radians(max_dist / 3600.)
267 for pattern_idx
in range(np.min((max_n_patterns,
268 n_source - n_match))):
272 if pattern_skip_array
is not None and \
273 np.any(pattern_skip_array == pattern_idx):
275 "Skipping previously matched bad pattern %i..." %
279 pattern = sorted_source_array[
280 pattern_idx: np.min((pattern_idx + n_check, n_source)), :3]
285 construct_return_struct = \
287 pattern, n_match, max_cos_shift, max_cos_rot_sq,
291 if construct_return_struct.ref_candidates
is None or \
292 construct_return_struct.shift_rot_matrix
is None or \
293 construct_return_struct.cos_shift
is None or \
294 construct_return_struct.sin_rot
is None:
298 ref_candidates = construct_return_struct.ref_candidates
299 shift_rot_matrix = construct_return_struct.shift_rot_matrix
300 cos_shift = construct_return_struct.cos_shift
301 sin_rot = construct_return_struct.sin_rot
305 if len(ref_candidates) < n_match:
311 tmp_rot_vect_list = []
312 for test_vect
in test_vectors:
313 tmp_rot_vect_list.append(np.dot(shift_rot_matrix, test_vect))
320 tmp_rot_vect_list.append(pattern_idx)
321 rot_vect_list.append(tmp_rot_vect_list)
333 n_matched = len(match_sources_struct.match_ids[
334 match_sources_struct.distances_rad < max_dist_rad])
337 if n_matched >= min_matches:
339 shift = np.degrees(np.arccos(cos_shift)) * 3600.
341 self.
log.debug(
"Succeeded after %i patterns." % pattern_idx)
342 self.
log.debug(
"\tShift %.4f arcsec" % shift)
343 self.
log.debug(
"\tRotation: %.4f deg" %
344 np.degrees(np.arcsin(sin_rot)))
347 output_match_struct.match_ids = \
348 match_sources_struct.match_ids
349 output_match_struct.distances_rad = \
350 match_sources_struct.distances_rad
351 output_match_struct.pattern_idx = pattern_idx
352 output_match_struct.shift = shift
353 return output_match_struct
355 self.
log.debug(
"Failed after %i patterns." % (pattern_idx + 1))
356 return output_match_struct
358 def _compute_test_vectors(self, source_array):
359 """Compute spherical 3 vectors at the edges of the x, y, z extent 360 of the input source catalog. 364 source_array : `numpy.ndarray`, (N, 3) 365 array of 3 vectors representing positions on the unit 370 test_vectors : `numpy.ndarray`, (N, 3) 371 Array of vectors representing the maximum extents in x, y, z 372 of the input source array. These are used with the rotations 373 the code finds to test for agreement from different patterns 374 when the code is running in pessimistic mode. 378 if np.any(np.logical_not(np.isfinite(source_array))):
379 self.
log.warn(
"Input source objects contain non-finite values. " 380 "This could end badly.")
381 center_vect = np.nanmean(source_array, axis=0)
385 xbtm_vect = np.array([np.min(source_array[:, 0]), center_vect[1],
386 center_vect[2]], dtype=np.float64)
387 xtop_vect = np.array([np.max(source_array[:, 0]), center_vect[1],
388 center_vect[2]], dtype=np.float64)
389 xbtm_vect /= np.sqrt(np.dot(xbtm_vect, xbtm_vect))
390 xtop_vect /= np.sqrt(np.dot(xtop_vect, xtop_vect))
392 ybtm_vect = np.array([center_vect[0], np.min(source_array[:, 1]),
393 center_vect[2]], dtype=np.float64)
394 ytop_vect = np.array([center_vect[0], np.max(source_array[:, 1]),
395 center_vect[2]], dtype=np.float64)
396 ybtm_vect /= np.sqrt(np.dot(ybtm_vect, ybtm_vect))
397 ytop_vect /= np.sqrt(np.dot(ytop_vect, ytop_vect))
399 zbtm_vect = np.array([center_vect[0], center_vect[1],
400 np.min(source_array[:, 2])], dtype=np.float64)
401 ztop_vect = np.array([center_vect[0], center_vect[1],
402 np.max(source_array[:, 2])], dtype=np.float64)
403 zbtm_vect /= np.sqrt(np.dot(zbtm_vect, zbtm_vect))
404 ztop_vect /= np.sqrt(np.dot(ztop_vect, ztop_vect))
407 return np.array([xbtm_vect, xtop_vect, ybtm_vect, ytop_vect,
408 zbtm_vect, ztop_vect])
410 def _construct_pattern_and_shift_rot_matrix(self, src_pattern_array,
411 n_match, max_cos_theta_shift,
412 max_cos_rot_sq, max_dist_rad):
413 """Test an input source pattern against the reference catalog. 415 Returns the candidate matched patterns and their 416 implied rotation matrices or None. 420 src_pattern_array : `numpy.ndarray`, (N, 3) 421 Sub selection of source 3 vectors to create a pattern from 423 Number of points to attempt to create a pattern from. Must be 424 >= len(src_pattern_array) 425 max_cos_theta_shift : `float` 426 Maximum shift allowed between two patterns' centers. 427 max_cos_rot_sq : `float` 428 Maximum rotation between two patterns that have been shifted 429 to have their centers on top of each other. 430 max_dist_rad : `float` 431 Maximum delta distance allowed between the source and reference 432 pair distances to consider the reference pair a candidate for 433 the source pair. Also sets the tolerance between the opening 434 angles of the spokes when compared to the reference. 438 output_matched_pattern : `lsst.pipe.base.Struct` 439 Result struct with components: 441 - ``ref_candidates`` : ids of the matched pattern in the internal 442 reference_array object (`list` of `int`). 443 - ``src_candidates`` : Pattern ids of the sources matched 445 - ``shift_rot_matrix_src_to_ref`` : 3x3 matrix specifying the full 446 shift and rotation between the reference and source objects. 447 Rotates source into reference frame. `None` if match is not 448 found. (`numpy.ndarray`, (3, 3)) 449 - ``shift_rot_matrix_ref_to_src`` : 3x3 matrix specifying the full 450 shift and rotation of the reference and source objects. Rotates 451 reference into source frame. None if match is not found 452 (`numpy.ndarray`, (3, 3)). 453 - ``cos_shift`` : Magnitude of the shift found between the two 454 patten centers. `None` if match is not found (`float`). 455 - ``sin_rot`` : float value of the rotation to align the already 456 shifted source pattern to the reference pattern. `None` if no match 464 output_matched_pattern = pipeBase.Struct(
467 shift_rot_matrix=
None,
473 src_delta_array = np.empty((len(src_pattern_array) - 1, 3))
474 src_delta_array[:, 0] = (src_pattern_array[1:, 0] -
475 src_pattern_array[0, 0])
476 src_delta_array[:, 1] = (src_pattern_array[1:, 1] -
477 src_pattern_array[0, 1])
478 src_delta_array[:, 2] = (src_pattern_array[1:, 2] -
479 src_pattern_array[0, 2])
480 src_dist_array = np.sqrt(src_delta_array[:, 0]**2 +
481 src_delta_array[:, 1]**2 +
482 src_delta_array[:, 2]**2)
491 for ref_dist_idx
in ref_dist_index_array:
495 tmp_ref_pair_list = self.
_id_array[ref_dist_idx]
496 for pair_idx, ref_id
in enumerate(tmp_ref_pair_list):
497 src_candidates = [0, 1]
499 shift_rot_matrix =
None 506 cos_shift = np.dot(src_pattern_array[0], ref_center)
507 if cos_shift < max_cos_theta_shift:
511 ref_candidates.append(ref_id)
517 ref_candidates.append(
518 tmp_ref_pair_list[1])
520 ref_candidates.append(
521 tmp_ref_pair_list[0])
530 src_pattern_array[0], ref_center, src_delta_array[0],
531 ref_delta, cos_shift, max_cos_rot_sq)
532 if test_rot_struct.cos_rot_sq
is None or \
533 test_rot_struct.proj_ref_ctr_delta
is None or \
534 test_rot_struct.shift_matrix
is None:
538 cos_rot_sq = test_rot_struct.cos_rot_sq
539 proj_ref_ctr_delta = test_rot_struct.proj_ref_ctr_delta
540 shift_matrix = test_rot_struct.shift_matrix
552 src_pattern_array[0], src_delta_array, src_dist_array,
554 tmp_ref_delta_array, tmp_ref_dist_array,
555 tmp_ref_id_array, max_dist_rad, n_match)
559 if len(pattern_spoke_struct.ref_spoke_list) < n_match - 2
or \
560 len(pattern_spoke_struct.src_spoke_list) < n_match - 2:
564 ref_candidates.extend(pattern_spoke_struct.ref_spoke_list)
565 src_candidates.extend(pattern_spoke_struct.src_spoke_list)
571 cos_rot_sq, shift_matrix, src_delta_array[0],
575 if shift_rot_struct.sin_rot
is None or \
576 shift_rot_struct.shift_rot_matrix
is None:
580 sin_rot = shift_rot_struct.sin_rot
581 shift_rot_matrix = shift_rot_struct.shift_rot_matrix
590 src_pattern_array[src_candidates],
592 shift_rot_matrix, max_dist_rad)
594 if fit_shift_rot_matrix
is not None:
596 output_matched_pattern.ref_candidates = ref_candidates
597 output_matched_pattern.src_candidates = src_candidates
598 output_matched_pattern.shift_rot_matrix = \
600 output_matched_pattern.cos_shift = cos_shift
601 output_matched_pattern.sin_rot = sin_rot
602 return output_matched_pattern
604 return output_matched_pattern
606 def _find_candidate_reference_pairs(self, src_dist, ref_dist_array,
608 """Wrap numpy.searchsorted to find the range of reference spokes 609 within a spoke distance tolerance of our source spoke. 611 Returns an array sorted from the smallest absolute delta distance 612 between source and reference spoke length. This sorting increases the 613 speed for the pattern search greatly. 618 float value of the distance we would like to search for in 619 the reference array in radians. 620 ref_dist_array : `numpy.ndarray`, (N,) 621 sorted array of distances in radians. 622 max_dist_rad : `float` 623 maximum plus/minus search to find in the reference array in 628 tmp_diff_array : `numpy.ndarray`, (N,) 629 indices lookup into the input ref_dist_array sorted by the 630 difference in value to the src_dist from absolute value 635 start_idx = np.searchsorted(ref_dist_array, src_dist - max_dist_rad)
636 end_idx = np.searchsorted(ref_dist_array, src_dist + max_dist_rad,
640 if start_idx == end_idx:
646 if end_idx > ref_dist_array.shape[0]:
647 end_idx = ref_dist_array.shape[0]
652 tmp_diff_array = np.fabs(ref_dist_array[start_idx:end_idx] - src_dist)
653 return tmp_diff_array.argsort() + start_idx
655 def _test_rotation(self, src_center, ref_center, src_delta, ref_delta,
656 cos_shift, max_cos_rot_sq):
657 """ Test if the rotation implied between the source 658 pattern and reference pattern is within tolerance. To test this 659 we need to create the first part of our spherical rotation matrix 660 which we also return for use later. 664 src_center : `numpy.ndarray`, (N, 3) 666 ref_center : `numpy.ndarray`, (N, 3) 667 3 vector defining the center of the candidate reference pinwheel 669 src_delta : `numpy.ndarray`, (N, 3) 670 3 vector delta between the source pattern center and the end of 672 ref_delta : `numpy.ndarray`, (N, 3) 673 3 vector delta of the candidate matched reference pair 675 Cosine of the angle between the source and reference candidate 677 max_cos_rot_sq : `float` 678 candidate reference pair after shifting the centers on top of each 679 other. The function will return None if the rotation implied is 680 greater than max_cos_rot_sq. 684 result : `lsst.pipe.base.Struct` 685 Result struct with components: 687 - ``cos_rot_sq`` : magnitude of the rotation needed to align the 688 two patterns after their centers are shifted on top of each 689 other. `None` if rotation test fails (`float`). 690 - ``shift_matrix`` : 3x3 rotation matrix describing the shift needed to 691 align the source and candidate reference center. `None` if rotation 692 test fails (`numpy.ndarray`, (N, 3)). 698 elif cos_shift < -1.0:
700 sin_shift = np.sqrt(1 - cos_shift ** 2)
706 rot_axis = np.cross(src_center, ref_center)
707 rot_axis /= sin_shift
709 rot_axis, cos_shift, sin_shift)
711 shift_matrix = np.identity(3)
715 rot_src_delta = np.dot(shift_matrix, src_delta)
716 proj_src_delta = (rot_src_delta -
717 np.dot(rot_src_delta, ref_center) * ref_center)
718 proj_ref_delta = (ref_delta -
719 np.dot(ref_delta, ref_center) * ref_center)
720 cos_rot_sq = (np.dot(proj_src_delta, proj_ref_delta) ** 2 /
721 (np.dot(proj_src_delta, proj_src_delta) *
722 np.dot(proj_ref_delta, proj_ref_delta)))
724 if cos_rot_sq < max_cos_rot_sq:
725 return pipeBase.Struct(
727 proj_ref_ctr_delta=
None,
731 return pipeBase.Struct(
732 cos_rot_sq=cos_rot_sq,
733 proj_ref_ctr_delta=proj_ref_delta,
734 shift_matrix=shift_matrix,)
736 def _create_spherical_rotation_matrix(self, rot_axis, cos_rotation,
738 """Construct a generalized 3D rotation matrix about a given 743 rot_axis : `numpy.ndarray`, (3,) 744 3 vector defining the axis to rotate about. 745 cos_rotation : `float` 746 cosine of the rotation angle. 747 sin_rotation : `float` 748 sine of the rotation angle. 752 shift_matrix : `numpy.ndarray`, (3, 3) 753 3x3 spherical, rotation matrix. 756 rot_cross_matrix = np.array(
757 [[0., -rot_axis[2], rot_axis[1]],
758 [rot_axis[2], 0., -rot_axis[0]],
759 [-rot_axis[1], rot_axis[0], 0.]], dtype=np.float64)
760 shift_matrix = (cos_rotation*np.identity(3) +
761 sin_rotion*rot_cross_matrix +
762 (1. - cos_rotation)*np.outer(rot_axis, rot_axis))
766 def _create_pattern_spokes(self, src_ctr, src_delta_array, src_dist_array,
767 ref_ctr, ref_ctr_id, proj_ref_ctr_delta,
768 ref_delta_array, ref_dist_array,
769 ref_id_array, max_dist_rad, n_match):
770 """ Create the individual spokes that make up the pattern now that the 771 shift and rotation are within tolerance. 773 If we can't create a valid pattern we exit early. 777 src_ctr : `numpy.ndarray`, (3,) 778 3 vector of the source pinwheel center 779 src_delta_array : `numpy.ndarray`, (N, 3) 780 Array of 3 vector deltas between the source center and the pairs 781 that make up the remaining spokes of the pinwheel 782 src_dist_array : `numpy.ndarray`, (N, 3) 783 Array of the distances of each src_delta in the pinwheel 784 ref_ctr : `numpy.ndarray`, (3,) 785 3 vector of the candidate reference center 787 id of the ref_ctr in the master reference array 788 proj_ref_ctr_delta : `numpy.ndarray`, (3,) 789 Plane projected 3 vector formed from the center point of the 790 candidate pin-wheel and the second point in the pattern to create 791 the first spoke pair. This is the candidate pair that was matched 792 in the main _construct_pattern_and_shift_rot_matrix loop 793 ref_delta_array : `numpy.ndarray`, (N,3) 794 Array of 3 vector deltas that are have the current candidate 795 reference center as part of the pair 796 ref_dist_array : `numpy.ndarray`, (N,) 797 Array of vector distances for each of the reference pairs 798 ref_id_array : `numpy.ndarray`, (N,) 799 Array of id lookups into the master reference array that our 800 center id object is paired with. 801 max_dist_rad : `float` 802 Maximum search distance 804 Number of source deltas that must be matched into the reference 805 deltas in order to consider this a successful pattern match. 809 output_spokes : `lsst.pipe.base.Struct` 810 Result struct with components: 812 - ``ref_spoke_list`` : list of ints specifying ids into the master 813 reference array (`list` of `int`). 814 - ``src_spoke_list`` : list of ints specifying indices into the 815 current source pattern that is being tested (`list` of `int`). 818 output_spokes = pipeBase.Struct(
830 proj_src_ctr_delta = (src_delta_array[0] -
831 np.dot(src_delta_array[0], src_ctr) * src_ctr)
832 proj_src_ctr_dist_sq = np.dot(proj_src_ctr_delta, proj_src_ctr_delta)
835 proj_ref_ctr_dist_sq = np.dot(proj_ref_ctr_delta, proj_ref_ctr_delta)
838 for src_idx
in range(1, len(src_dist_array)):
839 if n_fail > len(src_dist_array) - (n_match - 1):
844 src_sin_tol = (max_dist_rad /
845 (src_dist_array[src_idx] + max_dist_rad))
852 if src_sin_tol > max_sin_tol:
853 src_sin_tol = max_sin_tol
858 src_delta_array[src_idx] -
859 np.dot(src_delta_array[src_idx], src_ctr) * src_ctr)
860 geom_dist_src = np.sqrt(
861 np.dot(proj_src_delta, proj_src_delta) *
862 proj_src_ctr_dist_sq)
865 cos_theta_src = (np.dot(proj_src_delta, proj_src_ctr_delta) /
867 cross_src = (np.cross(proj_src_delta, proj_src_ctr_delta) /
869 sin_theta_src = np.dot(cross_src, src_ctr)
874 src_dist_array[src_idx], ref_dist_array, max_dist_rad)
884 proj_ref_ctr_dist_sq,
895 ref_spoke_list.append(ref_id)
896 src_spoke_list.append(src_idx + 1)
900 if len(ref_spoke_list) >= n_match - 2:
902 output_spokes.ref_spoke_list = ref_spoke_list
903 output_spokes.src_spoke_list = src_spoke_list
908 def _test_spoke(self, cos_theta_src, sin_theta_src, ref_ctr, ref_ctr_id,
909 proj_ref_ctr_delta, proj_ref_ctr_dist_sq,
910 ref_dist_idx_array, ref_delta_array,
911 ref_id_array, src_sin_tol):
912 """Test the opening angle between the first spoke of our pattern 913 for the source object against the reference object. 915 This method makes heavy use of the small angle approximation to perform 920 cos_theta_src : `float` 921 Cosine of the angle between the current candidate source spoke and 923 sin_theta_src : `float` 924 Sine of the angle between the current candidate source spoke and 926 ref_ctr : `numpy.ndarray`, (3,) 927 3 vector of the candidate reference center 929 id lookup of the ref_ctr into the master reference array 930 proj_ref_ctr_delta : `float` 931 Plane projected first spoke in the reference pattern using the 932 pattern center as normal. 933 proj_ref_ctr_dist_sq : `float` 934 Squared length of the projected vector. 935 ref_dist_idx_array : `numpy.ndarray`, (N,) 936 Indices sorted by the delta distance between the source 937 spoke we are trying to test and the candidate reference 939 ref_delta_array : `numpy.ndarray`, (N, 3) 940 Array of 3 vector deltas that are have the current candidate 941 reference center as part of the pair 942 ref_id_array : `numpy.ndarray`, (N,) 943 Array of id lookups into the master reference array that our 944 center id object is paired with. 945 src_sin_tol : `float` 946 Sine of tolerance allowed between source and reference spoke 952 If we can not find a candidate spoke we return `None` else we 953 return an int id into the master reference array. 957 for ref_dist_idx
in ref_dist_idx_array:
960 if ref_id_array[ref_dist_idx] < ref_ctr_id:
966 ref_delta_array[ref_dist_idx] -
967 np.dot(ref_delta_array[ref_dist_idx], ref_ctr) * ref_ctr)
968 geom_dist_ref = np.sqrt(proj_ref_ctr_dist_sq *
969 np.dot(proj_ref_delta, proj_ref_delta))
970 cos_theta_ref = ref_sign * (
971 np.dot(proj_ref_delta, proj_ref_ctr_delta) /
976 if cos_theta_ref ** 2 < (1 - src_sin_tol ** 2):
977 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
978 (1 - cos_theta_ref ** 2))
980 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
985 if cos_sq_comparison > src_sin_tol ** 2:
991 cross_ref = ref_sign * (
992 np.cross(proj_ref_delta, proj_ref_ctr_delta) /
994 sin_theta_ref = np.dot(cross_ref, ref_ctr)
998 if abs(cos_theta_src) < src_sin_tol:
999 sin_comparison = (sin_theta_src - sin_theta_ref) / src_sin_tol
1002 (sin_theta_src - sin_theta_ref) / cos_theta_ref
1004 if abs(sin_comparison) > src_sin_tol:
1008 return ref_id_array[ref_dist_idx]
1012 def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta,
1013 ref_ctr, ref_delta):
1014 """ Create the final part of our spherical rotation matrix. 1018 cos_rot_sq : `float` 1019 cosine of the rotation needed to align our source and reference 1021 shift_matrix : `numpy.ndarray`, (3, 3) 1022 3x3 rotation matrix for shifting the source pattern center on top 1023 of the candidate reference pattern center. 1024 src_delta : `numpy.ndarray`, (3,) 1025 3 vector delta of representing the first spoke of the source 1027 ref_ctr : `numpy.ndarray`, (3,) 1028 3 vector on the unit-sphere representing the center of our 1030 ref_delta : `numpy.ndarray`, (3,) 1031 3 vector delta made by the first pair of the reference pattern. 1035 result : `lsst.pipe.base.Struct` 1036 Result struct with components: 1038 - ``sin_rot`` : float sine of the amount of rotation between the 1039 source and reference pattern. We use sine here as it is 1040 signed and tells us the chirality of the rotation (`float`). 1041 - ``shift_rot_matrix`` : float array representing the 3x3 rotation 1042 matrix that takes the source pattern and shifts and rotates 1043 it to align with the reference pattern (`numpy.ndarray`, (3,3)). 1045 cos_rot = np.sqrt(cos_rot_sq)
1046 rot_src_delta = np.dot(shift_matrix, src_delta)
1047 delta_dot_cross = np.dot(np.cross(rot_src_delta, ref_delta), ref_ctr)
1049 sin_rot = np.sign(delta_dot_cross) * np.sqrt(1 - cos_rot_sq)
1051 ref_ctr, cos_rot, sin_rot)
1053 shift_rot_matrix = np.dot(rot_matrix, shift_matrix)
1055 return pipeBase.Struct(
1057 shift_rot_matrix=shift_rot_matrix,)
1059 def _intermediate_verify(self, src_pattern, ref_pattern, shift_rot_matrix,
1061 """ Perform an intermediate verify step. 1063 Rotate the matches references into the source frame and test their 1064 distances against tolerance. Only return true if all points are within 1069 src_pattern : `numpy.ndarray`, (N,3) 1070 Array of 3 vectors representing the points that make up our source 1072 ref_pattern : `numpy.ndarray`, (N,3) 1073 Array of 3 vectors representing our candidate reference pinwheel 1075 shift_rot_matrix : `numpy.ndarray`, (3,3) 1076 3x3 rotation matrix that takes the source objects and rotates them 1077 onto the frame of the reference objects 1078 max_dist_rad : `float` 1079 Maximum distance allowed to consider two objects the same. 1083 fit_shift_rot_matrix : `numpy.ndarray`, (3,3) 1084 Return the fitted shift/rotation matrix if all of the points in our 1085 source pattern are within max_dist_rad of their matched reference 1086 objects. Returns None if this criteria is not satisfied. 1088 if len(src_pattern) != len(ref_pattern):
1090 "Source pattern length does not match ref pattern.\n" 1091 "\t source pattern len=%i, reference pattern len=%i" %
1092 (len(src_pattern), len(ref_pattern)))
1095 src_pattern, ref_pattern, shift_rot_matrix, max_dist_rad):
1103 fit_shift_rot_matrix = least_squares(
1104 _rotation_matrix_chi_sq,
1105 x0=shift_rot_matrix.flatten(),
1106 args=(src_pattern, ref_pattern, max_dist_rad)
1110 src_pattern, ref_pattern, fit_shift_rot_matrix,
1112 return fit_shift_rot_matrix
1116 def _intermediate_verify_comparison(self, pattern_a, pattern_b,
1117 shift_rot_matrix, max_dist_rad):
1118 """Test the input rotation matrix against one input pattern and 1121 If every point in the pattern after rotation is within a distance of 1122 max_dist_rad to its candidate point in the other pattern, we return 1127 pattern_a : `numpy.ndarray`, (N,3) 1128 Array of 3 vectors representing the points that make up our source 1130 pattern_b : `numpy.ndarray`, (N,3) 1131 Array of 3 vectors representing our candidate reference pinwheel 1133 shift_rot_matrix : `numpy.ndarray`, (3,3) 1134 3x3 rotation matrix that takes the source objects and rotates them 1135 onto the frame of the reference objects 1136 max_dist_rad : `float` 1137 Maximum distance allowed to consider two objects the same. 1143 True if all rotated source points are within max_dist_rad of 1144 the candidate references matches. 1146 shifted_pattern_a = np.dot(shift_rot_matrix,
1147 pattern_a.transpose()).transpose()
1148 tmp_delta_array = shifted_pattern_a - pattern_b
1149 tmp_dist_array = (tmp_delta_array[:, 0] ** 2 +
1150 tmp_delta_array[:, 1] ** 2 +
1151 tmp_delta_array[:, 2] ** 2)
1152 return np.all(tmp_dist_array < max_dist_rad ** 2)
1154 def _test_pattern_lengths(self, test_pattern, max_dist_rad):
1155 """ Test that the all vectors in a pattern are unit length within 1158 This is useful for assuring the non unitary transforms do not contain 1159 too much distortion. 1163 test_pattern : `numpy.ndarray`, (N, 3) 1164 Test vectors at the maximum and minimum x, y, z extents. 1165 max_dist_rad : `float` 1166 maximum distance in radians to consider two points "agreeing" on 1174 dists = (test_pattern[:, 0] ** 2 +
1175 test_pattern[:, 1] ** 2 +
1176 test_pattern[:, 2] ** 2)
1178 np.logical_and((1 - max_dist_rad) ** 2 < dists,
1179 dists < (1 + max_dist_rad) ** 2))
1181 def _test_rotation_agreement(self, rot_vects, max_dist_rad):
1182 """ Test this rotation against the previous N found and return 1183 the number that a agree within tolerance to where our test 1188 rot_vects : `numpy.ndarray`, (N, 3) 1189 Arrays of rotated 3 vectors representing the maximum x, y, 1190 z extent on the unit sphere of the input source objects rotated by 1191 the candidate rotations into the reference frame. 1192 max_dist_rad : `float` 1193 maximum distance in radians to consider two points "agreeing" on 1199 Number of candidate rotations that agree for all of the rotated 1203 self.
log.debug(
"Comparing pattern %i to previous %i rotations..." %
1204 (rot_vects[-1][-1], len(rot_vects) - 1))
1207 for rot_idx
in range(max((len(rot_vects) - 1), 0)):
1209 for vect_idx
in range(len(rot_vects[rot_idx]) - 1):
1210 tmp_delta_vect = (rot_vects[rot_idx][vect_idx] -
1211 rot_vects[-1][vect_idx])
1212 tmp_dist_list.append(
1213 np.dot(tmp_delta_vect, tmp_delta_vect))
1214 if np.all(np.array(tmp_dist_list) < max_dist_rad ** 2):
1218 def _match_sources(self,
1221 """ Shift both the reference and source catalog to the the respective 1222 frames and find their nearest neighbor using a kdTree. 1224 Removes all matches who do not agree when either the reference or 1225 source catalog is rotated. Cuts on a maximum distance are left to an 1230 source_array : `numpy.ndarray`, (N, 3) 1231 array of 3 vectors representing the source objects we are trying 1232 to match into the source catalog. 1233 shift_rot_matrix : `numpy.ndarray`, (3, 3) 1234 3x3 rotation matrix that performs the spherical rotation from the 1235 source frame into the reference frame. 1239 results : `lsst.pipe.base.Struct` 1240 Result struct with components: 1242 - ``matches`` : array of integer ids into the source and 1243 reference arrays. Matches are only returned for those that 1244 satisfy the distance and handshake criteria 1245 (`numpy.ndarray`, (N, 2)). 1246 - ``distances`` : Distances between each match in radians after 1247 the shift and rotation is applied (`numpy.ndarray`, (N)). 1249 shifted_references = np.dot(
1250 np.linalg.inv(shift_rot_matrix),
1252 shifted_sources = np.dot(
1254 source_array.transpose()).transpose()
1256 ref_matches = np.empty((len(shifted_references), 2),
1258 src_matches = np.empty((len(shifted_sources), 2),
1261 ref_matches[:, 1] = np.arange(len(shifted_references),
1263 src_matches[:, 0] = np.arange(len(shifted_sources),
1267 src_kdtree = cKDTree(source_array)
1269 ref_to_src_dist, tmp_ref_to_src_idx = \
1270 src_kdtree.query(shifted_references)
1271 src_to_ref_dist, tmp_src_to_ref_idx = \
1272 ref_kdtree.query(shifted_sources)
1274 ref_matches[:, 0] = tmp_ref_to_src_idx
1275 src_matches[:, 1] = tmp_src_to_ref_idx
1278 return pipeBase.Struct(
1279 match_ids=src_matches[handshake_mask],
1280 distances_rad=src_to_ref_dist[handshake_mask],)
1282 def _handshake_match(self, matches_src, matches_ref):
1283 """Return only those matches where both the source 1284 and reference objects agree they they are each others' 1289 matches_src : `numpy.ndarray`, (N, 2) 1290 int array of nearest neighbor matches between shifted and 1291 rotated reference objects matched into the sources. 1292 matches_ref : `numpy.ndarray`, (N, 2) 1293 int array of nearest neighbor matches between shifted and 1294 rotated source objects matched into the references. 1297 handshake_mask_array : `numpy.ndarray`, (N,) 1298 Return the array positions where the two match catalogs agree. 1300 handshake_mask_array = np.zeros(len(matches_src), dtype=np.bool)
1302 for src_match_idx, match
in enumerate(matches_src):
1303 ref_match_idx = np.searchsorted(matches_ref[:, 1], match[1])
1304 if match[0] == matches_ref[ref_match_idx, 0]:
1305 handshake_mask_array[src_match_idx] =
True 1306 return handshake_mask_array
def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta, ref_ctr, ref_delta)
def _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)