def construct_beta_quat_array(
    ebsd_map: ebsd.Map,
    alpha_phase_id: int = 0,
    variant_map: np.ndarray = None,
) -> np.ndarray:
    """Construct

    Parameters
    ----------
    ebsd_map:
        EBSD map to assign the beta variants for.
    alpha_phase_id
        Index of the alpha phase in the EBSD map.

    """
    if variant_map is None:
        variant_map = construct_variant_map(ebsd_map, alpha_phase_id)

    transformations = []
    for sym in unq_hex_syms:
        transformations.append(burg_trans * sym.conjugate)
    trans_comps = Quat.extract_quat_comps(transformations)
    trans_comps = trans_comps[:, variant_map[variant_map >= 0]]

    quat_comps = Quat.extract_quat_comps(ebsd_map.quatArray[variant_map >= 0])
    quat_comps_beta = np.empty_like(quat_comps)

    # transformations[variant] * quat
    quat_comps_beta[0, :] = (trans_comps[0, :] * quat_comps[0, :] -
                             trans_comps[1, :] * quat_comps[1, :] -
                             trans_comps[2, :] * quat_comps[2, :] -
                             trans_comps[3, :] * quat_comps[3, :])
    quat_comps_beta[1, :] = (trans_comps[1, :] * quat_comps[0, :] +
                             trans_comps[0, :] * quat_comps[1, :] -
                             trans_comps[3, :] * quat_comps[2, :] +
                             trans_comps[2, :] * quat_comps[3, :])
    quat_comps_beta[2, :] = (trans_comps[2, :] * quat_comps[0, :] +
                             trans_comps[0, :] * quat_comps[2, :] -
                             trans_comps[1, :] * quat_comps[3, :] +
                             trans_comps[3, :] * quat_comps[1, :])
    quat_comps_beta[3, :] = (trans_comps[3, :] * quat_comps[0, :] +
                             trans_comps[0, :] * quat_comps[3, :] -
                             trans_comps[2, :] * quat_comps[1, :] +
                             trans_comps[1, :] * quat_comps[2, :])
    # swap into positive hemisphere if required
    quat_comps_beta[:, quat_comps_beta[0, :] < 0] *= -1

    beta_quat_array = np.empty_like(ebsd_map.quatArray)
    beta_quat_array[variant_map < 0] = Quat(1, 0, 0, 0)
    for i, idx in enumerate(zip(*np.where(variant_map >= 0))):
        beta_quat_array[idx] = Quat(quat_comps_beta[:, i])

    return beta_quat_array
def calc_beta_oris_from_misori(
        alpha_ori: Quat,
        neighbour_oris: List[Quat],
        burg_tol: float = 5) -> Tuple[List[List[Quat]], List[float]]:
    """Calculate the possible beta orientations for a given alpha
    orientation using the misorientation relation to neighbour orientations.

    Parameters
    ----------
    alpha_ori
        A quaternion representing the alpha orientation

    neighbour_oris
        Quaternions representing neighbour grain orientations

    burg_tol
        The threshold misorientation angle to determine neighbour relations

    Returns
    -------
    list of lists of defdap.Quat.quat
        Possible beta orientations, grouped by each neighbour. Any
        neighbour with deviation greater than the tolerance is excluded.
    list of float
        Deviations from perfect Burgers transformation

    """
    burg_tol *= np.pi / 180.
    # This needed to move further up calculation process
    unq_cub_sym_comps = Quat.extract_quat_comps(unq_cub_syms)

    alpha_ori_inv = alpha_ori.conjugate

    beta_oris = []
    beta_devs = []

    for neighbour_ori in neighbour_oris:

        min_misoris, min_cub_sym_idxs = calc_misori_of_variants(
            alpha_ori_inv, neighbour_ori, unq_cub_sym_comps)

        # find the hex symmetries (i, j) from give the minimum
        # deviation from the burgers relation for the minimum store:
        # the deviation, the hex symmetries (i, j) and the cubic
        # symmetry if the deviation is over a threshold then set
        # cubic symmetry to -1
        min_misori_idx = np.unravel_index(np.argmin(min_misoris),
                                          min_misoris.shape)
        burg_dev = min_misoris[min_misori_idx]

        if burg_dev < burg_tol:
            beta_oris.append(
                beta_oris_from_cub_sym(alpha_ori,
                                       min_cub_sym_idxs[min_misori_idx],
                                       int(min_misori_idx[0])))
            beta_devs.append(burg_dev)

    return beta_oris, beta_devs
def calc_misori_of_variants(
        alpha_ori_inv: Quat, neighbour_ori: Quat,
        unq_cub_sym_comps: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
    """Calculate possible symmetry variants between two orientations.

    Calculate all possible sym variants for misorientation between two
    orientations undergoing a Burgers type transformation. Then calculate
    the misorientation to the nearest cubic symmetry, this is the deviation
    to a perfect Burgers transformation.

    Parameters
    ----------
    alpha_ori_inv
        Inverse of first orientation
    neighbour_ori
        Second orientation
    unq_cub_sym_comps
        Components of the unique cubic symmetries

    Returns
    -------
    min_misoris : np.ndarray 
       The minimum misorientation for each of the possible beta variants - shape (12, 12)

    min_cub_sym_idx : np.ndarray
       The minimum cubic symmetry index for each of the possible variants - shape (12, 12)

    """
    # calculate all possible S^B_m (eqn 11. from [1]) from the
    # measured misorientation from 2 neighbour alpha grains
    # for each S^B_m calculate the 'closest' cubic symmetry
    # (from reduced subset) and the deviation from this symmetry

    # Vectorised calculation of:
    # hex_sym[j].inv * ((neighbour_ori * alpha_ori_inv) * hex_sym[i])
    # labelled: d = h2.inv * (c * h1)
    hex_sym_comps = Quat.extract_quat_comps(hex_syms)
    c = (neighbour_ori * alpha_ori_inv).quatCoef
    h1 = np.repeat(hex_sym_comps, 12, axis=1)  # outer loop
    h2 = np.tile(hex_sym_comps, (1, 12))  # inner loop
    d = np.zeros_like(h1)

    c_dot_h1 = c[1] * h1[1] + c[2] * h1[2] + c[3] * h1[3]
    c_dot_h2 = c[1] * h2[1] + c[2] * h2[2] + c[3] * h2[3]
    h1_dot_h2 = h1[1] * h2[1] + h1[2] * h2[2] + h1[3] * h2[3]

    d[0] = (c[0] * h1[0] * h2[0] - h2[0] * c_dot_h1 + c[0] * h1_dot_h2 +
            h1[0] * c_dot_h2 + h2[1] * (c[2] * h1[3] - c[3] * h1[2]) + h2[2] *
            (c[3] * h1[1] - c[1] * h1[3]) + h2[3] *
            (c[1] * h1[2] - c[2] * h1[1]))
    d[1] = (c[0] * h2[0] * h1[1] + h1[0] * h2[0] * c[1] -
            c[0] * h1[0] * h2[1] + c_dot_h1 * h2[1] + c_dot_h2 * h1[1] -
            h1_dot_h2 * c[1] + h2[0] * (c[2] * h1[3] - c[3] * h1[2]) + c[0] *
            (h1[2] * h2[3] - h1[3] * h2[2]) + h1[0] *
            (c[2] * h2[3] - c[3] * h2[2]))
    d[2] = (c[0] * h2[0] * h1[2] + h1[0] * h2[0] * c[2] -
            c[0] * h1[0] * h2[2] + c_dot_h1 * h2[2] + c_dot_h2 * h1[2] -
            h1_dot_h2 * c[2] + h2[0] * (c[3] * h1[1] - c[1] * h1[3]) + c[0] *
            (h1[3] * h2[1] - h1[1] * h2[3]) + h1[0] *
            (c[3] * h2[1] - c[1] * h2[3]))
    d[3] = (c[0] * h2[0] * h1[3] + h1[0] * h2[0] * c[3] -
            c[0] * h1[0] * h2[3] + c_dot_h1 * h2[3] + c_dot_h2 * h1[3] -
            h1_dot_h2 * c[3] + h2[0] * (c[1] * h1[2] - c[2] * h1[1]) + c[0] *
            (h1[1] * h2[2] - h1[2] * h2[1]) + h1[0] *
            (c[1] * h2[2] - c[2] * h2[1]))

    # Vectorised calculation of:
    # burg_trans * (d * burg_trans.inv)
    # labelled: beta_vars = b * (c * b.inv)
    b = burg_trans.quatCoef
    beta_vars = np.zeros_like(h1)

    b_dot_b = b[1] * b[1] + b[2] * b[2] + b[3] * b[3]
    b_dot_d = b[1] * d[1] + b[2] * d[2] + b[3] * d[3]

    beta_vars[0] = d[0] * (b[0] * b[0] + b_dot_b)
    beta_vars[1] = (d[1] * (b[0] * b[0] - b_dot_b) + 2 * b_dot_d * b[1] +
                    2 * b[0] * (b[2] * d[3] - b[3] * d[2]))
    beta_vars[2] = (d[2] * (b[0] * b[0] - b_dot_b) + 2 * b_dot_d * b[2] +
                    2 * b[0] * (b[3] * d[1] - b[1] * d[3]))
    beta_vars[3] = (d[3] * (b[0] * b[0] - b_dot_b) + 2 * b_dot_d * b[3] +
                    2 * b[0] * (b[1] * d[2] - b[2] * d[1]))

    # calculate misorientation to each of the cubic symmetries
    misoris = np.einsum("ij,ik->jk", beta_vars, unq_cub_sym_comps)
    misoris = np.abs(misoris)
    misoris[misoris > 1] = 1.
    misoris = 2 * np.arccos(misoris)

    # find the cubic symmetry with minimum misorientation for each of
    # the beta misorientation variants
    min_cub_sym_idx = np.argmin(misoris, axis=1)
    min_misoris = misoris[np.arange(144), min_cub_sym_idx]
    # reshape to 12 x 12 for each of the hex sym multiplications
    min_cub_sym_idx = min_cub_sym_idx.reshape((12, 12))
    min_misoris = min_misoris.reshape((12, 12))

    return min_misoris, min_cub_sym_idx
def calc_beta_oris_from_boundary_misori(
        grain: ebsd.Grain,
        neighbour_network: nx.Graph,
        quat_array: np.ndarray,
        alpha_phase_id: int,
        burg_tol: float = 5
) -> Tuple[List[List[Quat]], List[float], List[Quat]]:
    """Calculate the possible beta orientations for pairs of alpha and
    neighbour orientations using the misorientation relation to neighbour
    orientations.

    Parameters
    ----------
    grain
        The grain currently being reconstructed

    neighbour_network
        A neighbour network mapping grain boundary connectivity

    quat_array
        Array of quaternions, representing the orientations of the pixels of the EBSD map

    burg_tol :
        The threshold misorientation angle to determine neighbour relations

    Returns
    -------
    list of lists of defdap.Quat.quat
        Possible beta orientations, grouped by each neighbour. Any
        neighbour with deviation greater than the tolerance is excluded.
    list of float
        Deviations from perfect Burgers transformation
    list of Quat
        Alpha orientations

    """
    # This needed to move further up calculation process
    unq_cub_sym_comps = Quat.extract_quat_comps(unq_cub_syms)

    beta_oris = []
    beta_devs = []
    alpha_oris = []

    neighbour_grains = neighbour_network.neighbors(grain)
    neighbour_grains = [
        grain for grain in neighbour_grains if grain.phaseID == alpha_phase_id
    ]
    for neighbour_grain in neighbour_grains:

        bseg = neighbour_network[grain][neighbour_grain]['boundary']
        # check sense of bseg
        if grain is bseg.grain1:
            ipoint = 0
        else:
            ipoint = 1

        for boundary_point_pair in bseg.boundaryPointPairsX:
            point = boundary_point_pair[ipoint]
            alpha_ori = quat_array[point[1], point[0]]

            point = boundary_point_pair[ipoint - 1]
            neighbour_ori = quat_array[point[1], point[0]]

            min_misoris, min_cub_sym_idxs = calc_misori_of_variants(
                alpha_ori.conjugate, neighbour_ori, unq_cub_sym_comps)

            # find the hex symmetries (i, j) from give the minimum
            # deviation from the burgers relation for the minimum store:
            # the deviation, the hex symmetries (i, j) and the cubic
            # symmetry if the deviation is over a threshold then set
            # cubic symmetry to -1
            min_misori_idx = np.unravel_index(np.argmin(min_misoris),
                                              min_misoris.shape)
            burg_dev = min_misoris[min_misori_idx]

            if burg_dev < burg_tol / 180 * np.pi:
                beta_oris.append(
                    beta_oris_from_cub_sym(alpha_ori,
                                           min_cub_sym_idxs[min_misori_idx],
                                           int(min_misori_idx[0])))
                beta_devs.append(burg_dev)
                alpha_oris.append(alpha_ori)

    return beta_oris, beta_devs, alpha_oris