2 from __future__
import division, print_function, absolute_import
5 from scipy.optimize
import leastsq
6 from scipy.spatial
import cKDTree
8 import lsst.pipe.base
as pipeBase
11 def _rotation_matrix_chi_sq(flattened_rot_matrix, src_pattern, ref_pattern):
12 """Compute the squared differences for least squares fitting.
14 Given a flattened rotation matrix, a N point pattern from the source
15 catalog and the reference pattern the sources match to, compute
16 the squared differences between the points in the two patterns after
21 flattened_rot_matrix : float array
22 A flattened array representing a 3x3 rotation matrix. The array is
23 flattened to comply with the API of scipy.optimize.leastsq. Flattened
24 elements are [[0, 0], [0, 1], [0, 2], [1, 0]...]
25 src_patterm : float array of 3 vectors
26 A array containing N, 3 vectors
27 ref_pattern : float array of 3 vectors
28 A array containing N, 3 vectors
33 Array of differences between the vectors representing of the source
34 pattern rotated into the reference frame and the converse. This is
35 used to minimize in a least squares fitter.
38 rot_matrix = flattened_rot_matrix.reshape((3, 3))
40 diff_2_ref = np.dot(rot_matrix, src_pattern.transpose()).transpose() -\
46 diff_2_src = np.dot(rot_matrix.transpose(),
47 ref_pattern.transpose()).transpose() -\
50 return np.concatenate((diff_2_src.flatten(), diff_2_ref.flatten()))
54 """ Class implementing a pessimistic version of Optimsitic Pattern Matcher
55 B (OPMb) from Tabur 2007. The class loads and stores the reference object
56 in a convienent data structure for matching any set of source obejcts that
57 are assumed to contain each other. The pessimistic nature of the algorithm
58 comes from requiring that it discovers at least two patterns that agree on
59 the correct shift and rotation for matching before exiting. The original
60 behavior of OPMb can be recovered simply. Patterns matched between the
61 input datasets are n-spoked pinwheels created from n+1 points. Refer to
62 DMTN #031 for more details. http://github.com/lsst-dm/dmtn-031
63 --------------------------------------------------------------------------
65 reference_array : float array
66 spherical points x, y, z of to use as reference objects for
68 log : an lsst.log instance
69 pair_id_array : int array
70 Internal lookup table. Given an id in the reference array, return
71 an array of the id pair that contains this object's id sorted on
72 the distance to the pairs.
73 pair_delta_array : float array
74 Internal lookup table. Given an id in the reference array, return
75 an array of the 3 vector deltas of all other pairs sorted on their
77 pair_dist_array : float array
78 Internal lookup table. Given an id in the reference return an array
79 of pair distances of all other pairs sorted on distance.
80 dist_array : float array
81 Array of all pairs of objects in the reference array sorted on
84 Array of id pairs that lookup into the reference array sorted
86 delta_array : float array
87 Array of 3 vector deltas for each pair in the reference array
88 sorted on pair distance.
95 reference_array : float array
96 Array of spherical points x, y, z to use as reference objects.
98 logger object for reporting warnings and failures.
106 def _build_distances_and_angles(self):
107 """ Create the data structures we will use to search for our pattern
110 Throughout this function and the rest of the
112 class we use id to reference the position in the input reference
113 catalog and index to 'index' into the arrays sorted on distance.
138 sub_id_array_list = []
139 sub_delta_array_list = []
140 sub_dist_array_list = []
147 sub_id_array = np.zeros((self.
_n_reference - 1 - ref_id, 2),
149 sub_id_array[:, 0] = ref_id
150 sub_id_array[:, 1] = np.arange(ref_id + 1, self.
_n_reference,
157 sub_dist_array = np.sqrt(sub_delta_array[:, 0] ** 2 +
158 sub_delta_array[:, 1] ** 2 +
159 sub_delta_array[:, 2] ** 2)
163 sub_id_array_list.append(sub_id_array)
164 sub_delta_array_list.append(sub_delta_array)
165 sub_dist_array_list.append(sub_dist_array)
185 ref_id, sorted_pair_dist_args]
187 ref_id, sorted_pair_dist_args]
189 ref_id, sorted_pair_dist_args, :]
192 unsorted_id_array = np.concatenate(sub_id_array_list)
193 unsorted_delta_array = np.concatenate(sub_delta_array_list)
194 unsorted_dist_array = np.concatenate(sub_dist_array_list)
198 sorted_dist_args = unsorted_dist_array.argsort()
199 self.
_dist_array = unsorted_dist_array[sorted_dist_args]
200 self.
_id_array = unsorted_id_array[sorted_dist_args]
201 self.
_delta_array = unsorted_delta_array[sorted_dist_args]
205 def match(self, source_array, n_check, n_match, n_agree,
206 max_n_patterns, max_shift, max_rotation, max_dist,
207 min_matches, pattern_skip_array=
None):
208 """Match a given source catalog into the loaded reference catalog.
210 Given array of points on the unit sphere and tolerances, we
211 attempt to match a pinwheel like pattern between these input sources
212 and the reference objects this class was created with. This pattern
213 informs of the shift and rotation needed to align the input source
214 objects into the frame of the refernces.
218 source_array: float array
219 An array of spherical x,y,z coordinates and a magnitude in units
220 of objects having a lower value for sorting. The array should be
223 Number of sources to create a pattern from. Not all objects may be
224 checked if n_match criteria is before looping through all n_check
227 Number of objects to use in constructing a pattern to match.
229 Number of found patterns that must agree on their shift and
230 rotation before exiting. Set this value to 1 to recover the
231 expected behavior of Optimistic Pattern Matcher B.
232 max_n_patters : int value
233 Number of patterns to create from the input source objects to
234 attempt to match into the reference objects.
235 max_shift: float value
236 Maximum allowed shift to match patterns in arcseconds.
237 max_rotation: float value
238 Maximum allowed rotation between patterns in degrees.
239 max_dist: float value
240 Maximum distance in arcseconds allowed between candidate spokes in
241 the source and reference objects. Also sets that maximum distance
242 in the intermediate verify, pattern shift/rotation agreement, and
244 pattern_skip_array: int array
245 Patterns we would like to skip. This could be due to the pattern
246 being matched on a previous iteration that we now consider invalid.
247 This assumes the ordering of the source objects is the same
248 between different runs of the matcher which, assuming no object
249 has been inserted or the magnitudes have changed, it should be.
253 output_struct : pipe.base.struct
254 A pipebase struct containing the following outputs.
257 (N, 2) array of matched ids for pairs. Empty list if no
259 distances_rad : float array
260 Radian distances between the matched objects. Empty list
263 Index of matched pattern. None if no match found.
265 Magnitude for the shift between the source and
266 reference objects in arcseconds. None if no match found.
270 sorted_source_array = source_array[source_array[:, -1].argsort(), :3]
271 n_source = len(sorted_source_array)
274 output_match_struct = pipeBase.Struct(
281 self.log.warn(
"Source object array is empty. Unable to match. "
295 max_cos_shift = np.cos(np.radians(max_shift / 3600.))
296 max_cos_rot_sq = np.cos(np.radians(max_rotation)) ** 2
297 max_dist_rad = np.radians(max_dist / 3600.)
301 for pattern_idx
in range(np.min((max_n_patterns,
302 n_source - n_match))):
306 if pattern_skip_array
is not None and \
307 np.any(pattern_skip_array == pattern_idx):
309 "Skipping previously matched bad pattern %i..." %
313 pattern = sorted_source_array[
314 pattern_idx: np.min((pattern_idx + n_check, n_source)), :3]
319 construct_return_struct = \
321 pattern, n_match, max_cos_shift, max_cos_rot_sq,
325 if construct_return_struct.ref_candidates
is None or \
326 construct_return_struct.shift_rot_matrix
is None or\
327 construct_return_struct.cos_shift
is None or \
328 construct_return_struct.sin_rot
is None:
332 ref_candidates = construct_return_struct.ref_candidates
333 shift_rot_matrix = construct_return_struct.shift_rot_matrix
334 cos_shift = construct_return_struct.cos_shift
335 sin_rot = construct_return_struct.sin_rot
339 if len(ref_candidates) < n_match:
345 tmp_rot_vect_list = []
346 for test_vect
in test_vect_list:
347 tmp_rot_vect_list.append(np.dot(shift_rot_matrix, test_vect))
348 tmp_rot_vect_list.append(pattern_idx)
349 rot_vect_list.append(tmp_rot_vect_list)
359 source_array[:, :3], shift_rot_matrix)
362 if len(match_sources_struct.match_ids) >= min_matches:
364 shift = np.degrees(np.arccos(cos_shift)) * 3600.
366 self.log.debug(
"Succeeded after %i patterns." % pattern_idx)
367 self.log.debug(
"\tShift %.4f arcsec" % shift)
368 self.log.debug(
"\tRotation: %.4f deg" %
369 np.degrees(np.arcsin(sin_rot)))
372 output_match_struct.match_ids = \
373 match_sources_struct.match_ids
374 output_match_struct.distances_rad = \
375 match_sources_struct.distances_rad
376 output_match_struct.pattern_idx = pattern_idx
377 output_match_struct.shift = shift
378 return output_match_struct
380 self.log.warn(
"Failed after %i patterns." % (pattern_idx + 1))
381 return output_match_struct
383 def _compute_test_vectors(self, source_array):
384 """Compute spherical 3 vectors at the edges of the x, y, z extent
385 of the input source catalog.
389 source_array : float array (N, 3)
390 array of 3 vectors representing possitions on the unit
396 list of vectors representing the maxinum extents in x, y, z
397 of the input source array. These are used with the rotations
398 the code finds to test for agreement from different patterns
399 when the code is running in pessimistic mode.
403 if np.any(np.logical_not(np.isfinite(source_array))):
404 self.log.warn(
"Input source objects contain non-finite values. "
405 "This could end badly.")
406 center_vect = np.nanmean(source_array, axis=0)
410 xbtm_vect = np.array([np.min(source_array[:, 0]), center_vect[1],
411 center_vect[2]], dtype=np.float64)
412 xtop_vect = np.array([np.max(source_array[:, 0]), center_vect[1],
413 center_vect[2]], dtype=np.float64)
414 xbtm_vect /= np.sqrt(np.dot(xbtm_vect, xbtm_vect))
415 xtop_vect /= np.sqrt(np.dot(xtop_vect, xtop_vect))
417 ybtm_vect = np.array([center_vect[0], np.min(source_array[:, 1]),
418 center_vect[2]], dtype=np.float64)
419 ytop_vect = np.array([center_vect[0], np.max(source_array[:, 1]),
420 center_vect[2]], dtype=np.float64)
421 ybtm_vect /= np.sqrt(np.dot(ybtm_vect, ybtm_vect))
422 ytop_vect /= np.sqrt(np.dot(ytop_vect, ytop_vect))
424 zbtm_vect = np.array([center_vect[0], center_vect[1],
425 np.min(source_array[:, 2])], dtype=np.float64)
426 ztop_vect = np.array([center_vect[0], center_vect[1],
427 np.max(source_array[:, 2])], dtype=np.float64)
428 zbtm_vect /= np.sqrt(np.dot(zbtm_vect, zbtm_vect))
429 ztop_vect /= np.sqrt(np.dot(ztop_vect, ztop_vect))
432 return [xbtm_vect, xtop_vect, ybtm_vect, ytop_vect,
433 zbtm_vect, ztop_vect]
435 def _construct_pattern_and_shift_rot_matrix(self, src_pattern_array,
436 n_match, max_cos_theta_shift,
437 max_cos_rot_sq, max_dist_rad):
438 """Test an input source pattern against the reference catalog.
440 Returns the candidate matched patterns and their
441 implied rotation matrices or None.
445 src_pattern_array : float array
446 Sub selection of source 3 vectors to create a pattern from
448 Number of points to attempt to create a pattern from. Must be
449 >= len(src_pattern_array)
450 max_cos_theta_shift : float
451 Maximum shift allowed between two patterns' centers.
452 max_cos_rot_sq : float
453 Maximum rotation beteween two patterns that have been shifted
454 to have their centers on top of each other.
456 Maximum delta distance allowed between the source and reference
457 pair distances to consider the reference pair a candidate for
458 the source pair. Also sets the tolerance between the opening
459 angles of the spokes when compared to the reference.
463 lsst.pipe.base.Struct
464 Return a Struct containing the following data:
466 ref_candidates : list of ints
467 ids of the matched pattern in the internal reference_array
469 src_candidates : list of ints
470 Pattern ids of the sources matched.
471 shift_rot_matrix : float array
472 3x3 matrix specifying the full shift
473 and rotation between the reference and source objects. Rotates
474 source into reference frame. None if match is not found.
476 Magnitude of the shift found between the two patten
477 centers. None if match is not found.
478 sin_rot : float value of the rotation to align the already shifted
479 source pattern to the reference pattern. None if no match
487 output_matched_pattern = pipeBase.Struct(
490 shift_rot_matrix=
None,
496 src_delta_array = np.empty((len(src_pattern_array) - 1, 3))
497 src_delta_array[:, 0] = (src_pattern_array[1:, 0] -
498 src_pattern_array[0, 0])
499 src_delta_array[:, 1] = (src_pattern_array[1:, 1] -
500 src_pattern_array[0, 1])
501 src_delta_array[:, 2] = (src_pattern_array[1:, 2] -
502 src_pattern_array[0, 2])
503 src_dist_array = np.sqrt(src_delta_array[:, 0]**2 +
504 src_delta_array[:, 1]**2 +
505 src_delta_array[:, 2]**2)
514 for ref_dist_idx
in ref_dist_index_array:
518 tmp_ref_pair_list = self.
_id_array[ref_dist_idx]
519 for pair_idx, ref_id
in enumerate(tmp_ref_pair_list):
520 src_candidates = [0, 1]
522 shift_rot_matrix =
None
529 cos_shift = np.dot(src_pattern_array[0], ref_center)
530 if cos_shift < max_cos_theta_shift:
534 ref_candidates.append(ref_id)
540 ref_candidates.append(
541 tmp_ref_pair_list[1])
543 ref_candidates.append(
544 tmp_ref_pair_list[0])
553 src_pattern_array[0], ref_center, src_delta_array[0],
554 ref_delta, cos_shift, max_cos_rot_sq)
555 if test_rot_struct.cos_rot_sq
is None or \
556 test_rot_struct.shift_matrix
is None:
560 cos_rot_sq = test_rot_struct.cos_rot_sq
561 shift_matrix = test_rot_struct.shift_matrix
574 src_pattern_array[0], src_delta_array, src_dist_array,
576 ref_dist, tmp_ref_delta_array, tmp_ref_dist_arary,
577 tmp_ref_id_array, max_dist_rad,
582 if len(pattern_spoke_struct.ref_spoke_list) < n_match - 2
or \
583 len(pattern_spoke_struct.src_spoke_list) < n_match - 2:
587 ref_candidates.extend(pattern_spoke_struct.ref_spoke_list)
588 src_candidates.extend(pattern_spoke_struct.src_spoke_list)
594 cos_rot_sq, shift_matrix, src_delta_array[0],
598 if shift_rot_struct.sin_rot
is None or \
599 shift_rot_struct.shift_rot_matrix
is None:
603 sin_rot = shift_rot_struct.sin_rot
604 shift_rot_matrix = shift_rot_struct.shift_rot_matrix
611 src_pattern_array[src_candidates],
613 shift_rot_matrix, max_dist_rad)
614 if fit_shift_rot_matrix
is not None:
616 output_matched_pattern.ref_candidates = ref_candidates
617 output_matched_pattern.src_candidates = src_candidates
618 output_matched_pattern.shift_rot_matrix = \
620 output_matched_pattern.cos_shift = cos_shift
621 output_matched_pattern.sin_rot = sin_rot
622 return output_matched_pattern
624 return output_matched_pattern
626 def _find_candidate_reference_pairs(self, src_dist, ref_dist_array,
628 """Wrap numpy.searchsorted to find the range of reference spokes
629 within a spoke distance tolerance of our source spoke.
631 Returns an array sorted from the smallest absolute delta distance
632 between source and reference spoke length. This sorting increases the
633 speed for the pattern search greatly.
637 src_dist : float radians
638 float value of the distance we would like to search for in
639 the reference array in radians.
640 ref_dist_array : float array
641 sorted array of distances in radians.
643 maximum plus/minus search to find in the reference array in
649 indices lookup into the input ref_dist_array sorted by the
650 difference in value to the src_dist from absolute value
655 start_idx = np.searchsorted(ref_dist_array, src_dist - max_dist_rad)
656 end_idx = np.searchsorted(ref_dist_array, src_dist + max_dist_rad,
660 if start_idx == end_idx:
666 if end_idx > ref_dist_array.shape[0]:
667 end_idx = ref_dist_array.shape[0]
672 tmp_diff_array = np.fabs(ref_dist_array[start_idx:end_idx] - src_dist)
673 return tmp_diff_array.argsort() + start_idx
675 def _test_rotation(self, src_center, ref_center, src_delta, ref_delta,
676 cos_shift, max_cos_rot_sq):
677 """ Test if the rotation implied between the source
678 pattern and reference pattern is within tolerance. To test this
679 we need to create the first part of our spherical rotation matrix
680 which we also return for use later.
684 src_center : float array3
686 ref_center : float array
687 3 vector defining the center of the candidate refence pinwheel
689 src_delta : float array
690 3 vector delta between the source pattern center and the end of
692 ref_delta : float array
693 3 vector delta of the candidate matched reference pair
695 Cosine of the angle between the source and reference candidate
697 max_cos_rot_sq : float
698 candidate reference pair after shifting the centers on top of each
699 other. The function will return None if the rotation implied is
700 greater than max_cos_rot_sq.
704 lsst.pipe.base.Struct
705 Return a pipe.base.Struct containing the following data.
708 magnitude of the rotation needed to align the two patterns
709 after their centers are shifted on top of each other.
710 None if rotation test fails.
711 shift_matrix : float array
712 3x3 rotation matrix describing the shift needed to align
713 the source and candidate reference center.
714 None if rotation test fails.
720 elif cos_shift < -1.0:
722 sin_shift = np.sqrt(1 - cos_shift ** 2)
728 rot_axis = np.cross(src_center, ref_center)
729 rot_axis /= sin_shift
731 rot_axis, cos_shift, sin_shift)
733 shift_matrix = np.identity(3)
737 rot_src_delta = np.dot(shift_matrix, src_delta)
738 cos_rot_sq = (np.dot(rot_src_delta, ref_delta)**2 /
739 (np.dot(rot_src_delta, rot_src_delta) *
740 np.dot(ref_delta, ref_delta)))
742 if cos_rot_sq < max_cos_rot_sq:
743 return pipeBase.Struct(
746 return pipeBase.Struct(
747 cos_rot_sq=cos_rot_sq,
748 shift_matrix=shift_matrix,)
750 def _create_spherical_rotation_matrix(self, rot_axis, cos_rotation,
752 """Construct a generalized 3D rotation matrix about a given
757 rot_axis : float array
758 3 vector defining the axis to rotate about.
760 cosine of the rotation angle.
762 sine of the rotation angle.
767 3x3 spherical, rotation matrix.
770 rot_cross_matrix = np.array(
771 [[0., -rot_axis[2], rot_axis[1]],
772 [rot_axis[2], 0., -rot_axis[0]],
773 [-rot_axis[1], rot_axis[0], 0.]], dtype=np.float64)
774 shift_matrix = (cos_rotation*np.identity(3) +
775 sin_rotion*rot_cross_matrix +
776 (1. - cos_rotation)*np.outer(rot_axis, rot_axis))
780 def _create_pattern_spokes(self, src_ctr, src_delta_array, src_dist_array,
781 ref_ctr, ref_ctr_id, ref_delta, ref_dist,
782 ref_delta_array, ref_dist_array,
783 ref_id_array, max_dist_rad, n_match):
784 """ Create the individual spokes that make up the pattern now that the
785 shift and rotation are within tolerance.
787 If we can't create a valid pattern we exit early.
791 src_ctr : float array
792 3 vector of the source pinwheel center
793 src_delta_array : float array
794 Array of 3 vector deltas between the source center and the pairs
795 that make up the remaining spokes of the pinwheel
796 src_dist_array : float array
797 Array of the distances of each src_delta in the pinwheel
798 ref_ctr : float array
799 3 vector of the candidate refenerce center
801 id of the ref_ctr in the master reference array
802 ref_delta : float array
803 3 vector of the first candidate pair of the pinwheel. This is
804 the candidate pair that was matched in the
805 main _construct_pattern_and_shift_rot_matrix loop
807 Radian distance of the first candidate reference pair
808 ref_delta_array : float array
809 Array of 3 vector deltas that are have the current candidate
810 reference center as part of the pair
811 ref_dist_array : float array
812 Array of vector distances for each of the reference pairs
813 ref_id_array : int array
814 Array of id lookups into the master reference array that our
815 center id object is paired with.
817 Maximum search distance
819 Number of source deltas that must be matched into the reference
820 deltas in order to consider this a successful pattern match.
824 lsst.pipe.base.Struct
825 The Struct contains the following data:
827 ref_spoke_list : list of ints specifying ids into the master
829 src_spoke_list : list of ints specifying indices into the current
830 source pattern that is being tested.
833 output_spokes = pipeBase.Struct(
843 for src_idx
in range(1, len(src_dist_array)):
844 if n_fail > len(src_dist_array) - (n_match - 1):
849 src_sin_tol = (max_dist_rad /
850 (src_dist_array[src_idx] + max_dist_rad))
855 if src_sin_tol > 0.0447:
862 src_dist_array[src_idx], ref_dist_array, max_dist_rad)
867 src_ctr, src_delta_array[src_idx], src_dist_array[src_idx],
868 src_delta_array[0], src_dist_array[0], ref_ctr, ref_ctr_id,
869 ref_delta, ref_dist, ref_dist_idx_array, ref_delta_array,
871 ref_id_array, src_sin_tol)
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, src_ctr, src_delta, src_dist, src_ctr_delta,
892 src_ctr_dist, ref_ctr, ref_ctr_id, ref_delta, ref_dist,
893 ref_dist_idx_array, ref_delta_array, ref_dist_array,
894 ref_id_array, src_sin_tol):
895 """Test the opening angle between the first spoke of our pattern
896 for the soruce object against the reference object.
898 This method makes heavy use of the small angle approximation to perform
903 src_ctr : float array
904 3 vector of the source pinwheel center
905 src_delta : float array
906 3 vector delta from the source center and the source object that
907 makes up the current spoke of the pinwheel we are testing.
908 src_dist : float array
909 Distance of the current spoke we are testing
910 src_ctr_delta : float array
911 3 vector delta between the center of the pattern and the first
912 spoke of the pattern. Used to test compute the opening angle
913 between the current spoke and the first spoke.
915 Distance between the pairs that make up src_ctr_delta
916 ref_ctr : float array
917 3 vector of the candidate reference center
919 id lookup of the ref_ctr into the master reference array
920 ref_delta : float array
921 3 vector of the first candidate pair of the pinwheel. That is
922 the candidate pair that was matched in the
923 main _construct_pattern_and_shift_rot_matrix loop
925 Radian distance of the first candidate reference pair
926 ref_dist_idx_array : int array
927 Indices sorted by the delta distance between the source
928 spoke we are trying to test and the candidate reference
930 ref_delta_array : float array
931 Array of 3 vector deltas that are have the current candidate
932 reference center as part of the pair
933 ref_dist_array : float array
934 Array of vector distances for each of the reference pairs
935 ref_id_array : int array
936 Array of id lookups into the master reference array that our
937 center id object is paired with.
939 Sine of tolerance allowed between source and reference spoke
945 If we can not find a candidate spoke we return None else we
946 return an int id into the master reference array.
951 cos_theta_src = (np.dot(src_delta, src_ctr_delta) /
952 (src_dist * src_ctr_dist))
953 cross_src = (np.cross(src_delta, src_ctr_delta) /
954 (src_dist * src_ctr_dist))
955 dot_cross_src = np.dot(cross_src, src_ctr)
958 for ref_dist_idx
in ref_dist_idx_array:
961 if ref_id_array[ref_dist_idx] < ref_ctr_id:
966 cos_theta_ref = ref_sign * (
967 np.dot(ref_delta_array[ref_dist_idx], ref_delta) /
968 (ref_dist_array[ref_dist_idx] * ref_dist))
972 if cos_theta_ref ** 2 < (1 - src_sin_tol ** 2):
973 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
974 (1 - cos_theta_ref ** 2))
976 cos_sq_comparison = ((cos_theta_src - cos_theta_ref) ** 2 /
981 if cos_sq_comparison > src_sin_tol ** 2:
987 cross_ref = ref_sign * (
988 np.cross(ref_delta_array[ref_dist_idx], ref_delta) /
989 (ref_dist_array[ref_dist_idx] * ref_dist))
990 dot_cross_ref = np.dot(cross_ref, ref_ctr)
994 if abs(cos_theta_src) < src_sin_tol:
995 sin_comparison = (dot_cross_src - dot_cross_ref) / src_sin_tol
998 (dot_cross_src - dot_cross_ref) / cos_theta_ref
1000 if abs(sin_comparison) > src_sin_tol:
1004 return ref_id_array[ref_dist_idx]
1008 def _create_shift_rot_matrix(self, cos_rot_sq, shift_matrix, src_delta,
1009 ref_ctr, ref_delta):
1010 """ Create the final part of our spherical rotation matrix.
1015 cosine of the rotation needed to align our source and reference
1017 shift_matrix : float array
1018 3x3 rotation matrix for shifting the source pattern center on top
1019 of the candidate reference pattern center.
1020 src_delta : float array
1021 3 vector delta of representing the first spoke of the source
1023 ref_ctr : float array
1024 3 vector on the unitsphere representing the center of our
1026 ref_delta : float array
1027 3 vector delta made by the first pair of the reference pattern.
1031 lsst.pipe.base.Struct
1032 Struct object containing the following data:
1034 sin_rot : float sine of the amount of rotation between the
1035 source and reference pattern. We use sine here as it is
1036 signed and tells us the chirality of the rotation.
1037 shift_rot_matrix : float array representing the 3x3 rotation
1038 matrix that takes the source patern and shifts and rotates
1039 it to align with the reference pattern.
1041 cos_rot = np.sqrt(cos_rot_sq)
1042 rot_src_delta = np.dot(shift_matrix, src_delta)
1043 delta_dot_cross = np.dot(np.cross(rot_src_delta, ref_delta), ref_ctr)
1045 sin_rot = np.sign(delta_dot_cross) * np.sqrt(1 - cos_rot_sq)
1047 ref_ctr, cos_rot, sin_rot)
1049 shift_rot_matrix = np.dot(rot_matrix, shift_matrix)
1051 return pipeBase.Struct(
1053 shift_rot_matrix=shift_rot_matrix,)
1055 def _intermediate_verify(self, src_pattern, ref_pattern, shift_rot_matrix,
1057 """ Perform an intermediate verify step. Rotate the matches references
1058 into the source frame and test their distances against tolerance. Only
1059 return true if all points are within tolerance.
1063 src_pattern : float array
1064 Array of 3 vectors representing the points that make up our source
1066 ref_pattern : float array
1067 Array of 3 vectors representing our candidate reference pinwheel
1069 shift_rot_matrix : float array
1070 3x3 rotation matrix that takes the source objects and rotates them
1071 onto the frame of the reference objects
1072 max_dist_rad : float
1073 Maximum distance allowed to consider two objects the same.
1078 Return the fitted shift/rotation matrix if all of the points in our
1079 source pattern are within max_dist_rad of their matched reference
1080 objects. Returns None if this criteria is not satisfied.
1082 if len(src_pattern) != len(ref_pattern):
1084 "Source pattern length does not match ref pattern.\n"
1085 "\t source pattern len=%i, reference pattern len=%i" %
1086 (len(src_pattern), len(ref_pattern)))
1089 src_pattern, ref_pattern, shift_rot_matrix, max_dist_rad):
1097 fit_shift_rot_matrix = leastsq(
1098 _rotation_matrix_chi_sq, x0=shift_rot_matrix.flatten(),
1099 args=(src_pattern, ref_pattern,))[0].reshape((3, 3))
1102 src_pattern, ref_pattern, fit_shift_rot_matrix,
1104 return fit_shift_rot_matrix
1108 def _intermediate_verify_comparison(self, src_pattern, ref_pattern,
1109 shift_rot_matrix, max_dist_rad):
1110 """Test the input rotation matrix against the input source and
1113 If every point in the source patern after rotation is within a
1114 distance of max_dist_rad to its candidate reference point, we
1119 src_pattern : float array
1120 Array of 3 vectors representing the points that make up our source
1122 ref_pattern : float array
1123 Array of 3 vectors representing our candidate reference pinwheel
1125 shift_rot_matrix : float array
1126 3x3 rotation matrix that takes the source objects and rotates them
1127 onto the frame of the reference objects
1128 max_dist_rad : float
1129 Maximum distance allowed to consider two objects the same.
1135 True if all rotated source points are within max_dist_rad of
1136 the candidate references matches.
1138 shifted_ref_pattern = np.dot(shift_rot_matrix.transpose(),
1139 ref_pattern.transpose()).transpose()
1140 tmp_delta_array = src_pattern - shifted_ref_pattern
1141 tmp_dist_array = (tmp_delta_array[:, 0] ** 2 +
1142 tmp_delta_array[:, 1] ** 2 +
1143 tmp_delta_array[:, 2] ** 2)
1144 return np.all(tmp_dist_array < max_dist_rad ** 2)
1146 def _test_rotation_agreement(self, rot_vect_list, max_dist_rad):
1147 """ Test this rotation against the previous N found and return
1148 the number that a agree within tolerance to where our test
1153 rot_vect_list : list of lists of float arrays
1154 list of lists of rotated 3 vectors representing the maximum x, y,
1155 z extent on the unit sphere of the input source objects roated by
1156 the candidate rotations into the reference frame.
1157 max_dist_rad : float
1158 maximum distance in radians to consider two points "agreeing" on
1164 Number of candidate rotations that agree for all of the rotated
1168 self.log.debug(
"Comparing pattern %i to previous %i rotations..." %
1169 (rot_vect_list[-1][-1], len(rot_vect_list) - 1))
1172 for rot_idx
in range(max((len(rot_vect_list) - 1), 0)):
1174 for vect_idx
in range(len(rot_vect_list[rot_idx]) - 1):
1175 tmp_delta_vect = (rot_vect_list[rot_idx][vect_idx] -
1176 rot_vect_list[-1][vect_idx])
1177 tmp_dist_list.append(
1178 np.dot(tmp_delta_vect, tmp_delta_vect))
1179 if np.all(np.array(tmp_dist_list) < max_dist_rad ** 2):
1183 def _match_sources(self, source_array, shift_rot_matrix):
1184 """ Shift both the reference and source catalog to the the respective
1185 frames and find their nearest neighbor using a kdTree. Removes all
1186 matches who do not agree when either the refernce or source catalog is
1187 rotated and removes all matches greated than the requested distance.
1191 source_array : float array
1192 array of 3 vectors representing the source objects we are trying
1193 to match into the source catalog.
1194 shift_rot_matrix : float array
1195 3x3 rotation matrix that performs the spherical rotation from the
1196 source frame into the reference frame.
1200 lsst.pipe.base.Struct
1201 A Struct object containing the following data
1202 matches : a (N, 2) array of integer ids into the source and
1203 reference arrays. Matches are only returned for those that
1204 satisfy the distance and handshake criteria.
1205 distances : float array of the distance between each match in
1206 radians after the shift and rotation is applied.
1208 shifted_references = np.dot(
1209 shift_rot_matrix.transpose(),
1210 self._reference_array.transpose()).transpose()
1211 shifted_sources = np.dot(
1213 source_array.transpose()).transpose()
1215 ref_matches = np.empty((len(shifted_references), 2),
1217 src_matches = np.empty((len(shifted_sources), 2),
1220 ref_matches[:, 1] = np.arange(len(shifted_references),
1222 src_matches[:, 0] = np.arange(len(shifted_sources),
1226 src_kdtree = cKDTree(source_array)
1228 tmp_src_dist, tmp_src_idx = src_kdtree.query(shifted_references)
1229 tmp_ref_dist, tmp_ref_idx = ref_kdtree.query(shifted_sources)
1231 ref_matches[:, 0] = tmp_src_idx
1232 src_matches[:, 1] = tmp_ref_idx
1235 return pipeBase.Struct(
1236 match_ids=ref_matches[handshake_mask],
1237 distances_rad=tmp_src_dist[handshake_mask],)
1239 def _handshake_match(self, matches_ref, matches_src):
1240 """Return only those matches where both the source
1241 and reference objects agree they they are each others'
1246 matches_ref : int array
1247 (N, 2) int array of nearest neighbor matches between shifted and
1248 rotated source objects matched into the references.
1249 matches_src : int array
1250 (M, 2) int array of nearest neighbor matches between shifted and
1251 rotated reference objects matched into the sources.
1256 Return the array positions where the two match catalogs agree.
1258 handshake_mask_array = np.zeros(len(matches_ref), dtype=np.bool)
1260 for ref_match_idx, match
in enumerate(matches_ref):
1261 src_match_idx = np.searchsorted(matches_src[:, 0], match[0])
1262 if match[1] == matches_src[src_match_idx, 1]:
1263 handshake_mask_array[ref_match_idx] =
True
1264 return handshake_mask_array
def _test_rotation_agreement
def _create_shift_rot_matrix
def _create_pattern_spokes
def _compute_test_vectors
def _create_spherical_rotation_matrix
def _intermediate_verify_comparison
def _find_candidate_reference_pairs
def _build_distances_and_angles
def _construct_pattern_and_shift_rot_matrix