diff --git a/kikuchipy/detectors/ebsd_detector.py b/kikuchipy/detectors/ebsd_detector.py index d6fe96b0..2d54ce9f 100644 --- a/kikuchipy/detectors/ebsd_detector.py +++ b/kikuchipy/detectors/ebsd_detector.py @@ -1110,7 +1110,7 @@ def get_indexer( self, phase_list: PhaseList, reflectors: Optional[ - List[Union["ReciprocalLatticeVector", np.ndarray, None]] + List[Union["ReciprocalLatticeVector", np.ndarray, list, tuple, None]] ] = None, **kwargs, ) -> "EBSDIndexer": @@ -1123,13 +1123,13 @@ def get_indexer( only supports a list containing one face-centered cubic (FCC) phase, one body-centered cubic (BCC) phase or both. reflectors - List of unique reflectors :math:`\{hkl\}` to use in indexing - for each phase. If not passed, the default in + List of reflectors or pole families :math:`\{hkl\}` to use + in indexing for each phase. If not passed, the default in :func:`pyebsdindex.tripletvote.addphase` is used. For each - phase, the reflector list can either be None, a NumPy array, - or a - :class:`~diffsis.crystallography.ReciprocalLatticeVector`. - If None is passed, the default in PyEBSDIndex is used. + phase, the reflectors can either be a NumPy array, a list, + a tuple, a + :class:`~diffsis.crystallography.ReciprocalLatticeVector`, + or None. **kwargs Keyword arguments passed to :class:`~pyebsdindex.ebsd_index.EBSDIndexer`, except for the @@ -1151,6 +1151,10 @@ def get_indexer( Requires that PyEBSDIndex is installed, which is an optional dependency of kikuchipy. See :ref:`optional-dependencies` for details. + + See Also + -------- + pyebsdindex.tripletvote.addphase """ return _get_indexer_from_detector( phase_list=phase_list, diff --git a/kikuchipy/detectors/tests/test_ebsd_detector.py b/kikuchipy/detectors/tests/test_ebsd_detector.py index 354d0cbb..f4f2c2b5 100644 --- a/kikuchipy/detectors/tests/test_ebsd_detector.py +++ b/kikuchipy/detectors/tests/test_ebsd_detector.py @@ -1052,47 +1052,34 @@ def test_get_indexer_raises(self): # pragma: no cover not kp._pyebsdindex_installed, reason="pyebsdindex is not installed" ) def test_get_indexer_invalid_phase_lists(self): - # More than two phases - with pytest.raises(ValueError, match="Hough indexing only supports indexing "): - _ = self.det.get_indexer( - PhaseList(names=["a", "b", "c"], space_groups=[225, 227, 229]) - ) - # Not all phases have space groups pl = PhaseList(names=["a", "b"], point_groups=["m-3m", "432"]) pl["a"].space_group = 225 with pytest.raises(ValueError, match="Space group for each phase must be set,"): _ = self.det.get_indexer(pl) - # Not FCC or BCC - with pytest.raises(ValueError, match="Hough indexing only supports indexing "): - _ = self.det.get_indexer(PhaseList(names="sic", space_groups=186)) - @pytest.mark.skipif( not kp._pyebsdindex_installed, reason="pyebsdindex is not installed" ) def test_get_indexer(self): - indexer1 = self.det.get_indexer( - PhaseList(names=["a", "b"], space_groups=[225, 229]), nBands=6 - ) - assert indexer1.vendor == "KIKUCHIPY" - assert np.isclose(indexer1.sampleTilt, self.det.sample_tilt) - assert np.isclose(indexer1.camElev, self.det.tilt) - assert tuple(indexer1.bandDetectPlan.patDim) == self.det.shape - assert indexer1.bandDetectPlan.nBands == 6 - assert np.allclose(indexer1.PC, self.det.pc_flattened) - assert indexer1.phaselist == ["FCC", "BCC"] - - indexer2 = self.det.get_indexer(PhaseList(names="a", space_groups=225)) - assert indexer2.phaselist == ["FCC"] - - indexer3 = self.det.get_indexer(PhaseList(names="a", space_groups=220)) - assert indexer3.phaselist == ["BCC"] - - indexer4 = self.det.get_indexer( - PhaseList(names=["a", "b"], space_groups=[220, 225]) - ) - assert indexer4.phaselist == ["BCC", "FCC"] + # fmt: off + # -1 2/m 222 -3 -3m 4/m 4/mmm 6/m 6/mmm m-3 m-3m + space_groups = [ 1, 15, 74, 75, 142, 143, 167, 168, 194, 195, 207] + laue_codes = [ 1, 2, 22, 4, 42, 3, 32, 6, 62, 23, 43] + # fmt: on + names = "abcdefghijk" + + pl = PhaseList(names=list(names), space_groups=space_groups) + indexer = self.det.get_indexer(pl, nBands=6) + assert indexer.vendor == "KIKUCHIPY" + assert np.isclose(indexer.sampleTilt, self.det.sample_tilt) + assert np.isclose(indexer.camElev, self.det.tilt) + assert tuple(indexer.bandDetectPlan.patDim) == self.det.shape + assert indexer.bandDetectPlan.nBands == 6 + assert np.allclose(indexer.PC, self.det.pc_flattened) + for phase, sg, code in zip(indexer.phaselist, pl.space_groups, laue_codes): + assert phase.spacegroup == sg.number + assert phase.lauecode == code class TestSaveLoadDetector: diff --git a/kikuchipy/indexing/_hough_indexing.py b/kikuchipy/indexing/_hough_indexing.py index 5937e059..51b1bf6a 100644 --- a/kikuchipy/indexing/_hough_indexing.py +++ b/kikuchipy/indexing/_hough_indexing.py @@ -117,7 +117,9 @@ def _get_indexer_from_detector( pc: np.ndarray, sample_tilt: float, tilt: float, - reflectors: Optional[List[Union[ReciprocalLatticeVector, np.ndarray, None]]] = None, + reflectors: Optional[ + List[Union[ReciprocalLatticeVector, np.ndarray, list, tuple, None]] + ] = None, **kwargs, ) -> "EBSDIndexer": r"""Return a PyEBSDIndex EBSD indexer. @@ -135,13 +137,13 @@ def _get_indexer_from_detector( tilt Detector tilt in degrees. reflectors - List of unique reflectors :math:`\{hkl\}` to use in indexing for - each phase. If not passed, the default in + List of reflectors or pole families :math:`\{hkl\}` to use in + indexing for each phase. If not passed, the default in :func:`pyebsdindex.tripletvote.addphase` is used. For each - phase, the reflector list can either be None, a NumPy array, - or a - :class:`~diffsis.crystallography.ReciprocalLatticeVector`. - If None is passed, the default in PyEBSDIndex is used. + phase, the reflectors can either be a NumPy array, a list, a + tuple, a + :class:`~diffsis.crystallography.ReciprocalLatticeVector`, or + None. **kwargs Keyword arguments passed to :class:`~pyebsdindex.ebsd_index.EBSDIndexer`. @@ -250,28 +252,35 @@ def _hough_indexing( def _get_pyebsdindex_phaselist( phase_list: PhaseList, - reflectors: Optional[List[Union[ReciprocalLatticeVector, np.ndarray, None]]] = None, + reflectors: Optional[ + List[Union[ReciprocalLatticeVector, np.ndarray, list, tuple, None]] + ] = None, ) -> List["BandIndexer"]: - r"""Return a phase list compatible with PyEBSDIndex or raise a - ``ValueError`` if the given phase list is incompatible. + r"""Return a phase list compatible with PyEBSDIndex from an orix + phase list. + + A ``ValueError`` is raised if the orix phase list contains phases + without the space group set or if the length of the reflector list + is unequal to the list of phases. Parameters ---------- phase_list Phase list to convert to one compatible with PyEBSDIndex. reflectors - List of unique reflectors :math:`\{hkl\}` to use in indexing for - each phase. If not passed, the default in + List of reflectors or pole families :math:`\{hkl\}` to use in + indexing for each phase. If not passed, the default in :func:`pyebsdindex.tripletvote.addphase` is used. For each - phase, the reflector list can either be None, a NumPy array, - or a - :class:`~diffsis.crystallography.ReciprocalLatticeVector`. - If None is passed, the default in PyEBSDIndex is used. + phase, the reflectors can either be a NumPy array, a list, a + tuple, a + :class:`~diffsis.crystallography.ReciprocalLatticeVector`, or + None. Returns ------- - phase_list_pei - Phase list compatible with PyEBSDIndex. + phase_list_pei : list of pyebsdindex.tripletvote.BandIndexer + Phase list of phases (band indexers) compatible with + PyEBSDIndex. Raises ------ @@ -281,17 +290,17 @@ def _get_pyebsdindex_phaselist( """ from pyebsdindex.tripletvote import addphase + # Make list of reflectors iterable if reflectors is None: reflectors = [None] * phase_list.size elif isinstance(reflectors, (np.ndarray, ReciprocalLatticeVector)): - reflectors = [ - reflectors, - ] + reflectors = [reflectors] + elif isinstance(reflectors, (list, tuple)) and len(reflectors) != phase_list.size: + reflectors = [reflectors] if len(reflectors) != phase_list.size: raise ValueError( - "One list of reflectors (or None) must be passed per phase in the phase " - "list." + "One set of reflectors or None must be passed per phase in the phase list." ) phase_list_pei = [] @@ -307,8 +316,7 @@ def _get_pyebsdindex_phaselist( ref = reflectors[i] if isinstance(ref, ReciprocalLatticeVector): ref = ref.unique(use_symmetry=True).hkl - elif isinstance(ref, np.ndarray): - # Ensure uniqueness + elif isinstance(ref, (np.ndarray, list, tuple)): ref = ReciprocalLatticeVector(phase, hkl=ref).unique(use_symmetry=True).hkl else: ref = None @@ -334,8 +342,9 @@ def _indexer_is_compatible_with_kikuchipy( check_pc: bool = True, raise_if_not: bool = False, ) -> bool: - """Check whether an indexer is compatible with kikuchipy or raise a - ``ValueError`` if it is not. + """Check whether an indexer is compatible with kikuchipy. + + A ``ValueError`` is raised if it is not. Parameters ---------- @@ -350,8 +359,8 @@ def _indexer_is_compatible_with_kikuchipy( check_pc Whether to check ``indexer.PC`` (default is ``True``). raise_if_not - Whether to raise a ``ValueError`` if the indexer is not - compatible with the signal. Default is ``False``. + Whether to raise a ``ValueError`` if the indexer is incompatible + with the signal. Default is ``False``. Returns ------- @@ -390,18 +399,93 @@ def _indexer_is_compatible_with_kikuchipy( f"{pc_shape} instead." ) - # phase_list_pei = indexer.phaselist - # allowed_lists = [["FCC"], ["BCC"], ["FCC", "BCC"], ["BCC", "FCC"]] - # if compatible and phase_list_pei not in allowed_lists: - # compatible = False - # error_msg = f"`indexer.phaselist` must be one of {allowed_lists}" - if raise_if_not and not compatible: raise ValueError(error_msg) else: return compatible +def _phase_lists_are_compatible( + phase_list: PhaseList, + indexer, + raise_if_not: bool = False, +) -> bool: + """Check whether phase lists made with orix and PyEBSDIndex are + compatible. + + A ``ValueError`` is raised if they are not. + + They are compatible if the lists have an equal number of phases in + the same order and if the corresponding phases have equal lattice + parameters (to the 12th decimal) and the same space group number. + + Parameters + ---------- + phase_list + Phase list made with orix. + indexer : pyebsdindex.ebsd_index.EBSDIndexer + EBSD indexer with a phase list from PyEBSDIndex. + raise_if_not + Whether to raise a ``ValueError`` if the phase lists are + incompatible. Default is ``False``. + + Returns + ------- + compatible + Whether the phase lists are compatible. + + Raises + ------ + ValueError + Raised if the phase lists are incompatible. + """ + compatible = True + + phase_list_pei = indexer.phaselist + + msg = ( + f"`indexer.phaselist` {phase_list_pei} and the list determined from" + f" `phase_list` must be the same" + ) + + n_phases = phase_list.size + n_phases_pei = len(phase_list_pei) + if n_phases != n_phases_pei: + compatible = False + msg = ( + f"`phase_list` and `indexer.phaselist` have unequal lengths {n_phases} and " + f"{n_phases_pei}" + ) + else: + for (_, phase), phase_pei in zip(phase_list, phase_list_pei): + lat = phase.structure.lattice.abcABG() + lat_pei = phase_pei.latticeparameter + sg = phase.space_group.number + sg_pei = phase_pei.spacegroup + + if not np.allclose(lat, lat_pei, atol=1e-12): + compatible = False + msg = ( + f"Phase '{phase.name}' in `phase_list` and {phase_pei} in " + f"`indexer.phaselist` have unequal lattice parameters {lat} and " + f"{lat_pei}" + ) + break + elif sg != sg_pei: + compatible = False + msg = ( + f"Phase '{phase.name}' in `phase_list` and {phase_pei} in " + f"`indexer.phaselist` have unequal space group numbers {sg} and " + f"{sg_pei}" + ) + break + + if raise_if_not and not compatible: + raise ValueError(msg) + else: + return compatible + + def _get_info_message(nav_size: int, chunksize: int, indexer: "EBSDIndexer") -> str: from kikuchipy import _pyopencl_context_available diff --git a/kikuchipy/signals/ebsd.py b/kikuchipy/signals/ebsd.py index 192b9dde..1cbc7c7c 100644 --- a/kikuchipy/signals/ebsd.py +++ b/kikuchipy/signals/ebsd.py @@ -45,10 +45,10 @@ from kikuchipy.filters.window import Window from kikuchipy.indexing._dictionary_indexing import _dictionary_indexing from kikuchipy.indexing._hough_indexing import ( - _get_pyebsdindex_phaselist, _indexer_is_compatible_with_kikuchipy, _hough_indexing, _optimize_pc, + _phase_lists_are_compatible, ) from kikuchipy.indexing._refinement._refinement import ( _refine_orientation, @@ -1606,9 +1606,8 @@ def hough_indexing( Parameters ---------- phase_list - List of phases. The list can only contain one face-centered - cubic (FCC) phase, one body-centered cubic (BCC) phase or - both types. + List of phases. The list must correspond to the phase list + in the passed. indexer PyEBSDIndex EBSD indexer instance of which the :meth:`~pyebsdindex.ebsd_index.EBSDIndexer.index_pats` @@ -1675,15 +1674,10 @@ def hough_indexing( step_sizes = tuple([a.scale for a in am.navigation_axes[::-1]]) # Check indexer (but not the reflectors) - phase_list_pei = _get_pyebsdindex_phaselist(phase_list) _ = _indexer_is_compatible_with_kikuchipy( indexer, sig_shape, nav_size, raise_if_not=True ) - # if indexer.phaselist != phase_list_pei: - # raise ValueError( - # f"`indexer.phaselist` {indexer.phaselist} and the list determined from" - # f" `phase_list` {phase_list_pei} must be the same" - # ) + _ = _phase_lists_are_compatible(phase_list, indexer, raise_if_not=True) # Prepare patterns chunksize = min(chunksize, max(am.navigation_size, 1)) @@ -1724,7 +1718,7 @@ def hough_indexing_optimize_pc( pattern optimized using Hough indexing from :mod:`pyebsdindex`. See :class:`~pyebsdindex.ebsd_index.EBSDIndexer` and - :mod:`~pyebsdindex.pcopt.optimize` for details. + :mod:`~pyebsdindex.pcopt` for details. Parameters ---------- diff --git a/kikuchipy/signals/tests/test_ebsd_hough_indexing.py b/kikuchipy/signals/tests/test_ebsd_hough_indexing.py index d0a561cd..04bed8a3 100644 --- a/kikuchipy/signals/tests/test_ebsd_hough_indexing.py +++ b/kikuchipy/signals/tests/test_ebsd_hough_indexing.py @@ -143,7 +143,7 @@ def test_hough_indexing_return_band_data(self): def test_hough_indexing_raises_dissimilar_phase_lists(self): phase_list = PhaseList(names=["a", "b"], space_groups=[225, 229]) - with pytest.raises(ValueError, match=r"`indexer.phaselist` \['FCC'\] and the "): + with pytest.raises(ValueError, match=r"`phase_list` and `indexer.phaselist` "): _ = self.signal.hough_indexing(phase_list, self.indexer) def test_indexer_is_compatible_with_signal(self): @@ -182,17 +182,6 @@ def test_indexer_is_compatible_with_signal(self): indexer, (60, 60), 8, raise_if_not=True ) - # Phase list - indexer.phaselist = ["FCC", "FCC"] - assert not _indexer_is_compatible_with_kikuchipy(indexer, (60, 60), 9) - with pytest.raises( - ValueError, - match=r"`indexer.phaselist` must be one of \[\['FCC'\], \['BCC'\], \['FCC',", - ): - _indexer_is_compatible_with_kikuchipy( - indexer, (60, 60), 9, raise_if_not=True - ) - def test_hough_indexing_get_xmap_from_index_data(self): phase_list = self.phase_list xmap, index_data = self.signal.hough_indexing(