def get_vector_library(self, reciprocal_radius): """Calculates a library of diffraction vectors and pairwise inter-vector angles for a library of crystal structures. Parameters ---------- reciprocal_radius : float The maximum g-vector magnitude to be included in the library. Returns ------- vector_library : :class:`DiffractionVectorLibrary` Mapping of phase identifier to a numpy array with entries in the form: [hkl1, hkl2, len1, len2, angle] ; lengths are in reciprocal Angstroms and angles are in radians. """ # Define DiffractionVectorLibrary object to contain results vector_library = DiffractionVectorLibrary() # Get structures from structure library structure_library = self.structures.struct_lib # Iterate through phases in library. for phase_name in structure_library.keys(): # Get diffpy.structure object associated with phase structure = structure_library[phase_name][0] # Get reciprocal lattice points within reciprocal_radius recip_latt = structure.lattice.reciprocal() indices, coordinates, distances = get_points_in_sphere( recip_latt, reciprocal_radius) # Iterate through all pairs calculating interplanar angle phase_vector_pairs = [] for comb in itertools.combinations(np.arange(len(indices)), 2): i, j = comb[0], comb[1] # Specify hkls and lengths associated with the crystal structure. # TODO: This should be updated to reflect systematic absences if np.count_nonzero(coordinates[i]) == 0 or np.count_nonzero( coordinates[j]) == 0: continue # Ignore combinations including [000] hkl1 = indices[i] hkl2 = indices[j] len1 = distances[i] len2 = distances[j] if len1 < len2: # Keep the longest first hkl1, hkl2 = hkl2, hkl1 len1, len2 = len1, len2 angle = get_angle_cartesian(coordinates[i], coordinates[j]) phase_vector_pairs.append( np.array([hkl1, hkl2, len1, len2, angle])) vector_library[phase_name] = np.array(phase_vector_pairs) # Pass attributes to diffraction library from structure library. vector_library.identifiers = self.structures.identifiers vector_library.structures = self.structures.structures return vector_library
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_angle_cartesian(vec_a, vec_b, expected_angle): angle = get_angle_cartesian(vec_a, vec_b) np.testing.assert_allclose(angle, expected_angle)
def rotation_list_stereographic(structure, corner_a, corner_b, corner_c, inplane_rotations, resolution): """Generate a rotation list covering the inverse pole figure specified by three corners in cartesian coordinates. Parameters ---------- structure : diffpy.structure.Structure Structure for which to calculate the rotation list. corner_a, corner_b, corner_c : tuple The three corners of the inverse pole figure, each given by a three-dimensional coordinate. The coordinate system is given by the structure lattice. resolution : float Angular resolution in radians of the generated rotation list. inplane_rotations : list List of angles in radians for in-plane rotation of the diffraction pattern. This corresponds to the third Euler angle rotation. The rotation list will be generated for each of these angles, and combined. This should be done automatically, but by including all possible rotations in the rotation list, it becomes too large. To cover all inplane rotations, use e.g. np.linspace(0, 2*np.pi, 360). Returns ------- rotation_list : numpy.array Rotations covering the inverse pole figure given as an array of Euler angles in degrees. """ # Convert the crystal directions to cartesian vectors and normalize if len(corner_a) == 4: corner_a = uvtw_to_uvw(corner_a) if len(corner_b) == 4: corner_b = uvtw_to_uvw(corner_b) if len(corner_c) == 4: corner_c = uvtw_to_uvw(corner_c) lattice = structure.lattice corner_a = np.dot(corner_a, lattice.stdbase) corner_b = np.dot(corner_b, lattice.stdbase) corner_c = np.dot(corner_c, lattice.stdbase) corner_a /= np.linalg.norm(corner_a) corner_b /= np.linalg.norm(corner_b) corner_c /= np.linalg.norm(corner_c) angle_a_to_b = get_angle_cartesian(corner_a, corner_b) angle_a_to_c = get_angle_cartesian(corner_a, corner_c) angle_b_to_c = get_angle_cartesian(corner_b, corner_c) axis_a_to_b = np.cross(corner_a, corner_b) axis_a_to_c = np.cross(corner_a, corner_c) # Input validation. The corners have to define a non-degenerate triangle if np.count_nonzero(axis_a_to_b) == 0: raise ValueError('Directions a and b are parallel') if np.count_nonzero(axis_a_to_c) == 0: raise ValueError('Directions a and c are parallel') rotations = [] # Generate a list of theta_count evenly spaced angles theta_b in the range # [0, angle_a_to_b] and an equally long list of evenly spaced angles # theta_c in the range[0, angle_a_to_c]. # Ensure that we keep the resolution also along the direction to the corner # b or c farthest away from a. theta_count = math.ceil(max(angle_a_to_b, angle_a_to_c) / resolution) for i, (theta_b, theta_c) in enumerate( zip(np.linspace(0, angle_a_to_b, theta_count), np.linspace(0, angle_a_to_c, theta_count))): # Define the corner local_b at a rotation theta_b from corner_a toward # corner_b on the circle surface. Similarly, define the corner local_c # at a rotation theta_c from corner_a toward corner_c. rotation_a_to_b = axangle2mat(axis_a_to_b, theta_b) rotation_a_to_c = axangle2mat(axis_a_to_c, theta_c) local_b = np.dot(rotation_a_to_b, corner_a) local_c = np.dot(rotation_a_to_c, corner_a) # Then define an axis and a maximum rotation to create a great cicle # arc between local_b and local_c. Ensure that this is not a degenerate # case where local_b and local_c are coincident. angle_local_b_to_c = get_angle_cartesian(local_b, local_c) axis_local_b_to_c = np.cross(local_b, local_c) if np.count_nonzero(axis_local_b_to_c) == 0: # Theta rotation ended at the same position. First position, might # be other cases? axis_local_b_to_c = corner_a axis_local_b_to_c /= np.linalg.norm(axis_local_b_to_c) # Generate points along the great circle arc with a distance defined by # resolution. phi_count_local = max(math.ceil(angle_local_b_to_c / resolution), 1) for j, phi in enumerate( np.linspace(0, angle_local_b_to_c, phi_count_local)): rotation_phi = axangle2mat(axis_local_b_to_c, phi) for k, psi in enumerate(inplane_rotations): # Combine the rotations. Order is important. The matrix is # applied from the left, and we rotate by theta first toward # local_b, then across the triangle toward local_c rotation = list( mat2euler(rotation_phi @ rotation_a_to_b, 'rzxz')) rotations.append(np.rad2deg([rotation[0], rotation[1], psi])) return np.unique(rotations, axis=0)
def test_get_angle_cartesian(vec_a, vec_b, expected_angle): angle = get_angle_cartesian(vec_a, vec_b) assert np.isclose(angle, expected_angle)
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