Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
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)
Ejemplo n.º 6
0
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