From 06e9e6a379c7691125369a84804378cba3533ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 1 Dec 2024 21:20:40 +0100 Subject: [PATCH] Add reader for Oxford Instruments master patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- src/kikuchipy/io/_io.py | 2 +- .../plugins/oxford_master_pattern/__init__.py | 23 ++ .../oxford_master_pattern/__init__.pyi | 20 ++ .../io/plugins/oxford_master_pattern/_api.py | 243 ++++++++++++++++++ .../oxford_master_pattern/specification.yaml | 9 + 5 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 src/kikuchipy/io/plugins/oxford_master_pattern/__init__.py create mode 100644 src/kikuchipy/io/plugins/oxford_master_pattern/__init__.pyi create mode 100644 src/kikuchipy/io/plugins/oxford_master_pattern/_api.py create mode 100644 src/kikuchipy/io/plugins/oxford_master_pattern/specification.yaml diff --git a/src/kikuchipy/io/_io.py b/src/kikuchipy/io/_io.py index 55e083d5..ccd1db78 100644 --- a/src/kikuchipy/io/_io.py +++ b/src/kikuchipy/io/_io.py @@ -174,7 +174,7 @@ def _dict2signal( record_by = md["Signal"]["record_by"] if record_by != "image": raise ValueError( - "kikuchipy only supports `record_by = image`, not " f"{record_by}" + f"kikuchipy only supports `record_by = image`, not {record_by}" ) del md["Signal"]["record_by"] if "Signal" in md and "signal_type" in md["Signal"]: diff --git a/src/kikuchipy/io/plugins/oxford_master_pattern/__init__.py b/src/kikuchipy/io/plugins/oxford_master_pattern/__init__.py new file mode 100644 index 00000000..2f857066 --- /dev/null +++ b/src/kikuchipy/io/plugins/oxford_master_pattern/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2019-2024 The kikuchipy developers +# +# This file is part of kikuchipy. +# +# kikuchipy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kikuchipy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with kikuchipy. If not, see . + +import lazy_loader + +__getattr__, __dir__, __all__ = lazy_loader.attach_stub(__name__, __file__) + + +del lazy_loader diff --git a/src/kikuchipy/io/plugins/oxford_master_pattern/__init__.pyi b/src/kikuchipy/io/plugins/oxford_master_pattern/__init__.pyi new file mode 100644 index 00000000..65a3a663 --- /dev/null +++ b/src/kikuchipy/io/plugins/oxford_master_pattern/__init__.pyi @@ -0,0 +1,20 @@ +# Copyright 2019-2024 The kikuchipy developers +# +# This file is part of kikuchipy. +# +# kikuchipy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kikuchipy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with kikuchipy. If not, see . + +from ._api import file_reader + +__all__ = ["file_reader"] diff --git a/src/kikuchipy/io/plugins/oxford_master_pattern/_api.py b/src/kikuchipy/io/plugins/oxford_master_pattern/_api.py new file mode 100644 index 00000000..97b4455b --- /dev/null +++ b/src/kikuchipy/io/plugins/oxford_master_pattern/_api.py @@ -0,0 +1,243 @@ +# Copyright 2019-2024 The kikuchipy developers +# +# This file is part of kikuchipy. +# +# kikuchipy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kikuchipy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with kikuchipy. If not, see . + +"""Reader of EBSD master pattern simulations from an Oxford Instruments +SDF5 (HDF5) file. +""" + +from pathlib import Path +from typing import Literal + +import dask.array as da +from diffpy.structure import Lattice, Structure +import h5py +import numpy as np +from orix.crystal_map import Phase + +from kikuchipy._utils.vector import ValidHemispheres, parse_hemisphere +from kikuchipy.io.plugins._h5ebsd import _hdf5group2dict +from kikuchipy.io.plugins.emsoft_ebsd_master_pattern._api import HEMISPHERE_ARG + +ValidSimulationTypes = Literal["dynamical", "twobeam", "kinematic"] + + +class OxfordMasterPatternReader: + def __init__( + self, + filename: str | Path, + energy: range | None = None, + hemisphere: ValidHemispheres = "both", + simulation: ValidSimulationTypes = "dynamical", + lazy: bool = False, + ) -> None: + self.filename = Path(filename) + self.energy = energy + self.hemisphere = parse_hemisphere(hemisphere) + self.simulation = self.parse_simulation(simulation) + self.lazy = lazy + + @staticmethod + def check_file_format(file: h5py.File) -> None: + if not "Proprietary/Source Info" in file: + raise IOError(f"{file.filename!r} is not an Oxford Instruments SDF5 file") + + def get_axes(self, data_shape: tuple[int, ...]) -> list[dict]: + sy, sx = data_shape[-2:] + names = ["height", "width"] + units = ["px", "px"] + offsets = [-sy // 2, -sx // 2] + if self.hemisphere == "both": + names.insert(0, "hemisphere") + offsets.insert(0, 0) + units.insert(0, "") + axes = [] + for i in range(len(data_shape)): + axis = { + "size": data_shape[i], + "index_in_array": i, + "name": names[i], + "scale": 1, + "offset": offsets[i], + "units": units[i], + } + axes.append(axis) + return axes + + def parse_data(self, group: h5py.Group) -> dict: + d = _hdf5group2dict(group["Reflectors"]) + data_group = group[f"Master/{self.simulation}"] + data_kwargs = {} + if self.lazy: + data_read_func = da.from_array + data_stack_func = da.stack + data_kwargs["chunks"] = "auto" + else: + data_read_func = np.asanyarray + data_stack_func = np.stack + if self.hemisphere == "upper": + data = data_read_func(data_group["Upper"], **data_kwargs) + elif self.hemisphere == "lower": + data = data_read_func(data_group["Lower"], **data_kwargs) + else: + upper = data_read_func(data_group["Upper"], **data_kwargs) + lower = data_read_func(data_group["Lower"], **data_kwargs) + data = data_stack_func([upper, lower], axis=0) + return { + "data": data, + "reflectors": { + "extinction_distances": d["Extinction Distances"], + "hkl": d["HKL"], + "lattice_spacing": d["Lattice Spacing"], + "normal_directions": d["Normal Directions"], + "relative_intensities": d["Relative Intensities"], + }, + } + + def parse_header(self, group: h5py.Group) -> dict: + d = _hdf5group2dict(group) + phase_info = self.parse_phase_info(group["Phase Info"]) + return { + "beam_energy": d["Beam Voltage"], + "debye_waller_factor": d["Debye-Waller Factor"], + "minimum_intensity": d["Minimum Intensity"], + "minimum_lattice_spacing": d["Minimum Lattice Spacing"], + "phase": phase_info, + } + + @staticmethod + def parse_phase_info(group: h5py.Group) -> dict: + d = _hdf5group2dict(group) + abc = d["Lattice Dimensions"] + # TODO: (a, b, c) unit options are Angstrom and ...? Figure out + # the others, so we always return Ångstrøm. + # dimension_unit = group["Lattice Dimensions"].attrs["Unit"] + angles = d["Lattice Angles"] + angle_unit = group["Lattice Angles"].attrs["Unit"] + if angle_unit == "rad": + angles = np.rad2deg(angles) + return { + "laue_group": d["Laue Group"], + "name": d["Phase Name"], + "reference": d["Reference"], + "space_group": d["Space Group"], + "structure": { + "title": d["Phase Name"], + "lattice": { + "a": abc[0], + "b": abc[1], + "c": abc[2], + "alpha": angles[0], + "beta": angles[1], + "gamma": angles[2], + }, + }, + } + + @staticmethod + def parse_simulation(simulation: ValidSimulationTypes) -> str: + sim = simulation.lower() + options = ["dynamical", "twobeam", "kinematic"] + if sim not in options: + raise ValueError( + f"Unknown simulation type {simulation!r}. Options are " + + ",".join(options) + + "." + ) + sim = sim.capitalize() + if sim == "Twobeam": + sim = "TwoBeam" + return sim + + def read(self, **kwargs) -> list[dict]: + file = h5py.File(self.filename, mode="r", **kwargs) + self.check_file_format(file) + header = self.parse_header(file["Header"]) + all_data = self.parse_data(file["Data"]) + md = { + "Acquisition_instrument": { + "SEM": {"beam_energy": header.pop("beam_energy")} + }, + "General": { + "original_filename": self.filename.name, + "title": self.filename.stem, + }, + "Signal": {"record_by": "image", "signal_type": "EBSDMasterPattern"}, + } + phase_info = header.pop("phase") + phase = Phase( + name=phase_info["name"], + space_group=int(phase_info["space_group"]), + structure=Structure( + title=phase_info["structure"]["title"], + lattice=Lattice(**phase_info["structure"]["lattice"]), + ), + ) + data = all_data.pop("data") + out = { + "axes": self.get_axes(data.shape), + "data": data, + "hemisphere": "both", + "metadata": md, + "phase": phase, + "projection": "stereographic", + } + omd = header + omd.update(all_data) + out["original_metadata"] = omd + if not self.lazy: + file.close() + return [out] + + +def file_reader( + filename: str | Path, + energy: range | None = None, + hemisphere: ValidHemispheres = "both", + simulation: ValidSimulationTypes = "dynamical", + lazy: bool = False, + **kwargs, +) -> list[dict]: + """Read simulated electron backscatter diffraction master patterns + from Oxford Instruments' SDF5 (HDF5) file format. + + Not meant to be used directly; use :func:`~kikuchipy.load`. + + Parameters + ---------- + filename + Full file path of the SDF5 file. + energy + Desired beam energy. If not given, the simulation for the + highest beam energy is returned. + hemisphere + Projection hemisphere(s) to read. Options are "both" (default), + "upper", or "lower". If "both", these will be stacked in the + vertical navigation axis. + 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 :class:`h5py.File`. + + Returns + ------- + signal_dict_list + Data, axes, metadata, and original metadata. + """ + reader = OxfordMasterPatternReader(filename, energy, hemisphere, simulation, lazy) + return reader.read(**kwargs) diff --git a/src/kikuchipy/io/plugins/oxford_master_pattern/specification.yaml b/src/kikuchipy/io/plugins/oxford_master_pattern/specification.yaml new file mode 100644 index 00000000..9c915c46 --- /dev/null +++ b/src/kikuchipy/io/plugins/oxford_master_pattern/specification.yaml @@ -0,0 +1,9 @@ +name: oxford_master_pattern +description: > + Read support for simulated electron backscatter diffraction (EBSD) + master patterns stored in an Oxford Instruments *.sdf5 (HDF5) file. +file_extensions: ['sdf5'] +default_extension: 0 +writes: False +manufacturer: oxford +footprints: ['Proprietary/Source Info']