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 compute_refine_orientation_projection_center_results( results: list, detector, xmap: CrystalMap, master_pattern, ): """Compute the results from :meth:`~kikuchipy.signals.EBSD.refine_orientation_projection_center` and return the :class:`~orix.crystal_map.CrystalMap` and :class:`~kikuchipy.detectors.EBSDDetector`. Parameters ---------- results Results returned from `refine_orientation_projection_center()`, which is a list of :class:`~dask.delayed.Delayed`. detector : ~kikuchipy.detectors.EBSDDetector Detector passed to `refine_orientation_projection_center()` to obtain `results`. xmap Crystal map passed to `refine_orientation_projection_center()` to obtain `results`. master_pattern : ~kikuchipy.signals.EBSDMasterPattern Master pattern passed to `refine_orientation_projection_center()` to obtain `results`. Returns ------- xmap_refined : :class:`~orix.crystal_map.CrystalMap` Crystal map with refined orientations and scores. new_detector : :class:`~kikuchipy.detectors.EBSDDetector` EBSD detector with refined projection center parameters. """ n_patterns = len(results) nav_shape = xmap.shape with ProgressBar(): print( f"Refining {n_patterns} orientation(s) and projection center(s):", file=sys.stdout, ) computed_results = dask.compute(*results) computed_results = np.array(computed_results) # (n, score, phi1, Phi, phi2, PCx, PCy, PCz) xmap_refined = CrystalMap( rotations=Rotation.from_euler(computed_results[:, 1:4]), phase_id=np.zeros(n_patterns), x=xmap.x, y=xmap.y, phase_list=PhaseList(phases=master_pattern.phase), prop=dict(scores=computed_results[:, 0]), scan_unit=xmap.scan_unit, ) new_detector = detector.deepcopy() new_detector.pc = computed_results[:, 4:].reshape(nav_shape + (3, )) return xmap_refined, new_detector
def dict2phaselist(dictionary): """Get a :class:`~orix.crystal_map.phase_list.PhaseList` object from a dictionary. Parameters ---------- dictionary : dict Dictionary with phase list information. Returns ------- PhaseList """ dictionary = copy.deepcopy(dictionary) return PhaseList(phases={int(k): dict2phase(v) for k, v in dictionary.items()})
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 compute_refine_orientation_results(results: list, xmap: CrystalMap, master_pattern): """Compute the results from :meth:`~kikuchipy.signals.EBSD.refine_orientation` and return the :class:`~orix.crystal_map.CrystalMap`. Parameters ---------- results Results returned from `refine_orientation()`, which is a list of :class:`~dask.delayed.Delayed`. xmap Crystal map passed to `refine_orientation()` to obtain `results`. master_pattern : ~kikuchipy.signals.EBSDMasterPattern Master pattern passed to `refine_orientation()` to obtain `results`. Returns ------- refined_xmap : :class:`~orix.crystal_map.CrystalMap` Crystal map with refined orientations and scores. """ n_patterns = len(results) with ProgressBar(): print(f"Refining {n_patterns} orientation(s):", file=sys.stdout) computed_results = dask.compute(*results) # (n, score, phi1, Phi, phi2) computed_results = np.array(computed_results) xmap_refined = CrystalMap( rotations=Rotation.from_euler(computed_results[:, 1:]), phase_id=np.zeros(n_patterns), x=xmap.x, y=xmap.y, phase_list=PhaseList(phases=master_pattern.phase), prop=dict(scores=computed_results[:, 0]), scan_unit=xmap.scan_unit, ) return xmap_refined
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 file_reader( filename: str, scan_size: Union[None, int, Tuple[int, ...]] = None, lazy: bool = False, **kwargs, ) -> List[dict]: """Read dynamically simulated electron backscatter diffraction patterns from EMsoft's format produced by their EMEBSD.f90 program. Parameters ---------- filename Full file path of the HDF file. scan_size Scan size in number of patterns in width and height. lazy Open the data lazily without actually reading the data from disk until requested. Allows opening datasets larger than available memory. Default is False. kwargs : Keyword arguments passed to h5py.File. Returns ------- signal_dict_list: list of dicts Data, axes, metadata and original metadata. """ mode = kwargs.pop("mode", "r") f = File(filename, mode=mode, **kwargs) _check_file_format(f) # Read original metadata omd = hdf5group2dict(f["/"], data_dset_names=["EBSDPatterns"], recursive=True) # Set metadata and original metadata dictionaries md = _get_metadata(omd) md.update({ "Signal": { "signal_type": "EBSD", "record_by": "image" }, "General": { "title": f.filename.split("/")[-1].split(".")[0], "original_filename": f.filename.split("/")[-1], }, }) scan = {"metadata": md, "original_metadata": omd} # Read patterns dataset = f["EMData/EBSD/EBSDPatterns"] if lazy: chunks = "auto" if dataset.chunks is None else dataset.chunks patterns = da.from_array(dataset, chunks=chunks) else: patterns = np.asanyarray(dataset) # Reshape data if desired sy = omd["NMLparameters"]["EBSDNameList"]["numsy"] sx = omd["NMLparameters"]["EBSDNameList"]["numsx"] if scan_size is not None: if isinstance(scan_size, int): new_shape = (scan_size, sy, sx) else: new_shape = scan_size + (sy, sx) patterns = patterns.reshape(new_shape) scan["data"] = patterns # Set navigation and signal axes pixel_size = omd["NMLparameters"]["EBSDNameList"]["delta"] ndim = patterns.ndim units = ["px", "um", "um"] names = ["x", "dy", "dx"] scales = np.array([1, pixel_size, pixel_size]) if ndim == 4: units = ["px"] + units names = ["y"] + names scales = np.append([1], scales) scan["axes"] = [{ "size": patterns.shape[i], "index_in_array": i, "name": names[i], "scale": scales[i], "offset": 0, "units": units[i], } for i in range(patterns.ndim)] # Get crystal map phase = _crystaldata2phase(hdf5group2dict(f["CrystalData"])) xtal_fname = f["EMData/EBSD/xtalname"][()][0].decode().split("/")[-1] phase.name, _ = os.path.splitext(xtal_fname) scan["xmap"] = CrystalMap( rotations=Rotation.from_euler(f["EMData/EBSD/EulerAngles"][()]), phase_list=PhaseList(phase), ) if not lazy: f.close() return [scan]
def file_reader(filename): """Return a :class:`~orix.crystal_map.crystal_map.CrystalMap` object from a file in EDAX TLS's .ang format. The map in the input is assumed to be 2D. Many vendors produce an .ang file. Supported vendors are * EDAX TSL * NanoMegas ASTAR Index * EMsoft (from program `EMdpmerge`) * orix All points satisfying the following criteria are classified as not indexed: * EDAX TSL: confidence index == -1 Parameters ---------- filename : str Path and file name. Returns ------- CrystalMap """ # Get file header with open(filename) as f: header = _get_header(f) # Get phase names and crystal symmetries from header (potentially empty) phase_ids, phase_names, symmetries, lattice_constants = _get_phases_from_header( header) structures = [] for name, abcABG in zip(phase_names, lattice_constants): structures.append(Structure(title=name, lattice=Lattice(*abcABG))) # Read all file data file_data = np.loadtxt(filename) # Get vendor and column names n_rows, n_cols = file_data.shape vendor, column_names = _get_vendor_columns(header, n_cols) # Data needed to create a CrystalMap object data_dict = { "euler1": None, "euler2": None, "euler3": None, "x": None, "y": None, "phase_id": None, "prop": {}, } for column, name in enumerate(column_names): if name in data_dict.keys(): data_dict[name] = file_data[:, column] else: data_dict["prop"][name] = file_data[:, column] # Add phase list to dictionary data_dict["phase_list"] = PhaseList( names=phase_names, point_groups=symmetries, structures=structures, ids=phase_ids, ) # Set which data points are not indexed if vendor in ["orix", "tsl"]: data_dict["phase_id"][np.where(data_dict["prop"]["ci"] == -1)] = -1 # TODO: Add not-indexed convention for INDEX ASTAR # Set scan unit if vendor in ["tsl", "emsoft"]: scan_unit = "um" else: # NanoMegas scan_unit = "nm" data_dict["scan_unit"] = scan_unit # Create rotations data_dict["rotations"] = Rotation.from_euler( np.column_stack((data_dict.pop("euler1"), data_dict.pop("euler2"), data_dict.pop("euler3"))), ) return CrystalMap(**data_dict)
def merge_crystal_maps( crystal_maps: List[CrystalMap], mean_n_best: int = 1, metric: Union[str, SimilarityMetric] = None, scores_prop: str = "scores", simulation_indices_prop: Optional[str] = None, ): """Merge a list of at least two single phase :class:`~orix.crystal_map.crystal_map.CrystalMap` with a 1D or 2D navigation shape into one multi phase map. It is required that all maps have the same number of rotations and scores (and simulation indices if applicable) per point. Parameters ---------- crystal_maps : list of\ :class:`~orix.crystal_map.crystal_map.CrystalMap` A list of crystal maps with simulated indices and scores among their properties. mean_n_best : int, optional Number of best metric results to take the mean of before comparing. Default is 1. metric : str or SimilarityMetric, optional Similarity metric, default is None. scores_prop : str, optional Name of scores array in the crystal maps' properties. Default is "scores". simulation_indices_prop : str, optional Name of simulated indices array in the crystal maps' properties. If None (default), the merged crystal map will not contain an array of merged simulation indices from the input crystal maps' properties. If a string, there must be as many simulation indices per point as there are scores. Returns ------- merged_xmap : ~orix.crystal_map.crystal_map.CrystalMap A crystal map where the rotation of the phase with the best matching score(s) is assigned to each point. The best matching scores, merge sorted, are added to its properties with a name equal to whatever passed to `scores_prop` with "merged" as a suffix. If `simulation_indices_prop` is passed, the best matching simulation indices are added in the same way as the scores. Notes ----- `mean_n_best` can be given with a negative sign if `metric` is not given, in order to choose the lowest valued metric results. """ map_shapes = [xmap.shape for xmap in crystal_maps] if not np.sum(abs(np.diff(map_shapes, axis=0))) == 0: raise ValueError( "All crystal maps must have the same navigation shape") rot_per_point_per_map = [xmap.rotations_per_point for xmap in crystal_maps] if not all(np.diff(rot_per_point_per_map) == 0): raise ValueError( "All crystal maps must have the same number of rotations and scores" " per point.") if metric is None: sign = copysign(1, mean_n_best) mean_n_best = abs(mean_n_best) else: sign = _SIMILARITY_METRICS.get(metric, metric).sign # Notation used in the comments below: # - M: number of map points # - N: number of scores per point # - I: number of simulation indices per point # - K: number of maps to merge # Shape of the combined (unsorted) scores array, and the total # number of scores per point. Shape: (M, N, K) or (M, K) if only one # score is available (e.g. refined dot products from EMsoft) (comb_shape, n_scores_per_point) = _get_combined_scores_shape( crystal_maps=crystal_maps, scores_prop=scores_prop) # Combined (unsorted) scores array of shape (M, N, K) or (M, K) combined_scores = np.dstack( [xmap.prop[scores_prop] for xmap in crystal_maps]) combined_scores = combined_scores.reshape(comb_shape) # Best score in each map point if n_scores_per_point > 1: # (M, N, K) best_scores = np.mean(combined_scores[:, :mean_n_best], axis=1) else: # (M, K) best_scores = combined_scores # Phase of best score in each map point phase_id = np.argmax(sign * best_scores, axis=1) # Get the new CrystalMap's rotations, scores and indices, restricted # to one phase per point (uncombined) new_rotations = Rotation(np.zeros_like(crystal_maps[0].rotations.data)) new_scores = np.zeros_like(crystal_maps[0].prop[scores_prop]) if simulation_indices_prop is not None: new_indices = np.zeros_like( crystal_maps[0].prop[simulation_indices_prop]) phase_list = PhaseList() for i, xmap in enumerate(crystal_maps): mask = phase_id == i new_rotations[mask] = xmap.rotations[mask] new_scores[mask] = xmap.prop[scores_prop][mask] if simulation_indices_prop is not None: new_indices[mask] = xmap.prop[simulation_indices_prop][mask] if np.sum(mask) != 0: current_id = xmap.phases_in_data.ids[0] phase = xmap.phases_in_data[current_id].deepcopy() try: phase_list.add(phase) except ValueError: name = phase.name warnings.warn( f"There are duplicates of phase {name}, will therefore " f"rename this phase's name to {name + str(i)} in the merged" " PhaseList", ) phase.name = name + str(i) phase_list.add(phase) # To get the combined, best, sorted scores and simulation indices # from all maps (phases), we collapse the second and (potentially) # third axis to get (M, N * K) or (M, K) mergesort_shape = (comb_shape[0], np.prod(comb_shape[1:])) comb_scores_reshaped = combined_scores.reshape(mergesort_shape) best_sorted_idx = np.argsort(sign * -comb_scores_reshaped, kind="mergesort", axis=1) # Best, sorted scores in all maps (for all phases) per point merged_best_scores = np.take_along_axis(comb_scores_reshaped, best_sorted_idx, axis=-1) # Set up merged map's properties props = { scores_prop: new_scores, f"merged_{scores_prop}": merged_best_scores, } if simulation_indices_prop is not None: # Combined (unsorted) simulation indices array of shape # (M, N, K) or (M, K), accounting for the case where there are # more simulation indices per point than scores (e.g. refined # dot products from EMsoft) comb_sim_idx = np.dstack( [xmap.prop[simulation_indices_prop] for xmap in crystal_maps]) if comb_sim_idx.size != np.prod(mergesort_shape): raise ValueError( "Cannot merge maps with more simulation indices than scores per" " point.") # To enable calculation of an orientation similarity map from # the combined, sorted simulation indices array, we must make # the indices unique across all maps for i in range(1, comb_sim_idx.shape[-1]): increment = (abs(comb_sim_idx[..., i - 1].max() - comb_sim_idx[..., i].min()) + 1) comb_sim_idx[..., i] += increment # Collapse axes as for the combined scores array above comb_sim_idx = comb_sim_idx.reshape(mergesort_shape) # Best, sorted simulation indices in all maps (for all phases) # per point merged_simulated_indices = np.take_along_axis(comb_sim_idx, best_sorted_idx, axis=-1) # Finally, add to properties props[simulation_indices_prop] = new_indices props[f"merged_{simulation_indices_prop}"] = merged_simulated_indices return CrystalMap( rotations=new_rotations, phase_id=phase_id, phase_list=phase_list, x=crystal_maps[0].x, y=crystal_maps[0].y, z=crystal_maps[0].z, prop=props, scan_unit=crystal_maps[0].scan_unit, )
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 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