3 from scipy.optimize
import least_squares
4 from scipy.spatial
import cKDTree
6 import lsst.pipe.base
as pipeBase
9 def _rotation_matrix_chi_sq(flattened_rot_matrix,
13 """Compute the squared differences for least squares fitting. 15 Given a flattened rotation matrix, one N point pattern and another N point 16 pattern to transform into to, compute the squared differences between the 17 points in the two patterns after the rotation. 21 flattened_rot_matrix : `numpy.ndarray`, (9, ) 22 A flattened array representing a 3x3 rotation matrix. The array is 23 flattened to comply with the API of scipy.optimize.least_squares. 24 Flattened elements are [[0, 0], [0, 1], [0, 2], [1, 0]...] 25 pattern_a : `numpy.ndarray`, (N, 3) 26 A array containing N, 3 vectors representing the objects we would like 27 to transform into the frame of pattern_b. 28 pattern_b : `numpy.ndarray`, (N, 3) 29 A array containing N, 3 vectors representing the reference frame we 30 would like to transform pattern_a into. 31 max_dist_rad : `float` 32 The maximum distance allowed from the pattern matching. This value is 33 used as the standard error for the resultant chi values. 37 noralized_diff : `numpy.ndarray`, (9,) 38 Array of differences between the vectors representing of the source 39 pattern rotated into the reference frame and the converse. This is 40 used to minimize in a least squares fitter. 43 rot_matrix = flattened_rot_matrix.reshape((3, 3))
45 rot_pattern_a = np.dot(rot_matrix, pattern_a.transpose()).transpose()
46 diff_pattern_a_to_b = rot_pattern_a - pattern_b
49 return diff_pattern_a_to_b.flatten() / max_dist_rad
53 """Class implementing a pessimistic version of Optimistic Pattern Matcher 54 B (OPMb) from Tabur 2007. See `DMTN-031 <http://ls.st/DMTN-031`_ 58 reference_array : `numpy.ndarray`, (N, 3) 59 spherical points x, y, z of to use as reference objects for 62 Logger for outputting debug info. 66 The class loads and stores the reference object 67 in a convenient data structure for matching any set of source objects that 68 are assumed to contain each other. The pessimistic nature of the algorithm 69 comes from requiring that it discovers at least two patterns that agree on 70 the correct shift and rotation for matching before exiting. The original 71 behavior of OPMb can be recovered simply. Patterns matched between the 72 input datasets are n-spoked pinwheels created from n+1 points. Refer to 73 DMTN #031 for more details. http://github.com/lsst-dm/dmtn-031 83 def _build_distances_and_angles(self):
84 """Create the data structures we will use to search for our pattern 87 Throughout this function and the rest of the class we use id to 88 reference the position in the input reference catalog and index to 89 'index' into the arrays sorted on distance. 103 sub_id_array_list = []
104 sub_dist_array_list = []
112 sub_id_array = np.zeros((self.
_n_reference - 1 - ref_id, 2),
114 sub_id_array[:, 0] = ref_id
115 sub_id_array[:, 1] = np.arange(ref_id + 1, self.
_n_reference,
121 ref_obj).astype(np.float32)
122 sub_dist_array = np.sqrt(sub_delta_array[:, 0] ** 2 +
123 sub_delta_array[:, 1] ** 2 +
124 sub_delta_array[:, 2] ** 2)
128 sub_id_array_list.append(sub_id_array)
129 sub_dist_array_list.append(sub_dist_array)
146 ref_id, sorted_pair_dist_args]
148 ref_id, sorted_pair_dist_args]
151 unsorted_id_array = np.concatenate(sub_id_array_list)
152 unsorted_dist_array = np.concatenate(sub_dist_array_list)
156 sorted_dist_args = unsorted_dist_array.argsort()
157 self.
_dist_array = unsorted_dist_array[sorted_dist_args]
158 self.
_id_array = unsorted_id_array[sorted_dist_args]
162 def match(self, source_array, n_check, n_match, n_agree,
163 max_n_patterns, max_shift, max_rotation, max_dist,
164 min_matches, pattern_skip_array=None):
165 """Match a given source catalog into the loaded reference catalog. 167 Given array of points on the unit sphere and tolerances, we 168 attempt to match a pinwheel like pattern between these input sources 169 and the reference objects this class was created with. This pattern 170 informs of the shift and rotation needed to align the input source 171 objects into the frame of the references. 175 source_array : `numpy.ndarray`, (N, 3) 176 An array of spherical x,y,z coordinates and a magnitude in units 177 of objects having a lower value for sorting. The array should be 180 Number of sources to create a pattern from. Not all objects may be 181 checked if n_match criteria is before looping through all n_check 184 Number of objects to use in constructing a pattern to match. 186 Number of found patterns that must agree on their shift and 187 rotation before exiting. Set this value to 1 to recover the 188 expected behavior of Optimistic Pattern Matcher B. 189 max_n_patters : `int` 190 Number of patterns to create from the input source objects to 191 attempt to match into the reference objects. 193 Maximum allowed shift to match patterns in arcseconds. 194 max_rotation : `float` 195 Maximum allowed rotation between patterns in degrees. 197 Maximum distance in arcseconds allowed between candidate spokes in 198 the source and reference objects. Also sets that maximum distance 199 in the intermediate verify, pattern shift/rotation agreement, and 201 pattern_skip_array : `int` 202 Patterns we would like to skip. This could be due to the pattern 203 being matched on a previous iteration that we now consider invalid. 204 This assumes the ordering of the source objects is the same 205 between different runs of the matcher which, assuming no object 206 has been inserted or the magnitudes have changed, it should be. 210 output_struct : `lsst.pipe.base.Struct` 211 Result struct with components 213 - ``matches`` : (N, 2) array of matched ids for pairs. Empty list if no 214 match found (`numpy.ndarray`, (N, 2) or `list`) 215 - ``distances_rad`` : Radian distances between the matched objects. 216 Empty list if no match found (`numpy.ndarray`, (N,)) 217 - ``pattern_idx``: Index of matched pattern. None if no match found 219 - ``shift`` : Magnitude for the shift between the source and reference 220 objects in arcseconds. None if no match found (`float`). 224 sorted_source_array = source_array[source_array[:, -1].argsort(), :3]
225 n_source = len(sorted_source_array)
228 output_match_struct = pipeBase.Struct(
235 self.
log.warn(
"Source object array is empty. Unable to match. " 249 max_cos_shift = np.cos(np.radians(max_shift / 3600.))
250 max_cos_rot_sq = np.cos(np.radians(max_rotation)) ** 2
251 max_dist_rad = np.radians(max_dist / 3600.)
255 for pattern_idx
in range(np.min((max_n_patterns,
256 n_source - n_match))):
260 if pattern_skip_array
is not None and \
261 np.any(pattern_skip_array == pattern_idx):
263 "Skipping previously matched bad pattern %i..." %
267 pattern = sorted_source_array[
268 pattern_idx: np.min((pattern_idx + n_check, n_source)), :3]
273 construct_return_struct = \
275 pattern, n_match, max_cos_shift, max_cos_rot_sq,
279 if construct_return_struct.ref_candidates
is None or \
280 construct_return_struct.shift_rot_matrix
is None or \
281 construct_return_struct.cos_shift
is None or \
282 construct_return_struct.sin_rot
is None:
286 ref_candidates = construct_return_struct.ref_candidates
287 shift_rot_matrix = construct_return_struct.shift_rot_matrix
288 cos_shift = construct_return_struct.cos_shift
289 sin_rot = construct_return_struct.sin_rot
293 if len(ref_candidates) < n_match:
299 tmp_rot_vect_list = []
300 for test_vect
in test_vectors:
301 tmp_rot_vect_list.append(np.dot(shift_rot_matrix, test_vect))
308 tmp_rot_vect_list.append(pattern_idx)
309 rot_vect_list.append(tmp_rot_vect_list)
321 n_matched = len(match_sources_struct.match_ids[
322 match_sources_struct.distances_rad < max_dist_rad])
325 if n_matched >= min_matches:
327 shift = np.degrees(np.arccos(cos_shift)) * 3600.
329 self.
log.debug(
"Succeeded after %i patterns." % pattern_idx)
330 self.
log.debug(
"\tShift %.4f arcsec" % shift)
331 self.
log.debug(
"\tRotation: %.4f deg" %
332 np.degrees(np.arcsin(sin_rot)))
335 output_match_struct.match_ids = \
336 match_sources_struct.match_ids
337 output_match_struct.distances_rad = \
338 match_sources_struct.distances_rad
339 output_match_struct.pattern_idx = pattern_idx
340 output_match_struct.shift = shift
341 return output_match_struct
343 self.
log.debug(
"Failed after %i patterns." % (pattern_idx + 1))
344 return output_match_struct
346 def _compute_test_vectors(self, source_array):
347 """Compute spherical 3 vectors at the edges of the x, y, z extent 348 of the input source catalog. 352 source_array : `numpy.ndarray`, (N, 3) 353 array of 3 vectors representing positions on the unit 358 test_vectors : `numpy.ndarray`, (N, 3) 359 Array of vectors representing the maximum extents in x, y, z 360 of the input source array. These are used with the rotations 361 the code finds to test for agreement from different patterns 362 when the code is running in pessimistic mode. 366 if np.any(np.logical_not(np.isfinite(source_array))):
367 self.
log.warn(
"Input source objects contain non-finite values. " 368 "This could end badly.")
369 center_vect = np.nanmean(source_array, axis=0)
373 xbtm_vect = np.array([np.min(source_array[:, 0]), center_vect[1],
374 center_vect[2]], dtype=np.float64)
375 xtop_vect = np.array([np.max(source_array[:, 0]), center_vect[1],
376 center_vect[2]], dtype=np.float64)
377 xbtm_vect /= np.sqrt(np.dot(xbtm_vect, xbtm_vect))
378 xtop_vect /= np.sqrt(np.dot(xtop_vect, xtop_vect))
380 ybtm_vect = np.array([center_vect[0], np.min(source_array[:, 1]),
381 center_vect[2]], dtype=np.float64)
382 ytop_vect = np.array([center_vect[0], np.max(source_array[:, 1]),
383 center_vect[2]], dtype=np.float64)
384 ybtm_vect /= np.sqrt(np.dot(ybtm_vect, ybtm_vect))
385 ytop_vect /= np.sqrt(np.dot(ytop_vect, ytop_vect))
387 zbtm_vect = np.array([center_vect[0], center_vect[1],
388 np.min(source_array[:, 2])], dtype=np.float64)
389 ztop_vect = np.array([center_vect[0], center_vect[1],
390 np.max(source_array[:, 2])], dtype=np.float64)
391 zbtm_vect /= np.sqrt(np.dot(zbtm_vect, zbtm_vect))
392 ztop_vect /= np.sqrt(np.dot(ztop_vect, ztop_vect))
395 return np.array([xbtm_vect, xtop_vect, ybtm_vect, ytop_vect,
396 zbtm_vect, ztop_vect])
398 def _construct_pattern_and_shift_rot_matrix(self, src_pattern_array,
399 n_match, max_cos_theta_shift,
400 max_cos_rot_sq, max_dist_rad):
401 """Test an input source pattern against the reference catalog. 403 Returns the candidate matched patterns and their 404 implied rotation matrices or None. 408 src_pattern_array : `numpy.ndarray`, (N, 3) 409 Sub selection of source 3 vectors to create a pattern from 411 Number of points to attempt to create a pattern from. Must be 412 >= len(src_pattern_array) 413 max_cos_theta_shift : `float` 414 Maximum shift allowed between two patterns' centers. 415 max_cos_rot_sq : `float` 416 Maximum rotation between two patterns that have been shifted 417 to have their centers on top of each other. 418 max_dist_rad : `float` 419 Maximum delta distance allowed between the source and reference 420 pair distances to consider the reference pair a candidate for 421 the source pair. Also sets the tolerance between the opening 422 angles of the spokes when compared to the reference. 426 output_matched_pattern : `lsst.pipe.base.Struct` 427 Result struct with components: 429 - ``ref_candidates`` : ids of the matched pattern in the internal 430 reference_array object (`list` of `int`). 431 - ``src_candidates`` : Pattern ids of the sources matched 433 - ``shift_rot_matrix_src_to_ref`` : 3x3 matrix specifying the full 434 shift and rotation between the reference and source objects. 435 Rotates source into reference frame. `None` if match is not 436 found. (`numpy.ndarray`, (3, 3)) 437 - ``shift_rot_matrix_ref_to_src`` : 3x3 matrix specifying the full 438 shift and rotation of the reference and source objects. Rotates 439 reference into source frame. None if match is not found 440 (`numpy.ndarray`, (3, 3)). 441 - ``cos_shift`` : Magnitude of the shift found between the two 442 patten centers. `None` if match is not found (`float`). 443 - ``sin_rot`` : float value of the rotation to align the already 444 shifted source pattern to the reference pattern. `None` if no match 452 output_matched_pattern = pipeBase.Struct(
455 shift_rot_matrix=
None,
461 src_delta_array = np.empty((len(src_pattern_array) - 1, 3))
462 src_delta_array[:, 0] = (src_pattern_array[1:, 0] -
463 src_pattern_array[0, 0])
464 src_delta_array[:, 1] = (src_pattern_array[1:, 1] -
465 src_pattern_array[0, 1])
466 src_delta_array[:, 2] = (src_pattern_array[1:, 2] -
467 src_pattern_array[0, 2])
468 src_dist_array = np.sqrt(src_delta_array[:, 0]**2 +
469 src_delta_array[:, 1]**2 +
470 src_delta_array[:, 2]**2)
479 for ref_dist_idx
in ref_dist_index_array:
483 tmp_ref_pair_list = self.
_id_array[ref_dist_idx]
484 for pair_idx, ref_id
in enumerate(tmp_ref_pair_list):
485 src_candidates = [0, 1]
487 shift_rot_matrix =
None 494 cos_shift = np.dot(src_pattern_array[0], ref_center)
495 if cos_shift < max_cos_theta_shift:
499 ref_candidates.append(ref_id)
502 ref_candidates.append(
503 tmp_ref_pair_list[1])
507 ref_candidates.append(
508 tmp_ref_pair_list[0])
518 src_pattern_array[0], ref_center, src_delta_array[0],
519 ref_delta, cos_shift, max_cos_rot_sq)
520 if test_rot_struct.cos_rot_sq
is None or \
521 test_rot_struct.proj_ref_ctr_delta
is None or \
522 test_rot_struct.shift_matrix
is None:
526 cos_rot_sq = test_rot_struct.cos_rot_sq
527 proj_ref_ctr_delta = test_rot_struct.proj_ref_ctr_delta
528 shift_matrix = test_rot_struct.shift_matrix
539 src_pattern_array[0], src_delta_array, src_dist_array,
541 tmp_ref_dist_array, tmp_ref_id_array, max_dist_rad,
546 if len(pattern_spoke_struct.ref_spoke_list) < n_match - 2
or \
547 len(pattern_spoke_struct.src_spoke_list) < n_match - 2:
551 ref_candidates.extend(pattern_spoke_struct.ref_spoke_list)
552 src_candidates.extend(pattern_spoke_struct.src_spoke_list)
558 cos_rot_sq, shift_matrix, src_delta_array[0],
562 if shift_rot_struct.sin_rot
is None or \
563 shift_rot_struct.shift_rot_matrix
is None:
567 sin_rot = shift_rot_struct.sin_rot
568 shift_rot_matrix = shift_rot_struct.shift_rot_matrix
577 src_pattern_array[src_candidates],
579 shift_rot_matrix, max_dist_rad)
581 if fit_shift_rot_matrix
is not None:
583 output_matched_pattern.ref_candidates = ref_candidates
584 output_matched_pattern.src_candidates = src_candidates
585 output_matched_pattern.shift_rot_matrix = \
587 output_matched_pattern.cos_shift = cos_shift
588 output_matched_pattern.sin_rot = sin_rot
589 return output_matched_pattern
591 return output_matched_pattern
593 def _find_candidate_reference_pairs(self, src_dist, ref_dist_array,
595 """Wrap numpy.searchsorted to find the range of reference spokes 596 within a spoke distance tolerance of our source spoke. 598 Returns an array sorted from the smallest absolute delta distance 599 between source and reference spoke length. This sorting increases the 600 speed for the pattern search greatly. 605 float value of the distance we would like to search for in 606 the reference array in radians. 607 ref_dist_array : `numpy.ndarray`, (N,) 608 sorted array of distances in radians. 609 max_dist_rad : `float` 610 maximum plus/minus search to find in the reference array in 615 tmp_diff_array : `numpy.ndarray`, (N,) 616 indices lookup into the input ref_dist_array sorted by the 617 difference in value to the src_dist from absolute value 622 start_idx = np.searchsorted(ref_dist_array, src_dist - max_dist_rad)
623 end_idx = np.searchsorted(ref_dist_array, src_dist + max_dist_rad,
627 if start_idx == end_idx:
633 if end_idx > ref_dist_array.shape[0]:
634 end_idx = ref_dist_array.shape[0]
639 tmp_diff_array = np.fabs(ref_dist_array[start_idx:end_idx] - src_dist)
640 return tmp_diff_array.argsort() + start_idx
642 def _test_rotation(self, src_center, ref_center, src_delta, ref_delta,
643 cos_shift, max_cos_rot_sq):
644 """ Test if the rotation implied between the source 645 pattern and reference pattern is within tolerance. To test this 646 we need to create the first part of our spherical rotation matrix 647 which we also return for use later. 651 src_center : `numpy.ndarray`, (N, 3) 653 ref_center : `numpy.ndarray`, (N, 3) 654 3 vector defining the center of the candidate reference pinwheel 656 src_delta : `numpy.ndarray`, (N, 3) 657 3 vector delta between the source pattern center and the end of 659 ref_delta : `numpy.ndarray`, (N, 3) 660 3 vector delta of the candidate matched reference pair 662 Cosine of the angle between the source and reference candidate 664 max_cos_rot_sq : `float` 665 candidate reference pair after shifting the centers on top of each 666 other. The function will return None if the rotation implied is 667 greater than max_cos_rot_sq. 671 result : `lsst.pipe.base.Struct` 672 Result struct with components: 674 - ``cos_rot_sq`` : magnitude of the rotation needed to align the 675 two patterns after their centers are shifted on top of each 676 other. `None` if rotation test fails (`float`). 677 - ``shift_matrix`` : 3x3 rotation matrix describing the shift needed to 678 align the source and candidate reference center. `None` if rotation 679 test fails (`numpy.ndarray`, (N, 3)). 685 elif cos_shift < -1.0:
687 sin_shift = np.sqrt(1 - cos_shift ** 2)
693 rot_axis = np.cross(src_center, ref_center)
694 rot_axis /= sin_shift
696 rot_axis, cos_shift, sin_shift)
698 shift_matrix = np.identity(3)
702 rot_src_delta = np.dot(shift_matrix, src_delta)
703 proj_src_delta = (rot_src_delta -
704 np.dot(rot_src_delta, ref_center) * ref_center)
705 proj_ref_delta = (ref_delta -
706 np.dot(ref_delta, ref_center) * ref_center)
707 cos_rot_sq = (np.dot(proj_src_delta, proj_ref_delta) ** 2 /
708 (np.dot(proj_src_delta, proj_src_delta) *
709 np.dot(proj_ref_delta, proj_ref_delta)))
711 if cos_rot_sq < max_cos_rot_sq:
712 return pipeBase.Struct(
714 proj_ref_ctr_delta=
None,
718 return pipeBase.Struct(
719 cos_rot_sq=cos_rot_sq,
720 proj_ref_ctr_delta=proj_ref_delta,
721 shift_matrix=shift_matrix,)
723 def _create_spherical_rotation_matrix(self, rot_axis, cos_rotation,
725 """Construct a generalized 3D rotation matrix about a given 730 rot_axis : `numpy.ndarray`, (3,) 731 3 vector defining the axis to rotate about. 732 cos_rotation : `float` 733 cosine of the rotation angle. 734 sin_rotation : `float` 735 sine of the rotation angle. 739 shift_matrix : `numpy.ndarray`, (3, 3) 740 3x3 spherical, rotation matrix. 743 rot_cross_matrix = np.array(
744 [[0., -rot_axis[2], rot_axis[1]],
745 [rot_axis[2], 0., -rot_axis[0]],
746 [-rot_axis[1], rot_axis[0], 0.]], dtype=np.float64)
747 shift_matrix = (cos_rotation*np.identity(3) +
748 sin_rotion*rot_cross_matrix +
749 (1. - cos_rotation)*np.outer(rot_axis, rot_axis))
753 def _create_pattern_spokes(self, src_ctr, src_delta_array, src_dist_array,
754 ref_ctr, ref_ctr_id, proj_ref_ctr_delta,
755 ref_dist_array, ref_id_array, max_dist_rad,
757 """ Create the individual spokes that make up the pattern now that the 758 shift and rotation are within tolerance. 760 If we can't create a valid pattern we exit early. 764 src_ctr : `numpy.ndarray`, (3,) 765 3 vector of the source pinwheel center 766 src_delta_array : `numpy.ndarray`, (N, 3) 767 Array of 3 vector deltas between the source center and the pairs 768 that make up the remaining spokes of the pinwheel 769 src_dist_array : `numpy.ndarray`, (N, 3) 770 Array of the distances of each src_delta in the pinwheel 771 ref_ctr : `numpy.ndarray`, (3,) 772 3 vector of the candidate reference center 774 id of the ref_ctr in the master reference array 775 proj_ref_ctr_delta : `numpy.ndarray`, (3,) 776 Plane projected 3 vector formed from the center point of the 777 candidate pin-wheel and the second point in the pattern to create 778 the first spoke pair. This is the candidate pair that was matched 779 in the main _construct_pattern_and_shift_rot_matrix loop 780 ref_dist_array : `numpy.ndarray`, (N,) 781 Array of vector distances for each of the reference pairs 782 ref_id_array : `numpy.ndarray`, (N,) 783 Array of id lookups into the master reference array that our 784 center id object is paired with. 785 max_dist_rad : `float` 786 Maximum search distance 788 Number of source deltas that must be matched into the reference 789 deltas in order to consider this a successful pattern match. 793 output_spokes : `lsst.pipe.base.Struct` 794 Result struct with components: 796 - ``ref_spoke_list`` : list of ints specifying ids into the master 797 reference array (`list` of `int`). 798 - ``src_spoke_list`` : list of ints specifying indices into the 799 current source pattern that is being tested (`list` of `int`). 802 output_spokes = pipeBase.Struct(
814 proj_src_ctr_delta = (src_delta_array[0] -
815 np.dot(src_delta_array[0], src_ctr) * src_ctr)
816 proj_src_ctr_dist_sq = np.dot(proj_src_ctr_delta, proj_src_ctr_delta)
819 proj_ref_ctr_dist_sq = np.dot(proj_ref_ctr_delta, proj_ref_ctr_delta)
822 for src_idx
in range(1, len(src_dist_array)):
823 if n_fail > len(src_dist_array) - (n_match - 1):
828 src_sin_tol = (max_dist_rad /
829 (src_dist_array[src_idx] + max_dist_rad))
836 if src_sin_tol > max_sin_tol:
837 src_sin_tol = max_sin_tol
842 src_delta_array[src_idx] -
843 np.dot(src_delta_array[src_idx], src_ctr) * src_ctr)
844 geom_dist_src = np.sqrt(
845 np.dot(proj_src_delta, proj_src_delta) *
846 proj_src_ctr_dist_sq)
849 cos_theta_src = (np.dot(proj_src_delta, proj_src_ctr_delta) /
851 cross_src = (np.cross(proj_src_delta, proj_src_ctr_delta) /
853 sin_theta_src = np.dot(cross_src, src_ctr)
858 src_dist_array[src_idx], ref_dist_array, max_dist_rad)
868 proj_ref_ctr_dist_sq,
878 ref_spoke_list.append(ref_id)
879 src_spoke_list.append(src_idx + 1)
883 if len(ref_spoke_list) >= n_match - 2:
885 output_spokes.ref_spoke_list = ref_spoke_list
886 output_spokes.src_spoke_list = src_spoke_list
891 def _test_spoke(self, cos_theta_src, sin_theta_src, ref_ctr, ref_ctr_id,
892 proj_ref_ctr_delta, proj_ref_ctr_dist_sq,
893 ref_dist_idx_array, ref_id_array, src_sin_tol):
894 """Test the opening angle between the first spoke of our pattern 895 for the source object against the reference object. 897 This method makes heavy use of the small angle approximation to perform 902 cos_theta_src : `float` 903 Cosine of the angle between the current candidate source spoke and 905 sin_theta_src : `float` 906 Sine of the angle between the current candidate source spoke and 908 ref_ctr : `numpy.ndarray`, (3,) 909 3 vector of the candidate reference center 911 id lookup of the ref_ctr into the master reference array 912 proj_ref_ctr_delta : `float` 913 Plane projected first spoke in the reference pattern using the 914 pattern center as normal. 915 proj_ref_ctr_dist_sq : `float` 916 Squared length of the projected vector. 917 ref_dist_idx_array : `numpy.ndarray`, (N,) 918 Indices sorted by the delta distance between the source 919 spoke we are trying to test and the candidate reference 921 ref_id_array : `numpy.ndarray`, (N,) 922 Array of id lookups into the master reference array that our 923 center id object is paired with. 924 src_sin_tol : `float` 925 Sine of tolerance allowed between source and reference spoke 931 If we can not find a candidate spoke we return `None` else we 932 return an int id into the master reference array. 936 for ref_dist_idx
in ref_dist_idx_array:
942 proj_ref_delta = ref_delta - np.dot(ref_delta, ref_ctr) * ref_ctr
943 geom_dist_ref = np.sqrt(proj_ref_ctr_dist_sq *
944 np.dot(proj_ref_delta, proj_ref_delta))
945 cos_theta_ref = (np.dot(proj_ref_delta, proj_ref_ctr_delta) /
950 if cos_theta_ref ** 2 < (1 - src_sin_tol ** 2):
951 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
952 (1 - cos_theta_ref ** 2))
954 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
959 if cos_sq_comparison > src_sin_tol ** 2:
965 cross_ref = (np.cross(proj_ref_delta, proj_ref_ctr_delta) /
967 sin_theta_ref = np.dot(cross_ref, ref_ctr)
971 if abs(cos_theta_src) < src_sin_tol:
972 sin_comparison = (sin_theta_src - sin_theta_ref) / src_sin_tol
975 (sin_theta_src - sin_theta_ref) / cos_theta_ref
977 if abs(sin_comparison) > src_sin_tol:
981 return ref_id_array[ref_dist_idx]
985 def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta,
987 """ Create the final part of our spherical rotation matrix. 992 cosine of the rotation needed to align our source and reference 994 shift_matrix : `numpy.ndarray`, (3, 3) 995 3x3 rotation matrix for shifting the source pattern center on top 996 of the candidate reference pattern center. 997 src_delta : `numpy.ndarray`, (3,) 998 3 vector delta of representing the first spoke of the source 1000 ref_ctr : `numpy.ndarray`, (3,) 1001 3 vector on the unit-sphere representing the center of our 1003 ref_delta : `numpy.ndarray`, (3,) 1004 3 vector delta made by the first pair of the reference pattern. 1008 result : `lsst.pipe.base.Struct` 1009 Result struct with components: 1011 - ``sin_rot`` : float sine of the amount of rotation between the 1012 source and reference pattern. We use sine here as it is 1013 signed and tells us the chirality of the rotation (`float`). 1014 - ``shift_rot_matrix`` : float array representing the 3x3 rotation 1015 matrix that takes the source pattern and shifts and rotates 1016 it to align with the reference pattern (`numpy.ndarray`, (3,3)). 1018 cos_rot = np.sqrt(cos_rot_sq)
1019 rot_src_delta = np.dot(shift_matrix, src_delta)
1020 delta_dot_cross = np.dot(np.cross(rot_src_delta, ref_delta), ref_ctr)
1022 sin_rot = np.sign(delta_dot_cross) * np.sqrt(1 - cos_rot_sq)
1024 ref_ctr, cos_rot, sin_rot)
1026 shift_rot_matrix = np.dot(rot_matrix, shift_matrix)
1028 return pipeBase.Struct(
1030 shift_rot_matrix=shift_rot_matrix,)
1032 def _intermediate_verify(self, src_pattern, ref_pattern, shift_rot_matrix,
1034 """ Perform an intermediate verify step. 1036 Rotate the matches references into the source frame and test their 1037 distances against tolerance. Only return true if all points are within 1042 src_pattern : `numpy.ndarray`, (N,3) 1043 Array of 3 vectors representing the points that make up our source 1045 ref_pattern : `numpy.ndarray`, (N,3) 1046 Array of 3 vectors representing our candidate reference pinwheel 1048 shift_rot_matrix : `numpy.ndarray`, (3,3) 1049 3x3 rotation matrix that takes the source objects and rotates them 1050 onto the frame of the reference objects 1051 max_dist_rad : `float` 1052 Maximum distance allowed to consider two objects the same. 1056 fit_shift_rot_matrix : `numpy.ndarray`, (3,3) 1057 Return the fitted shift/rotation matrix if all of the points in our 1058 source pattern are within max_dist_rad of their matched reference 1059 objects. Returns None if this criteria is not satisfied. 1061 if len(src_pattern) != len(ref_pattern):
1063 "Source pattern length does not match ref pattern.\n" 1064 "\t source pattern len=%i, reference pattern len=%i" %
1065 (len(src_pattern), len(ref_pattern)))
1068 src_pattern, ref_pattern, shift_rot_matrix, max_dist_rad):
1076 fit_shift_rot_matrix = least_squares(
1077 _rotation_matrix_chi_sq,
1078 x0=shift_rot_matrix.flatten(),
1079 args=(src_pattern, ref_pattern, max_dist_rad)
1083 src_pattern, ref_pattern, fit_shift_rot_matrix,
1085 return fit_shift_rot_matrix
1089 def _intermediate_verify_comparison(self, pattern_a, pattern_b,
1090 shift_rot_matrix, max_dist_rad):
1091 """Test the input rotation matrix against one input pattern and 1094 If every point in the pattern after rotation is within a distance of 1095 max_dist_rad to its candidate point in the other pattern, we return 1100 pattern_a : `numpy.ndarray`, (N,3) 1101 Array of 3 vectors representing the points that make up our source 1103 pattern_b : `numpy.ndarray`, (N,3) 1104 Array of 3 vectors representing our candidate reference pinwheel 1106 shift_rot_matrix : `numpy.ndarray`, (3,3) 1107 3x3 rotation matrix that takes the source objects and rotates them 1108 onto the frame of the reference objects 1109 max_dist_rad : `float` 1110 Maximum distance allowed to consider two objects the same. 1116 True if all rotated source points are within max_dist_rad of 1117 the candidate references matches. 1119 shifted_pattern_a = np.dot(shift_rot_matrix,
1120 pattern_a.transpose()).transpose()
1121 tmp_delta_array = shifted_pattern_a - pattern_b
1122 tmp_dist_array = (tmp_delta_array[:, 0] ** 2 +
1123 tmp_delta_array[:, 1] ** 2 +
1124 tmp_delta_array[:, 2] ** 2)
1125 return np.all(tmp_dist_array < max_dist_rad ** 2)
1127 def _test_pattern_lengths(self, test_pattern, max_dist_rad):
1128 """ Test that the all vectors in a pattern are unit length within 1131 This is useful for assuring the non unitary transforms do not contain 1132 too much distortion. 1136 test_pattern : `numpy.ndarray`, (N, 3) 1137 Test vectors at the maximum and minimum x, y, z extents. 1138 max_dist_rad : `float` 1139 maximum distance in radians to consider two points "agreeing" on 1147 dists = (test_pattern[:, 0] ** 2 +
1148 test_pattern[:, 1] ** 2 +
1149 test_pattern[:, 2] ** 2)
1151 np.logical_and((1 - max_dist_rad) ** 2 < dists,
1152 dists < (1 + max_dist_rad) ** 2))
1154 def _test_rotation_agreement(self, rot_vects, max_dist_rad):
1155 """ Test this rotation against the previous N found and return 1156 the number that a agree within tolerance to where our test 1161 rot_vects : `numpy.ndarray`, (N, 3) 1162 Arrays of rotated 3 vectors representing the maximum x, y, 1163 z extent on the unit sphere of the input source objects rotated by 1164 the candidate rotations into the reference frame. 1165 max_dist_rad : `float` 1166 maximum distance in radians to consider two points "agreeing" on 1172 Number of candidate rotations that agree for all of the rotated 1176 self.
log.debug(
"Comparing pattern %i to previous %i rotations..." %
1177 (rot_vects[-1][-1], len(rot_vects) - 1))
1180 for rot_idx
in range(max((len(rot_vects) - 1), 0)):
1182 for vect_idx
in range(len(rot_vects[rot_idx]) - 1):
1183 tmp_delta_vect = (rot_vects[rot_idx][vect_idx] -
1184 rot_vects[-1][vect_idx])
1185 tmp_dist_list.append(
1186 np.dot(tmp_delta_vect, tmp_delta_vect))
1187 if np.all(np.array(tmp_dist_list) < max_dist_rad ** 2):
1191 def _match_sources(self,
1194 """ Shift both the reference and source catalog to the the respective 1195 frames and find their nearest neighbor using a kdTree. 1197 Removes all matches who do not agree when either the reference or 1198 source catalog is rotated. Cuts on a maximum distance are left to an 1203 source_array : `numpy.ndarray`, (N, 3) 1204 array of 3 vectors representing the source objects we are trying 1205 to match into the source catalog. 1206 shift_rot_matrix : `numpy.ndarray`, (3, 3) 1207 3x3 rotation matrix that performs the spherical rotation from the 1208 source frame into the reference frame. 1212 results : `lsst.pipe.base.Struct` 1213 Result struct with components: 1215 - ``matches`` : array of integer ids into the source and 1216 reference arrays. Matches are only returned for those that 1217 satisfy the distance and handshake criteria 1218 (`numpy.ndarray`, (N, 2)). 1219 - ``distances`` : Distances between each match in radians after 1220 the shift and rotation is applied (`numpy.ndarray`, (N)). 1222 shifted_references = np.dot(
1223 np.linalg.inv(shift_rot_matrix),
1225 shifted_sources = np.dot(
1227 source_array.transpose()).transpose()
1229 ref_matches = np.empty((len(shifted_references), 2),
1231 src_matches = np.empty((len(shifted_sources), 2),
1234 ref_matches[:, 1] = np.arange(len(shifted_references),
1236 src_matches[:, 0] = np.arange(len(shifted_sources),
1240 src_kdtree = cKDTree(source_array)
1242 ref_to_src_dist, tmp_ref_to_src_idx = \
1243 src_kdtree.query(shifted_references)
1244 src_to_ref_dist, tmp_src_to_ref_idx = \
1245 ref_kdtree.query(shifted_sources)
1247 ref_matches[:, 0] = tmp_ref_to_src_idx
1248 src_matches[:, 1] = tmp_src_to_ref_idx
1251 return pipeBase.Struct(
1252 match_ids=src_matches[handshake_mask],
1253 distances_rad=src_to_ref_dist[handshake_mask],)
1255 def _handshake_match(self, matches_src, matches_ref):
1256 """Return only those matches where both the source 1257 and reference objects agree they they are each others' 1262 matches_src : `numpy.ndarray`, (N, 2) 1263 int array of nearest neighbor matches between shifted and 1264 rotated reference objects matched into the sources. 1265 matches_ref : `numpy.ndarray`, (N, 2) 1266 int array of nearest neighbor matches between shifted and 1267 rotated source objects matched into the references. 1270 handshake_mask_array : `numpy.ndarray`, (N,) 1271 Return the array positions where the two match catalogs agree. 1273 handshake_mask_array = np.zeros(len(matches_src), dtype=np.bool)
1275 for src_match_idx, match
in enumerate(matches_src):
1276 ref_match_idx = np.searchsorted(matches_ref[:, 1], match[1])
1277 if match[0] == matches_ref[ref_match_idx, 0]:
1278 handshake_mask_array[src_match_idx] =
True 1279 return handshake_mask_array
def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta, ref_ctr, ref_delta)
def _test_pattern_lengths(self, test_pattern, max_dist_rad)
def _test_spoke(self, cos_theta_src, sin_theta_src, ref_ctr, ref_ctr_id, proj_ref_ctr_delta, proj_ref_ctr_dist_sq, ref_dist_idx_array, ref_id_array, src_sin_tol)
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 _create_pattern_spokes(self, src_ctr, src_delta_array, src_dist_array, ref_ctr, ref_ctr_id, proj_ref_ctr_delta, ref_dist_array, ref_id_array, max_dist_rad, n_match)
def _test_rotation(self, src_center, ref_center, src_delta, ref_delta, cos_shift, max_cos_rot_sq)
def _intermediate_verify_comparison(self, pattern_a, pattern_b, shift_rot_matrix, max_dist_rad)
def _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 _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)