def test_get_rotation_matrix_between_vectors(from_v1, from_v2, to_v1, to_v2, expected_rotation): rotation_matrix = get_rotation_matrix_between_vectors( np.array(from_v1), np.array(from_v2), np.array([to_v1]), np.array([to_v2])) np.testing.assert_allclose(rotation_matrix, np.array([expected_rotation]), atol=1e-15)
def match_vectors(peaks, library, mag_tol, angle_tol, index_error_tol, n_peaks_to_index, n_best): # TODO: Sort peaks by intensity or SNR """Assigns hkl indices to pairs of diffraction vectors. Parameters ---------- peaks : np.array() The experimentally measured diffraction vectors, associated with a particular probe position, to be indexed. In Cartesian coordinates. library : VectorLibrary Library of reciprocal space vectors to be matched to the vectors. mag_tol : float Max allowed magnitude difference when comparing vectors. angle_tol : float Max allowed angle difference in radians when comparing vector pairs. index_error_tol : float Max allowed error in peak indexation for classifying it as indexed, calculated as :math:`|hkl_calculated - round(hkl_calculated)|`. n_peaks_to_index : int The maximum number of peak to index. n_best : int The maximum number of good solutions to be retained for each phase. Returns ------- indexation : np.array() A numpy array containing the indexation results, each result consisting of 5 entries: [phase index, rotation matrix, match rate, error hkls, total error] """ if peaks.shape == (1, ) and peaks.dtype == np.object: peaks = peaks[0] # Assign empty array to hold indexation results. The n_best best results # from each phase is returned. top_matches = np.empty(len(library) * n_best, dtype="object") res_rhkls = [] # Iterate over phases in DiffractionVectorLibrary and perform indexation # on each phase, storing the best results in top_matches. for phase_index, (phase, structure) in enumerate( zip(library.values(), library.structures)): solutions = [] lattice_recip = structure.lattice.reciprocal() phase_indices = phase["indices"] phase_measurements = phase["measurements"] if peaks.shape[0] < 2: # pragma: no cover continue # Choose up to n_peaks_to_index unindexed peaks to be paired in all # combinations. # TODO: Matching can be done iteratively where successfully indexed # peaks are removed after each iteration. This can possibly # handle overlapping patterns. # unindexed_peak_ids = range(min(peaks.shape[0], n_peaks_to_index)) # TODO: Better choice of peaks (longest, highest SNR?) # TODO: Inline after choosing the best, and possibly require external sorting (if using sorted)? unindexed_peak_ids = _choose_peak_ids(peaks, n_peaks_to_index) # Find possible solutions for each pair of peaks. for vector_pair_index, peak_pair_indices in enumerate( list(combinations(unindexed_peak_ids, 2))): # Consider a pair of experimental scattering vectors. q1, q2 = peaks[peak_pair_indices, :] q1_len, q2_len = np.linalg.norm(q1), np.linalg.norm(q2) # Ensure q1 is longer than q2 for consistent order. if q1_len < q2_len: q1, q2 = q2, q1 q1_len, q2_len = q2_len, q1_len # Calculate the angle between experimental scattering vectors. angle = get_angle_cartesian(q1, q2) # Get library indices for hkls matching peaks within tolerances. # TODO: phase are object arrays. Test performance of direct float arrays tolerance_mask = np.abs(phase_measurements[:, 0] - q1_len) < mag_tol tolerance_mask[tolerance_mask] &= ( np.abs(phase_measurements[tolerance_mask, 1] - q2_len) < mag_tol) tolerance_mask[tolerance_mask] &= ( np.abs(phase_measurements[tolerance_mask, 2] - angle) < angle_tol) # Iterate over matched library vectors determining the error in the # associated indexation. if np.count_nonzero(tolerance_mask) == 0: continue # Reference vectors are cartesian coordinates of hkls reference_vectors = lattice_recip.cartesian( phase_indices[tolerance_mask]) # Rotation from experimental to reference frame rotations = get_rotation_matrix_between_vectors( q1, q2, reference_vectors[:, 0], reference_vectors[:, 1]) # Index the peaks by rotating them to the reference coordinate # system. Use rotation directly since it is multiplied from the # right. Einsum gives list of peaks.dot(rotation). hklss = lattice_recip.fractional( np.einsum("ijk,lk->ilj", rotations, peaks)) # Evaluate error of peak hkl indexation rhklss = np.rint(hklss) ehklss = np.abs(hklss - rhklss) valid_peak_mask = np.max(ehklss, axis=-1) < index_error_tol valid_peak_counts = np.count_nonzero(valid_peak_mask, axis=-1) error_means = ehklss.mean(axis=(1, 2)) num_peaks = len(peaks) match_rates = (valid_peak_counts * (1 / num_peaks)) if num_peaks else 0 possible_solution_mask = match_rates > 0 solutions += [ OrientationResult( phase_index=phase_index, rotation_matrix=R, match_rate=match_rate, error_hkls=ehkls, total_error=error_mean, scale=1.0, center_x=0.0, center_y=0.0, ) for R, match_rate, ehkls, error_mean in zip( rotations[possible_solution_mask], match_rates[possible_solution_mask], ehklss[possible_solution_mask], error_means[possible_solution_mask], ) ] res_rhkls += rhklss[possible_solution_mask].tolist() n_solutions = min(n_best, len(solutions)) i = phase_index * n_best # starting index in unfolded array if n_solutions > 0: top_n = sorted(solutions, key=attrgetter("match_rate"), reverse=True)[:n_solutions] # Put the top n ranked solutions in the output array top_matches[i:i + n_solutions] = top_n if n_solutions < n_best: # Fill with dummy values top_matches[i + n_solutions:i + n_best] = [ OrientationResult( phase_index=0, rotation_matrix=np.identity(3), match_rate=0.0, error_hkls=np.array([]), total_error=1.0, scale=1.0, center_x=0.0, center_y=0.0, ) for x in range(n_best - n_solutions) ] # Because of a bug in numpy (https://github.com/numpy/numpy/issues/7453), # triggered by the way HyperSpy reads results (np.asarray(res), which fails # when the two tuple values have the same first dimension), we cannot # return a tuple directly, but instead have to format the result as an # array ourselves. res = np.empty(2, dtype=np.object) res[0] = top_matches res[1] = np.asarray(res_rhkls) return res
def test_get_rotation_matrix_between_vectors(k1, k2, ref_k1, ref_k2, expected_rotation): rotation_matrix = get_rotation_matrix_between_vectors( k1, k2, ref_k1, ref_k2) assert np.allclose(rotation_matrix, expected_rotation)
def match_vectors(peaks, library, mag_tol, angle_tol, index_error_tol, n_peaks_to_index, n_best, keys=[], *args, **kwargs): """Assigns hkl indices to pairs of diffraction vectors. Parameters ---------- peaks : np.array() The experimentally measured diffraction vectors, associated with a particular probe position, to be indexed. In Cartesian coordinates. library : VectorLibrary Library of reciprocal space vectors to be matched to the vectors. mag_tol : float Max allowed magnitude difference when comparing vectors. angle_tol : float Max allowed angle difference in radians when comparing vector pairs. index_error_tol : float Max allowed error in peak indexation for classifying it as indexed, calculated as |hkl_calculated - round(hkl_calculated)|. n_peaks_to_index : int The maximum number of peak to index. n_best : int The maximum number of good solutions to be retained. Returns ------- indexation : np.array() A numpy array containing the indexation results, each result consisting of 5 entries: [phase index, rotation matrix, match rate, error hkls, total error] """ if peaks.shape == (1, ) and peaks.dtype == 'object': peaks = peaks[0] # Initialise for loop with first entry & assign empty array to hold # indexation results. top_matches = np.empty((len(library), n_best, 5), dtype='object') res_rhkls = [] # TODO: Sort these by intensity or SNR # Iterate over phases in DiffractionVectorLibrary and perform indexation # with respect to each phase. for phase_index, (phase_name, structure) in enumerate( zip(library.keys(), library.structures)): solutions = [] lattice_recip = structure.lattice.reciprocal() # Choose up to n_peaks_to_index unindexed peaks to be paired in all # combinations unindexed_peak_ids = range(min(peaks.shape[0], n_peaks_to_index)) # Determine overall indexations associated with each peak pair for peak_pair_indices in combinations(unindexed_peak_ids, 2): # Consider a pair of experimental scattering vectors. q1, q2 = peaks[peak_pair_indices, :] q1_len, q2_len = np.linalg.norm(q1), np.linalg.norm(q2) # Ensure q1 is longer than q2 so combinations in correct order. if q1_len < q2_len: q1, q2 = q2, q1 q1_len, q2_len = q2_len, q1_len # Calculate the angle between experimental scattering vectors. angle = get_angle_cartesian(q1, q2) # Get library indices for hkls matching peaks within tolerances. # TODO: Library[key] are object arrays. Test performance of direct float arrays # TODO: Test performance with short circuiting (np.where for each step) match_ids = np.where( (np.abs(q1_len - library[phase_name][:, 2]) < mag_tol) & (np.abs(q2_len - library[phase_name][:, 3]) < mag_tol) & (np.abs(angle - library[phase_name][:, 4]) < angle_tol))[0] # Iterate over matched library vectors determining the error in the # associated indexation and finding the minimum error cases. peak_pair_solutions = [] for i, match_id in enumerate(match_ids): hkl1, hkl2 = library[phase_name][:, :2][match_id] # Reference vectors are cartesian coordinates of hkls ref_q1, ref_q2 = lattice_recip.cartesian( hkl1), lattice_recip.cartesian(hkl2) # Rotation from ref to experimental R = get_rotation_matrix_between_vectors(q1, q2, ref_q1, ref_q2) # Index the peaks by rotating them to the reference coordinate # system. R is used directly since it is multiplied from the # right. cartesian_to_index = structure.lattice.base hkls = lattice_recip.fractional(peaks.dot(R)) # Evaluate error of peak hkl indexation and total error. rhkls = np.rint(hkls) ehkls = np.abs(hkls - rhkls) res_rhkls.append(rhkls) # Indices of matched peaks within error tolerance pair_ids = np.where(np.max(ehkls, axis=1) < index_error_tol)[0] # TODO: SPIND allows trying to match multiple crystals # (overlap) by iteratively matching until match_rate == 0 on # the unindexed peaks # pair_ids = list(set(pair_ids) - set(indexed_peak_ids)) # calculate match_rate as fraction of peaks indexed num_pairs = len(pair_ids) num_peaks = len(peaks) match_rate = num_pairs / num_peaks if len(pair_ids) == 0: # no matching peaks, set error to 1 total_error = 1.0 else: # naive error of matching peaks total_error = ehkls[pair_ids].mean() peak_pair_solutions.append([R, match_rate, ehkls, total_error]) solutions += peak_pair_solutions # TODO: Intersect the solutions from each pair based on orientation. # If there is only one in the intersection, assume that this is # the correct crystal. # TODO: SPIND sorts by highest match rate then lowest total error and # returns the single best solution. Here, we instead return the n # best solutions. Correct approach for pyXem? # best_match_rate_solutions = solutions[solutions[6].argmax()] n_solutions = min(n_best, len(solutions)) if n_solutions > 0: match_rate_index = 1 solutions = np.array(solutions) top_n = solutions[solutions[:, match_rate_index].argpartition( -n_solutions)[-n_solutions:]] # Put the top n ranked solutions in the output array top_matches[phase_index, :, 0] = phase_index top_matches[phase_index, :n_solutions, 1:] = top_n if n_solutions < n_best: # Fill with dummy values top_matches[phase_index, n_solutions:] = [ 0, np.identity(3), 0, np.array([]), 1.0 ] # TODO: Refine? # Because of a bug in numpy (https://github.com/numpy/numpy/issues/7453), # triggered by the way HyperSpy reads results (np.asarray(res), which fails # when the two tuple values have the same first dimension), we cannot # return a tuple directly, but instead have to format the result as an # array ourselves. res = np.empty(2, dtype='object') res[0] = top_matches.reshape((len(library) * n_best, 5)) res[1] = np.asarray(res_rhkls) return res