def test_merging_refined_maps(self): ny, nx = (3, 3) nav_size = ny * nx r = Rotation.from_euler(np.ones((nav_size, 3))) x = np.tile(np.arange(ny), nx) y = np.repeat(np.arange(nx), ny) # Simulation indices n_sim_indices = 10 sim_indices1 = np.random.randint(low=0, high=1000, size=n_sim_indices * nav_size).reshape( (nav_size, n_sim_indices)) sim_indices2 = np.random.randint(low=0, high=1000, size=n_sim_indices * nav_size).reshape( (nav_size, n_sim_indices)) # Scores scores1 = np.ones(nav_size) scores1[0] = 3 scores2 = 2 * np.ones(nav_size) xmap1 = CrystalMap( rotations=r, phase_id=np.ones(nav_size) * 0, phase_list=PhaseList(Phase(name="a")), x=x, y=y, prop={ "simulation_indices": sim_indices1, "scores": scores1 }, ) xmap2 = CrystalMap( rotations=r, phase_id=np.ones(nav_size), phase_list=PhaseList(Phase(name="b")), x=x, y=y, prop={ "simulation_indices": sim_indices2, "scores": scores2 }, ) xmap_merged = merge_crystal_maps(crystal_maps=[xmap1, xmap2]) assert "simulation_indices" not in xmap_merged.prop.keys() assert "merged_simulation_indices" not in xmap_merged.prop.keys() with pytest.raises(ValueError, match="Cannot merge maps with more"): _ = merge_crystal_maps( crystal_maps=[xmap1, xmap2], simulation_indices_prop="simulation_indices", )
def test_phase2dict_spacegroup(self): """Space group is written to dict as an int or "None".""" sg100 = 100 phase = Phase(space_group=sg100) phase_dict1 = phase2dict(phase) assert phase_dict1["space_group"] == sg100 sg200 = GetSpaceGroup(200) phase.space_group = sg200 phase_dict2 = phase2dict(phase) assert phase_dict2["space_group"] == sg200.number phase.space_group = None phase_dict3 = phase2dict(phase) assert phase_dict3["space_group"] == "None"
def test_symmetrise(self): assert np.allclose( ReciprocalLatticePoint(phase=Phase(space_group=225), hkl=[1, 1, 1]) .symmetrise() .hkl.data, np.array( [ [1, 1, 1], [-1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], [-1, -1, -1], ] ), ) rlp2, multiplicity = ReciprocalLatticePoint( phase=Phase(space_group=186), hkl=[2, 2, 0] ).symmetrise(return_multiplicity=True) assert multiplicity == 12 assert np.allclose( rlp2.hkl.data, [ [2, 2, 0], [-2, 0, 0], [0, -2, 0], [-2, -2, 0], [2, 0, 0], [0, 2, 0], [-2, 2, 0], [0, -2, 0], [2, 0, 0], [2, -2, 0], [0, 2, 0], [-2, 0, 0], ], ) rlp3 = ReciprocalLatticePoint( phase=Phase(space_group=186), hkl=[2, 2, 0] ).symmetrise(antipodal=False) assert np.allclose( rlp3.hkl.data, [[2, 2, 0], [-2, 0, 0], [0, -2, 0], [-2, -2, 0], [2, 0, 0], [0, 2, 0]], )
def nickel_phase(): return Phase( name="nickel", space_group=225, structure=Structure( lattice=Lattice(3.5236, 3.5236, 3.5236, 90, 90, 90), atoms=[Atom(xyz=[0, 0, 0], atype="Ni", Uisoequiv=0.006332)], ), )
def __init__(self, *args, **kwargs): """Create an :class:`~kikuchipy.signals.EBSDMasterPattern` object from a :class:`hyperspy.signals.Signal2D` or a :class:`numpy.ndarray`. """ Signal2D.__init__(self, *args, **kwargs) self.phase = kwargs.pop("phase", Phase()) self.projection = kwargs.pop("projection", None) self.hemisphere = kwargs.pop("hemisphere", None)
def test_allowed_b_centering(self): """B centering will never fire since no diffpy.structure space group has 'B' first in the name. """ phase = Phase(space_group=15) phase.space_group.short_name = "B" rlp = ReciprocalLatticePoint( phase=phase, hkl=[[1, 2, 2], [1, 1, -1], [1, 1, 2]] ) assert np.allclose(rlp.allowed, [False, True, False])
def test_dict2phase_spacegroup(self): """Space group number int or None is properly parsed from a dict. """ phase1 = Phase(space_group=200) phase_dict = phase2dict(phase1) phase2 = dict2phase(phase_dict) assert phase1.space_group.number == phase2.space_group.number phase_dict.pop("space_group") phase3 = dict2phase(phase_dict) assert phase3.space_group is None
def __init__(self, *args, **kwargs): """Create an :class:`~kikuchipy.signals.EBSDMasterPattern` instance from a :class:`hyperspy.signals.Signal2D` or a :class:`numpy.ndarray`. See the docstring of :class:`hyperspy.signal.BaseSignal` for optional input parameters. """ Signal2D.__init__(self, *args, **kwargs) self.phase = kwargs.pop("phase", Phase()) self.projection = kwargs.pop("projection", None) self.hemisphere = kwargs.pop("hemisphere", None)
def ferrite_phase(): return Phase( name="ferrite", space_group=229, structure=Structure( lattice=Lattice(2.8665, 2.8665, 2.8665, 90, 90, 90), atoms=[ Atom(xyz=[0, 0, 0], atype="Fe", Uisoequiv=0.006332), Atom(xyz=[0.5, 0.5, 0.5], atype="Fe", Uisoequiv=0.006332), ], ), )
def test_init_no_metadata(self): s = EBSDMasterPattern( np.zeros((2, 10, 11, 11)), projection="lambert", hemisphere="both", phase=Phase("a"), ) assert isinstance(s.phase, Phase) assert s.phase.name == "a" assert s.projection == "lambert" assert s.hemisphere == "both"
def silicon_carbide_phase(): """Silicon Carbide 4H polytype (hexagonal, space group 186).""" return Phase( space_group=186, structure=Structure( lattice=Lattice(3.073, 3.073, 10.053, 90, 90, 120), atoms=[ Atom(atype="Si", xyz=[0, 0, 0]), Atom(atype="Si", xyz=[0.33, 0.667, 0.25]), Atom(atype="C", xyz=[0, 0, 0.188]), Atom(atype="C", xyz=[0.333, 0.667, 0.438]), ], ), )
def test_extra_phases(self, crystal_map, tmp_path, extra_phase_names): crystal_map.phases.add_not_indexed() for i, name in enumerate(extra_phase_names): crystal_map.phases.add(Phase(name=name)) crystal_map[i].phase_id = crystal_map.phases.id_from_name(name) fname = tmp_path / "test_extra_phases.ang" save(fname, crystal_map) xmap_reload = load(fname) crystal_map.phases[0].name = "phase1" assert np.allclose(xmap_reload.phase_id - 1, crystal_map.phase_id) pl = crystal_map.phases del pl[-1] assert xmap_reload.phases.names == pl.names
def __init__( self, detector: EBSDDetector, phase: Phase, rotations: Rotation, ): """A generator storing necessary parameters to simulate geometrical EBSD patterns. Parameters ---------- detector Detector describing the detector-sample geometry. phase A phase container with a crystal structure and a space and point group describing the allowed symmetry operations. rotations Unit cell rotations to simulate patterns for. The navigation shape of the resulting simulation is determined from the rotations' shape, with a maximum dimension of 2. Examples -------- >>> from orix.crystal_map import Phase >>> from orix.quaternion import Rotation >>> from kikuchipy.detectors import EBSDDetector >>> from kikuchipy.generators import EBSDSimulationGenerator >>> det = EBSDDetector( ... shape=(60, 60), sample_tilt=70, pc=[0.5,] * 3 ... ) >>> p = Phase(name="ni", space_group=225) >>> p.structure.lattice.setLatPar(3.52, 3.52, 3.52, 90, 90, 90) >>> simgen = EBSDSimulationGenerator( ... detector=det, ... phase=p, ... rotations=Rotation.from_euler([90, 45, 90]) ... ) >>> simgen EBSDSimulationGenerator (1,) EBSDDetector (60, 60), px_size 1 um, binning 1, tilt 0, pc (0.5, 0.5, 0.5) <name: . space group: None. point group: None. proper point group: None. color: tab:blue> Rotation (1,) """ self.detector = detector.deepcopy() self.phase = phase.deepcopy() self.rotations = deepcopy(rotations)
def dict2phase(dictionary): """Get a :class:`~orix.crystal_map.phase_list.Phase` object from a dictionary. Parameters ---------- dictionary : dict Dictionary with phase information. Returns ------- Phase """ dictionary = copy.deepcopy(dictionary) structure = dict2structure(dictionary["structure"]) structure.title = dictionary["name"] # TODO: Remove this check in v0.6.0, since space_group was introduced in v0.4.0 try: space_group = dictionary["space_group"] # Either "None" or int except KeyError: # v0.3.0 space_group = "None" if space_group == "None": space_group = None else: space_group = int(space_group) # TODO: Remove this check in v0.6.0, since name change was introduced in v0.4.0 try: point_group = dictionary["point_group"] except KeyError: # v0.3.0 point_group = dictionary["symmetry"] if point_group == "None": point_group = None return Phase( name=dictionary["name"], space_group=space_group, point_group=point_group, structure=structure, color=dictionary["color"], )
def _get_single_phase_xmap( nav_shape, rotations_per_point=5, prop_names=["scores", "simulation_indices"], name="a", phase_id=0, ): d, map_size = _get_spatial_array_dicts(nav_shape) rot_idx = np.random.choice(np.arange(rotations.size), map_size * rotations_per_point) data_shape = (map_size, ) if rotations_per_point > 1: data_shape += (rotations_per_point, ) d["rotations"] = rotations[rot_idx].reshape(*data_shape) d["phase_id"] = np.ones(map_size) * phase_id d["phase_list"] = PhaseList(Phase(name=name)) # Scores and simulation indices d["prop"] = { prop_names[0]: np.ones(data_shape, dtype=np.float32), prop_names[1]: np.arange(np.prod(data_shape)).reshape(data_shape), } return CrystalMap(**d)
def _crystaldata2phase(dictionary: dict) -> Phase: """Return a :class:`~orix.crystal_map.Phase` object from a dictionary with EMsoft CrystalData group content. Parameters ---------- dictionary Dictionary with phase information. Returns ------- Phase """ # TODO: Move this to orix.io.plugins.emsoft_h5ebsd as part of v0.6 # Get list of atoms n_atoms = dictionary["Natomtypes"] atom_data = dictionary["AtomData"] atom_types = dictionary["Atomtypes"] if n_atoms == 1: atom_types = (atom_types, ) # Make iterable atoms = [] for i in range(n_atoms): # TODO: Convert atom type integer to element name, like Ni for 26 atoms.append( Atom( atype=atom_types[i], xyz=atom_data[:3, i], occupancy=atom_data[3, i], Uisoequiv=atom_data[4, i] / (8 * np.pi**2) * 1e2, # Å^-2 )) # TODO: Use space group setting return Phase( space_group=int(dictionary["SpaceGroupNumber"]), structure=Structure( lattice=Lattice(*dictionary["LatticeParameters"].T), atoms=atoms), )
def test_check_master_pattern_and_get_data(self): axes = [ dict(name="hemisphere", size=2, scale=1), dict(name="energy", size=5, offset=16, scale=1), dict(name="dy", size=5, scale=1), dict(name="dx", size=5, scale=1), ] mp_data = np.random.rand(2, 5, 5, 5).astype(np.float64) mp = kp.signals.EBSDMasterPattern( mp_data, axes=axes, projection="lambert", hemisphere="both", phase=Phase("ni", 225), ) ( mpn, mps, npx, npy, scale, ) = kp.indexing._refinement._refinement._check_master_pattern_and_get_data( master_pattern=mp, energy=20, ) assert npx == mp_data.shape[3] assert npy == mp_data.shape[2] assert scale == (mp_data.shape[3] - 1) / 2 # Master patterns are rescaled assert mpn.dtype == np.float32 assert not np.allclose(mpn, mp_data[0, -1]) assert not np.allclose(mps, mp_data[1, -1]) assert np.min(mpn) >= -1 assert np.max(mpn) <= 1
def test_get_patterns(self): emsoft_key = load(EMSOFT_EBSD_FILE) emsoft_key = emsoft_key.data[0] r = Rotation.from_euler(np.radians([120, 45, 60])) mp1 = nickel_ebsd_master_pattern_small(projection="lambert", hemisphere="both") kp_pattern = mp1.get_patterns(rotations=r, detector=self.detector, energy=20, dtype_out=emsoft_key.dtype) kp_pat = kp_pattern.data[0].compute() assert kp_pat.dtype == emsoft_key.dtype ncc = NormalizedCrossCorrelationMetric(1, 1) ncc1 = ncc(kp_pat, emsoft_key) assert ncc1 >= 0.935 ndp = NormalizedDotProductMetric(1, 1) ndp1 = ndp(kp_pat, emsoft_key) assert ndp1 >= 0.935 detector_shape = self.detector.shape r2 = Rotation.from_euler(((0, 0, 0), (1, 1, 1), (2, 2, 2))) mp2 = kp.signals.EBSDMasterPattern(np.zeros((2, 10, 11, 11))) mp2.axes_manager[0].name = "hemisphere" mp2.axes_manager[1].name = "energy" mp2.projection = "lambert" mp2.phase = Phase("Ni", 225) out2 = mp2.get_patterns(r2, self.detector, 5) assert isinstance(out2, kp.signals.LazyEBSD) desired_data_shape = (3, ) + detector_shape[::-1] assert out2.axes_manager.shape == desired_data_shape mp3 = kp.signals.EBSDMasterPattern(np.zeros((10, 11, 11))) mp3.axes_manager[0].name = "energy" mp3.projection = "lambert" mp3.phase = Phase("Ni", 225) out3 = mp3.get_patterns(r2, self.detector, 5) assert isinstance(out3, kp.signals.LazyEBSD) assert out3.axes_manager.shape == desired_data_shape mp4 = kp.signals.EBSDMasterPattern(np.zeros((11, 11))) mp4.projection = "lambert" mp4.phase = Phase("Ni", 225) out41 = mp4.get_patterns(r2, self.detector, 5) out42 = mp4.get_patterns(r2, self.detector, 5, compute=True) assert isinstance(out41, kp.signals.LazyEBSD) assert isinstance(out42, kp.signals.EBSD) assert out41.axes_manager.shape == desired_data_shape mp5 = kp.signals.EBSDMasterPattern(np.zeros((11, 11))) mp5.projection = "lambert" mp5.phase = Phase("!Ni", 220) with pytest.raises(AttributeError): _ = mp5.get_patterns(r2, self.detector, 5) mp6 = kp.signals.EBSDMasterPattern(np.zeros((2, 11, 11))) with pytest.raises(AttributeError, match="Master pattern `phase` attribute"): _ = mp6.get_patterns(r2, self.detector, 5) mp7 = kp.signals.EBSDMasterPattern(np.zeros((10, 11, 11))) mp7.axes_manager[0].name = "energy" mp7.projection = "lambert" mp7.phase = Phase("!Ni", 220) with pytest.raises(AttributeError, match="For point groups without inversion"): _ = mp7.get_patterns(r2, self.detector, 5) # More than one PC is currently not supported so should fail d2 = kp.detectors.EBSDDetector( shape=(10, 10), px_size=50, pc=((0, 0, 15000), (0, 0, 15000)), convention="emsoft4", sample_tilt=70, ) with pytest.raises(NotImplementedError): _ = mp4.get_patterns(r2, d2, 5)
def test_get_patterns(self): # Ni Test EMSOFT_EBSD_FILE = os.path.join( DIR_PATH, "../../data/emsoft_ebsd/EBSD_TEST_Ni.h5") emsoft_key = load(EMSOFT_EBSD_FILE) emsoft_key = emsoft_key.data[0] angles = np.array((120, 45, 60)) r = Rotation.from_euler(np.radians(angles)) kp_mp = nickel_ebsd_master_pattern_small(projection="lambert", hemisphere="both") kp_pattern = kp_mp.get_patterns(rotations=r, detector=self.detector, energy=20, dtype_out=np.uint8) kp_pat = kp_pattern.data[0].compute() ncc1 = ncc(kp_pat, emsoft_key) ndp1 = ndp(kp_pat, emsoft_key) assert ncc1 >= 0.935 assert ndp1 >= 0.935 detector_shape = self.detector.shape r2 = Rotation.from_euler(((0, 0, 0), (1, 1, 1), (2, 2, 2))) mp_a = EBSDMasterPattern(np.zeros((2, 10, 11, 11))) print(mp_a.axes_manager) mp_a.axes_manager[0].name = "hemisphere" mp_a.axes_manager[1].name = "energy" mp_a.projection = "lambert" mp_a.phase = Phase("Ni", 225) out_a = mp_a.get_patterns(r2, self.detector, 5) assert isinstance(out_a, LazyEBSD) desired_data_shape = (3, ) + detector_shape[::-1] assert out_a.axes_manager.shape == desired_data_shape mp_b = EBSDMasterPattern(np.zeros((10, 11, 11))) mp_b.axes_manager[0].name = "energy" mp_b.projection = "lambert" mp_b.phase = Phase("Ni", 225) out_b = mp_b.get_patterns(r2, self.detector, 5) assert isinstance(out_b, LazyEBSD) assert out_b.axes_manager.shape == desired_data_shape mp_c = EBSDMasterPattern(np.zeros((11, 11))) mp_c.projection = "lambert" mp_c.phase = Phase("Ni", 225) out_c = mp_c.get_patterns(r2, self.detector, 5) out_c_2 = mp_c.get_patterns(r2, self.detector, 5, compute=True) assert isinstance(out_c, LazyEBSD) assert isinstance(out_c_2, EBSD) assert out_c.axes_manager.shape == desired_data_shape mp_c2 = EBSDMasterPattern(np.zeros((11, 11))) mp_c2.projection = "lambert" mp_c2.phase = Phase("!Ni", 220) with pytest.raises(AttributeError): mp_c2.get_patterns(r2, self.detector, 5) mp_d = EBSDMasterPattern(np.zeros((2, 11, 11))) with pytest.raises(NotImplementedError): mp_d.get_patterns(r2, self.detector, 5) mp_e = EBSDMasterPattern(np.zeros((10, 11, 11))) mp_e.axes_manager[0].name = "energy" mp_e.projection = "lambert" mp_e.phase = Phase("!Ni", 220) with pytest.raises(AttributeError): mp_e.get_patterns(r2, self.detector, 5) # More than one Projection center is currently not supported so # it should fail d2 = EBSDDetector( shape=(10, 10), px_size=50, pc=((0, 0, 15000), (0, 0, 15000)), convention="emsoft4", tilt=0, sample_tilt=70, ) with pytest.raises(NotImplementedError): mp_c.get_patterns(r2, d2, 5)
class EBSDMasterPattern(CommonImage, Signal2D): """Simulated Electron Backscatter Diffraction (EBSD) master pattern. This class extends HyperSpy's Signal2D class for EBSD master patterns. Methods inherited from HyperSpy can be found in the HyperSpy user guide. See the docstring of :class:`hyperspy.signal.BaseSignal` for a list of additional attributes. Attributes ---------- projection : str Which projection the pattern is in, "stereographic" or "lambert". hemisphere : str Which hemisphere the data contains: "north", "south" or "both". phase : orix.crystal_map.phase_list.Phase Phase describing the crystal structure used in the master pattern simulation. """ _signal_type = "EBSDMasterPattern" _alias_signal_types = ["ebsd_master_pattern", "master_pattern"] _lazy = False phase = Phase() projection = None hemisphere = None def __init__(self, *args, **kwargs): """Create an :class:`~kikuchipy.signals.EBSDMasterPattern` object from a :class:`hyperspy.signals.Signal2D` or a :class:`numpy.ndarray`. """ Signal2D.__init__(self, *args, **kwargs) self.phase = kwargs.pop("phase", Phase()) self.projection = kwargs.pop("projection", None) self.hemisphere = kwargs.pop("hemisphere", None) def get_patterns( self, rotations: Rotation, detector: EBSDDetector, energy: Union[int, float], dtype_out: type = np.float32, compute: bool = False, **kwargs, ) -> Union[EBSD, LazyEBSD]: """Return a dictionary of EBSD patterns projected onto a detector from a master pattern in the square Lambert projection :cite:`callahan2013dynamical`, for a set of crystal rotations relative to the EDAX TSL sample reference frame (RD, TD, ND) and a fixed detector-sample geometry. Parameters ---------- rotations Set of crystal rotations to get patterns from. The shape of this object, a maximum of two dimensions, determines the navigation shape of the output signal. detector EBSD detector describing the detector dimensions and the detector-sample geometry with a single, fixed projection/pattern center. energy Acceleration voltage, in kV, used to simulate the desired master pattern to create a dictionary from. dtype_out Data type of the returned patterns, by default np.float32. compute Whether to return a lazy result, by default False. For more information see :func:`~dask.array.Array.compute`. kwargs Keyword arguments passed to :func:`~kikuchipy.signals.util.get_chunking` to control the number of chunks the dictionary creation and the output data array is split into. Only `chunk_shape`, `chunk_bytes` and `dtype_out` (to `dtype`) are passed on. Returns ------- EBSD or LazyEBSD Signal with navigation and signal shape equal to the rotation object's and detector shape, respectively. Notes ----- If the master pattern phase has a non-centrosymmetric point group, both the northern and southern hemispheres must be provided. For more details regarding the reference frame visit the reference frame user guide at: https://kikuchipy.org/en/latest/reference_frames.html. """ if self.projection != "lambert": raise NotImplementedError( "Master pattern must be in the square Lambert projection" ) if len(detector.pc) > 1: raise NotImplementedError( "Detector must have exactly one projection center" ) # Get suitable chunks when iterating over the rotations. The # signal axes are not chunked. nav_shape = rotations.shape nav_dim = len(nav_shape) if nav_dim > 2: raise ValueError( "The rotations object can only have one or two dimensions, but " f"an object with {nav_dim} was passed" ) data_shape = nav_shape + detector.shape chunks = get_chunking( data_shape=data_shape, nav_dim=nav_dim, sig_dim=len(detector.shape), chunk_shape=kwargs.pop("chunk_shape", None), chunk_bytes=kwargs.pop("chunk_bytes", None), dtype=dtype_out, ) # Get the master pattern arrays created by a desired energy north_slice = () if "energy" in [i.name for i in self.axes_manager.navigation_axes]: energies = self.axes_manager["energy"].axis north_slice += ((np.abs(energies - energy)).argmin(),) south_slice = north_slice if self.hemisphere == "both": north_slice = (0,) + north_slice south_slice = (1,) + south_slice elif not self.phase.point_group.contains_inversion: raise AttributeError( "For crystals of point groups without inversion symmetry, like " f"the current {self.phase.point_group.name}, both hemispheres " "must be present in the master pattern signal" ) master_north = self.data[north_slice] master_south = self.data[south_slice] # Whether to rescale pattern intensities after projection rescale = False if dtype_out != np.float32: rescale = True # Get direction cosines for each detector pixel relative to the # source point dc = _get_direction_cosines(detector) # Get dask array from rotations r_da = da.from_array(rotations.data, chunks=chunks[:nav_dim] + (-1,)) # Which axes to drop and add when iterating over the rotations # dask array to produce the EBSD signal array, i.e. drop the # (4,)-shape quaternion axis and add detector shape axes, e.g. # (60, 60) if nav_dim == 1: drop_axis = 1 new_axis = (1, 2) else: # nav_dim == 2 drop_axis = 2 new_axis = (2, 3) # Project simulated patterns onto detector npx, npy = self.axes_manager.signal_shape scale = (npx - 1) / 2 simulated = r_da.map_blocks( _get_patterns_chunk, dc=dc, master_north=master_north, master_south=master_south, npx=npx, npy=npy, scale=scale, rescale=rescale, dtype_out=dtype_out, drop_axis=drop_axis, new_axis=new_axis, chunks=chunks, dtype=dtype_out, ) # Add crystal map and detector to keyword arguments kwargs = dict( xmap=CrystalMap( phase_list=PhaseList(self.phase), rotations=rotations, ), detector=detector, ) # Specify navigation and signal axes for signal initialization names = ["y", "x", "dy", "dx"] scales = np.ones(4) ndim = simulated.ndim if ndim == 3: names = names[1:] scales = scales[1:] axes = [ dict( size=data_shape[i], index_in_array=i, name=names[i], scale=scales[i], offset=0.0, units="px", ) for i in range(ndim) ] if compute: with ProgressBar(): print( f"Creating a dictionary of {nav_shape} simulated patterns:", file=sys.stdout, ) patterns = simulated.compute() out = EBSD(patterns, axes=axes, **kwargs) else: out = LazyEBSD(simulated, axes=axes, **kwargs) return out
def nickel_phase(nickel_structure): """A orix.crystal_map.Phase with a Nickel crystal structure and symmetry operations. """ return Phase(name="ni", structure=nickel_structure, space_group=225)
class TestCrystallographicComputations: @pytest.mark.parametrize( "hkl, desired_uvw", [ ([1, 1, 1], np.array([]).reshape((0, 3))), ([[1, 1, 1], [2, 2, 2]], np.array([]).reshape((0, 3))), ([[1, 1, 1], [1, 1, -1]], [[-1, 1, 0], [1, -1, 0]]), ], ) def test_get_uvw_from_hkl(self, hkl, desired_uvw): """Desired uvw from the cross product of hkl.""" assert np.allclose(_get_uvw_from_hkl(hkl), desired_uvw) @pytest.mark.parametrize( ("hkl, desired_family_keys, desired_family_values, desired_indices, " "reduce"), [ ([1, 1, 1], [[1, 1, 1]], [1, 1, 1], [0], False), ([1, 1, 1], [[1, 1, 1]], [1, 1, 1], [0], True), ( [[1, 1, 1], [2, 0, 0]], [[1, 1, 1], [2, 0, 0]], [[[1, 1, 1]], [[2, 0, 0]]], [[0], [1]], False, ), ( ReciprocalLatticePoint(phase=Phase(space_group=225), hkl=[1, 1, 1]).symmetrise().hkl.data, [1, 1, 1], [[ [1, 1, 1], [-1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], [-1, -1, -1], ]], [np.arange(8)], False, ), ( ReciprocalLatticePoint(phase=Phase(space_group=225), hkl=[[1, 1, 1], [2, 0, 0] ]).symmetrise().hkl.data, [[1, 1, 1], [2, 0, 0]], [ [ [1, 1, 1], [-1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], [-1, -1, -1], ], [ [2, 0, 0], [0, 2, 0], [-2, 0, 0], [0, -2, 0], [0, 0, 2], [0, 0, -2], ], ], [np.arange(8).tolist(), np.arange(8, 14).tolist()], False, ), ( ReciprocalLatticePoint(phase=Phase(space_group=225), hkl=[[1, 1, 1], [2, 2, 2] ]).symmetrise().hkl.data, [1, 1, 1], [[ [1, 1, 1], [-1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], [-1, -1, -1], [2, 2, 2], [-2, 2, 2], [-2, -2, 2], [2, -2, 2], [2, -2, -2], [2, 2, -2], [-2, 2, -2], [-2, -2, -2], ]], [np.arange(16)], True, ), ], ) def test_get_hkl_family(self, hkl, desired_family_keys, desired_family_values, desired_indices, reduce): """Desired sets of families and indices.""" families, families_idx = _get_hkl_family(hkl, reduce=reduce) for i, (k, v) in enumerate(families.items()): assert np.allclose(k, desired_family_keys[i]) assert np.allclose(v, desired_family_values[i]) assert np.allclose(families_idx[k], desired_indices[i]) @pytest.mark.parametrize( "highest_hkl, color_cycle, desired_hkl_colors", [ ([1, 1, 1], ["C0", "C1"], [[1, 1, 1], [0.12, 0.47, 0.71]]), ( [2, 2, 2], ["g", "b"], [ [[1, 1, 1], [0, 0.5, 0]], [[2, 0, 0], [0, 0, 1]], [[2, 2, 0], [0, 0.5, 0]], [[2, 2, 2], [0, 0.5, 0]], ], ), ( [2, 2, 2], None, [ [[1, 1, 1], [1, 0, 0]], [[2, 0, 0], [1, 1, 0]], [[2, 2, 0], [0, 1, 0]], [[2, 2, 2], [1, 0, 0]], ], ), ], ) def test_get_colors_for_allowed_bands(self, nickel_phase, highest_hkl, color_cycle, desired_hkl_colors): """Desired colors for bands.""" hkl_colors = _get_colors_for_allowed_bands(phase=nickel_phase, highest_hkl=highest_hkl, color_cycle=color_cycle) assert np.allclose(hkl_colors, desired_hkl_colors, atol=1e-2) def test_get_colors_for_allowed_bands_999(self, nickel_phase): """Not passing `highest_hkl` works fine.""" hkl_colors = _get_colors_for_allowed_bands(phase=nickel_phase) assert np.shape(hkl_colors) == (69, 2, 3)
def test_get_allowed_without_space_group_raises(self): phase = Phase(point_group="432") rlp = ReciprocalLatticePoint(phase=phase, hkl=[1, 1, 1]) with pytest.raises(ValueError, match=f"The phase {phase} must have a"): _ = rlp.allowed
def test_init_without_point_group_raises(self): phase = Phase() with pytest.raises(ValueError, match=f"The phase {phase} must have a"): _ = ReciprocalLatticePoint(phase=phase, hkl=[1, 1, 1])
class EBSDMasterPattern(CommonImage, Signal2D): """Simulated Electron Backscatter Diffraction (EBSD) master pattern. This class extends HyperSpy's Signal2D class for EBSD master patterns. Methods inherited from HyperSpy can be found in the HyperSpy user guide. See the docstring of :class:`hyperspy.signal.BaseSignal` for a list of additional attributes. Attributes ---------- projection : str Which projection the pattern is in, "stereographic" or "lambert". hemisphere : str Which hemisphere the data contains: "north", "south" or "both". phase : orix.crystal_map.phase_list.Phase Phase describing the crystal structure used in the master pattern simulation. """ _signal_type = "EBSDMasterPattern" _alias_signal_types = ["ebsd_master_pattern", "master_pattern"] _lazy = False # ---------------------- Custom properties ----------------------- # phase = Phase() projection = None hemisphere = None def __init__(self, *args, **kwargs): """Create an :class:`~kikuchipy.signals.EBSDMasterPattern` instance from a :class:`hyperspy.signals.Signal2D` or a :class:`numpy.ndarray`. See the docstring of :class:`hyperspy.signal.BaseSignal` for optional input parameters. """ Signal2D.__init__(self, *args, **kwargs) self.phase = kwargs.pop("phase", Phase()) self.projection = kwargs.pop("projection", None) self.hemisphere = kwargs.pop("hemisphere", None) def get_patterns( self, rotations: Rotation, detector: EBSDDetector, energy: Union[int, float], dtype_out: Union[type, np.dtype] = np.float32, compute: bool = False, **kwargs, ) -> Union[EBSD, LazyEBSD]: """Return a dictionary of EBSD patterns projected onto a detector from a master pattern in the square Lambert projection :cite:`callahan2013dynamical`, for a set of crystal rotations relative to the EDAX TSL sample reference frame (RD, TD, ND) and a fixed detector-sample geometry. Parameters ---------- rotations Crystal rotations to get patterns from. The shape of this instance, a maximum of two dimensions, determines the navigation shape of the output signal. detector EBSD detector describing the detector dimensions and the detector-sample geometry with a single, fixed projection/pattern center. energy Acceleration voltage, in kV, used to simulate the desired master pattern to create a dictionary from. If only a single energy is present in the signal, this will be returned no matter its energy. dtype_out Data type of the returned patterns, by default np.float32. compute Whether to return a lazy result, by default False. For more information see :func:`~dask.array.Array.compute`. kwargs Keyword arguments passed to :func:`~kikuchipy.signals.util.get_chunking` to control the number of chunks the dictionary creation and the output data array is split into. Only `chunk_shape`, `chunk_bytes` and `dtype_out` (to `dtype`) are passed on. Returns ------- EBSD or LazyEBSD Signal with navigation and signal shape equal to the rotation instance and detector shape, respectively. Notes ----- If the master pattern phase has a non-centrosymmetric point group, both the northern and southern hemispheres must be provided. For more details regarding the reference frame visit the reference frame user guide. """ self._is_suitable_for_projection(raise_if_not=True) if len(detector.pc) > 1: raise NotImplementedError( "Detector must have exactly one projection center") # Get suitable chunks when iterating over the rotations. Signal # axes are not chunked. nav_shape = rotations.shape nav_dim = len(nav_shape) if nav_dim > 2: raise ValueError( "`rotations` can only have one or two dimensions, but an instance with " f"{nav_dim} dimensions was passed") data_shape = nav_shape + detector.shape chunks = get_chunking( data_shape=data_shape, nav_dim=nav_dim, sig_dim=len(detector.shape), chunk_shape=kwargs.pop("chunk_shape", None), chunk_bytes=kwargs.pop("chunk_bytes", None), dtype=dtype_out, ) # Whether to rescale pattern intensities after projection if dtype_out != self.data.dtype: rescale = True if isinstance(dtype_out, np.dtype): dtype_out = dtype_out.type out_min, out_max = dtype_range[dtype_out] else: rescale = False # Cannot be None due to Numba, so they are set to something # here. Values aren't used unless `rescale` is True. out_min, out_max = 1, 2 # Get direction cosines for each detector pixel relative to the # source point direction_cosines = _get_direction_cosines_for_single_pc_from_detector( detector) # Get dask array from rotations rot_da = da.from_array(rotations.data, chunks=chunks[:nav_dim] + (-1, )) # Which axes to drop and add when iterating over the rotations # dask array to produce the EBSD signal array, i.e. drop the # (4,)-shape quaternion axis and add detector shape axes, e.g. # (60, 60) if nav_dim == 1: drop_axis = 1 new_axis = (1, 2) else: # nav_dim == 2 drop_axis = 2 new_axis = (2, 3) master_north, master_south = self._get_master_pattern_arrays_from_energy( energy) # Project simulated patterns onto detector npx, npy = self.axes_manager.signal_shape scale = (npx - 1) / 2 # TODO: Use dask.delayed instead? simulated = rot_da.map_blocks( _project_patterns_from_master_pattern, direction_cosines=direction_cosines, master_north=master_north, master_south=master_south, npx=int(npx), npy=int(npy), scale=float(scale), dtype_out=dtype_out, rescale=rescale, out_min=out_min, out_max=out_max, drop_axis=drop_axis, new_axis=new_axis, chunks=chunks, dtype=dtype_out, ) # Add crystal map and detector to keyword arguments kwargs = dict( xmap=CrystalMap(phase_list=PhaseList(self.phase), rotations=rotations), detector=detector, ) # Specify navigation and signal axes for signal initialization names = ["y", "x", "dy", "dx"] scales = np.ones(4) ndim = simulated.ndim if ndim == 3: names = names[1:] scales = scales[1:] axes = [ dict( size=data_shape[i], index_in_array=i, name=names[i], scale=scales[i], offset=0.0, units="px", ) for i in range(ndim) ] if compute: patterns = np.zeros(shape=simulated.shape, dtype=simulated.dtype) with ProgressBar(): print( f"Creating a dictionary of {nav_shape} simulated patterns:", file=sys.stdout, ) simulated.store(patterns, compute=True) out = EBSD(patterns, axes=axes, **kwargs) else: out = LazyEBSD(simulated, axes=axes, **kwargs) gc.collect() return out # ------ Methods overwritten from hyperspy.signals.Signal2D ------ # def deepcopy(self): new = super().deepcopy() new.phase = self.phase.deepcopy() new.projection = copy.deepcopy(self.projection) new.hemisphere = copy.deepcopy(self.hemisphere) return new # ------------------------ Private methods ----------------------- # def _get_master_pattern_arrays_from_energy( self, energy: Union[int, float]) -> Tuple[np.ndarray, np.ndarray]: """Return northern and southern master patterns created with a single, given energy. Parameters ---------- energy Acceleration voltage in kV. If only a single energy is present in the signal, this will be returned no matter its energy. Returns ------- master_north, master_south Northern and southern hemispheres of master pattern. """ if "energy" in [i.name for i in self.axes_manager.navigation_axes]: master_patterns = self.inav[float(energy)].data else: # Assume a single energy master_patterns = self.data if self.hemisphere == "both": master_north, master_south = master_patterns else: master_north = master_south = master_patterns return master_north, master_south def _is_suitable_for_projection(self, raise_if_not: bool = False): """Check whether the master pattern is suitable for projection onto an EBSD detector and return a bool or raise an error message if desired. """ suitable = True error = None if self.projection != "lambert": error = NotImplementedError( "Master pattern must be in the square Lambert projection") suitable = False pg = self.phase.point_group if pg is None: error = AttributeError( "Master pattern `phase` attribute must have a valid point group" ) suitable = False elif self.hemisphere != "both" and not pg.contains_inversion: error = AttributeError( "For point groups without inversion symmetry, like the current " f"{pg.name}, both hemispheres must be present in the master pattern " "signal") suitable = False if not suitable and raise_if_not: raise error else: return suitable
def file_reader(filename, refined=False, **kwargs): """Return a :class:`~orix.crystal_map.crystal_map.CrystalMap` object from a file in EMsoft's dictionary indexing dot product file format. Parameters ---------- filename : str Path and file name. refined : bool, optional Whether to return refined orientations (default is False). kwargs Keyword arguments passed to :func:`h5py.File`. Returns ------- CrystalMap """ mode = kwargs.pop("mode", "r") f = File(filename, mode=mode, **kwargs) # Get groups for convenience ebsd_group = f["Scan 1/EBSD"] data_group = ebsd_group["Data"] header_group = ebsd_group["Header"] phase_group = header_group["Phase/1"] # Get map shape and step sizes ny = header_group["nRows"][:][0] nx = header_group["nColumns"][:][0] step_y = header_group["Step Y"][:][0] map_size = ny * nx # Some of the data needed to create a CrystalMap object phase_name, point_group, structure = _get_phase(phase_group) data_dict = { # Get map coordinates ("Y Position" data set is not correct in EMsoft as of # 2020-04, see: # https://github.com/EMsoft-org/EMsoft/blob/7762e1961508fe3e71d4702620764ceb98a78b9e/Source/EMsoftHDFLib/EMh5ebsd.f90#L1093) "x": data_group["X Position"][:], # y = data_group["Y Position"][:] "y": np.sort(np.tile(np.arange(ny) * step_y, nx)), # Get phase IDs "phase_id": data_group["Phase"][:], # Get phase name, point group and structure (lattice) "phase_list": PhaseList( Phase(name=phase_name, point_group=point_group, structure=structure)), "scan_unit": "um", } # Get rotations if refined: euler = data_group["RefinedEulerAngles"][:] # Radians else: # Get n top matches for each pixel top_match_idx = data_group["TopMatchIndices"][:][:map_size] - 1 dictionary_size = data_group["FZcnt"][:][0] # Degrees dictionary_euler = data_group[ "DictionaryEulerAngles"][:][:dictionary_size] dictionary_euler = np.deg2rad(dictionary_euler) euler = dictionary_euler[top_match_idx, :] data_dict["rotations"] = Rotation.from_euler(euler) # Get number of top matches kept per data point n_top_matches = f["NMLparameters/EBSDIndexingNameListType/nnk"][:][0] data_dict["prop"] = _get_properties( data_group=data_group, n_top_matches=n_top_matches, map_size=map_size, ) f.close() return CrystalMap(**data_dict)
def test_allowed(self, space_group, hkl, centering, desired_allowed): rlp = ReciprocalLatticePoint(phase=Phase(space_group=space_group), hkl=hkl) assert rlp.phase.space_group.short_name[0] == centering assert np.allclose(rlp.allowed, desired_allowed)
import numpy as np from orix.crystal_map import Phase import pytest from diffsims.structure_factor import ( find_asymmetric_positions, get_kinematical_structure_factor, get_doyleturner_structure_factor, get_refraction_corrected_wavelength, ) # Debye-Waller factor in Å^-2: Biosequiv = 8 * np.pi ** 2 * Uisoequiv nickel = Phase( space_group=225, structure=Structure( lattice=Lattice(3.5236, 3.5236, 3.5236, 90, 90, 90), atoms=[Atom(xyz=[0, 0, 0], atype="Ni", Uisoequiv=0.006332)], ), ) ferrite = Phase( space_group=229, structure=Structure( lattice=Lattice(2.8665, 2.8665, 2.8665, 90, 90, 90), atoms=[ Atom(xyz=[0, 0, 0], atype="Fe", Uisoequiv=0.006332), Atom(xyz=[0.5, 0.5, 0.5], atype="Fe", Uisoequiv=0.006332), ], ), ) sic4h = Phase( space_group=186,