Skip to content

Commit

Permalink
Adopt RosettaSciIO's plugin specification architecture
Browse files Browse the repository at this point in the history
Signed-off-by: Håkon Wiik Ånes <[email protected]>
  • Loading branch information
hakonanes committed Nov 2, 2023
1 parent 08f4839 commit 6babb59
Show file tree
Hide file tree
Showing 41 changed files with 656 additions and 116 deletions.
130 changes: 55 additions & 75 deletions kikuchipy/io/_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,57 +16,37 @@
# along with kikuchipy. If not, see <http://www.gnu.org/licenses/>.

import glob
import importlib
import os
from pathlib import Path
from typing import List, Optional, Union

from hyperspy.io_plugins import hspy
from hyperspy.misc.io.tools import overwrite as overwrite_method
from hyperspy.misc.utils import strlist2enumeration, find_subclasses
from hyperspy.signal import BaseSignal
from h5py import File, is_hdf5, Group
import numpy as np
from rsciio import IO_PLUGINS
from rsciio.utils.tools import overwrite as overwrite_method
import yaml

import kikuchipy.signals
from kikuchipy.io.plugins import (
bruker_h5ebsd,
ebsd_directory,
edax_binary,
edax_h5ebsd,
emsoft_ebsd,
emsoft_ebsd_master_pattern,
emsoft_ecp_master_pattern,
emsoft_tkd_master_pattern,
kikuchipy_h5ebsd,
nordif,
nordif_calibration_patterns,
oxford_binary,
oxford_h5ebsd,
)
from kikuchipy.io._util import _get_input_bool, _ensure_directory


plugins = [
bruker_h5ebsd,
ebsd_directory,
edax_binary,
edax_h5ebsd,
emsoft_ebsd,
emsoft_ebsd_master_pattern,
emsoft_ecp_master_pattern,
emsoft_tkd_master_pattern,
hspy,
kikuchipy_h5ebsd,
nordif,
nordif_calibration_patterns,
oxford_binary,
oxford_h5ebsd,
]

default_write_ext = set()
for plugin in plugins:
if plugin.writes:
default_write_ext.add(plugin.file_extensions[plugin.default_extension])
plugins = []
write_extensions = []
specification_paths = list(Path(__file__).parent.rglob("specification.yaml"))
for path in specification_paths:
with open(path) as f:
spec = yaml.safe_load(f)
spec["api"] = f"kikuchipy.io.plugins.{path.parts[-2]}"
plugins.append(spec)
if spec["writes"]:
for ext in spec["file_extensions"]:
write_extensions.append(ext)
for plugin in IO_PLUGINS:
if plugin["name"] in ["HSPY", "ZSPY"]:
plugins.append(plugin)


def load(
Expand Down Expand Up @@ -131,21 +111,23 @@ def load(
extension = os.path.splitext(filename)[1][1:]
readers = []
for plugin in plugins:
if extension.lower() in plugin.file_extensions:
if extension.lower() in plugin["file_extensions"]:
readers.append(plugin)
if len(readers) == 0:
raise IOError(
f"Could not read '{filename}'. If the file format is supported, please "
"report this error"
)
elif len(readers) > 1 and is_hdf5(filename):
reader = _plugin_from_footprints(filename, plugins=readers)
reader = _hdf5_plugin_from_footprints(filename, plugins=readers)
else:
reader = readers[0]

# Get data and metadata (from potentially multiple signals if an h5ebsd
# file)
signal_dicts = reader.file_reader(filename, lazy=lazy, **kwargs)
# Get data and metadata (from potentially multiple signals if an
# h5ebsd file)
signal_dicts = importlib.import_module(reader["api"]).file_reader(
filename, lazy=lazy, **kwargs
)
out = []
for signal in signal_dicts:
out.append(_dict2signal(signal, lazy=lazy))
Expand Down Expand Up @@ -208,7 +190,7 @@ def _dict2signal(signal_dict: dict, lazy: bool = False):
return signal


def _plugin_from_footprints(filename: str, plugins) -> Optional[object]:
def _hdf5_plugin_from_footprints(filename: str, plugins: list) -> Optional[dict]:
"""Get HDF5 correct plugin from a list of potential plugins based on
their unique footprints.
Expand Down Expand Up @@ -253,13 +235,10 @@ def _exists(obj, chain):
with File(filename) as f:
d = _hdf5group2dict(f["/"])

plugins_with_footprints = [p for p in plugins if hasattr(p, "footprint")]
plugins_with_manufacturer = [
p for p in plugins_with_footprints if hasattr(p, "manufacturer")
]

matching_plugin = None
# Check manufacturer if possible (all h5ebsd files have this)

match_manufacturer = []
# Search for the manufacturer (all h5ebsd files have this)
for key, val in d.items():
if key == "manufacturer":
# Extracting the manufacturer is finicky
Expand All @@ -268,23 +247,26 @@ def _exists(obj, chain):
man = man[0]
if isinstance(man, bytes):
man = man.decode("latin-1")
for p in plugins_with_manufacturer:
if man.lower() == p.manufacturer:
matching_plugin = p
break

# If no match found, continue searching
if matching_plugin is None:
for p in plugins_with_footprints:
for p in plugins:
if man.lower() == p["manufacturer"]:
match_manufacturer.append(p)

if len(match_manufacturer) == 1:
matching_plugin = match_manufacturer[0]
else:
# Search for a unique footprint
match_footprint = []
for p in plugins:
n_matches = 0
n_desired_matches = len(p.footprint)
for fp in p.footprint:
n_desired_matches = len(p["footprints"])
for fp in p["footprints"]:
fp = fp.lower().split("/")
if _exists(d, fp) is not None:
n_matches += 1
if n_matches == n_desired_matches:
matching_plugin = p
break
if n_matches > 0 and n_matches == n_desired_matches:
match_footprint.append(p)
if len(match_footprint) == 1:
matching_plugin = match_footprint[0]

return matching_plugin

Expand Down Expand Up @@ -398,26 +380,26 @@ def _save(

writer = None
for plugin in plugins:
if ext.lower() in plugin.file_extensions and plugin.writes:
if ext.lower() in plugin["file_extensions"] and plugin["writes"]:
writer = plugin
break

if writer is None:
raise ValueError(
f"'{ext}' does not correspond to any supported format. Supported file "
f"extensions are: '{strlist2enumeration(default_write_ext)}'"
f"extensions are: '{write_extensions}'"
)
else:
sd = signal.axes_manager.signal_dimension
nd = signal.axes_manager.navigation_dimension
if writer.writes is not True and (sd, nd) not in writer.writes:
if writer["writes"] is not True and [sd, nd] not in writer["writes"]:
# Get writers that can write this data
writing_plugins = []
for plugin in plugins:
if (
plugin.writes is True
or plugin.writes is not False
and (sd, nd) in plugin.writes
plugin["writes"] is True
or plugin["writes"] is not False
and (sd, nd) in plugin["writes"]
):
writing_plugins.append(plugin)
raise ValueError(
Expand All @@ -429,11 +411,7 @@ def _save(
is_file = os.path.isfile(filename)

# Check if we are to add signal to an already existing h5ebsd file
if (
writer.format_name == "kikuchipy_h5ebsd"
and overwrite is not True
and is_file
):
if writer["name"] == "kikuchipy_h5ebsd" and overwrite is not True and is_file:
if add_scan is None:
q = "Add scan to '{}' (y/n)?\n".format(filename)
add_scan = _get_input_bool(q)
Expand All @@ -456,7 +434,9 @@ def _save(

# Finally, write file
if write:
writer.file_writer(filename, signal, **kwargs)
importlib.import_module(writer["api"]).file_writer(
filename, signal, **kwargs
)
directory, filename = os.path.split(os.path.abspath(filename))
signal.tmp_parameters.set_item("folder", directory)
signal.tmp_parameters.set_item("filename", os.path.splitext(filename)[0])
Expand Down
2 changes: 1 addition & 1 deletion kikuchipy/io/plugins/_emsoft_master_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from h5py import Dataset, File, Group
import numpy as np

from kikuchipy.io.plugins.emsoft_ebsd import _crystaldata2phase
from kikuchipy.io.plugins.emsoft_ebsd._api import _crystaldata2phase
from kikuchipy.io.plugins._h5ebsd import _hdf5group2dict


Expand Down
35 changes: 35 additions & 0 deletions kikuchipy/io/plugins/bruker_h5ebsd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2019-2023 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 <http://www.gnu.org/licenses/>.

"""Reader of EBSD data from a Bruker Nano h5ebsd file."""

from kikuchipy.io.plugins.bruker_h5ebsd._api import file_reader


__all__ = ["file_reader"]


def __dir__():
return sorted(__all__)


def __getattr__(name):
if name in __all__:
import importlib

return importlib.import_module("." + name, __name__)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
5 changes: 0 additions & 5 deletions kikuchipy/io/plugins/bruker_h5ebsd/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@
from kikuchipy.io.plugins._h5ebsd import _hdf5group2dict, H5EBSDReader


# Unique HDF5 footprint
footprint = ["manufacturer", "version"]
manufacturer = "bruker nano"


class BrukerH5EBSDReader(H5EBSDReader):
"""Bruker Nano h5ebsd file reader.
Expand Down
13 changes: 13 additions & 0 deletions kikuchipy/io/plugins/bruker_h5ebsd/specification.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: bruker_h5ebsd
description: >
Read support for electron backscatter diffraction patterns stored in
an HDF5 file formatted in Bruker Nano's h5ebsd format, similar to the
format described in Jackson et al.: h5ebsd: an archival data format
for electron back-scatter diffraction data sets. Integrating Materials
and Manufacturing Innovation 2014 3:4,
doi: https://dx.doi.org/10.1186/2193-9772-3-4.
file_extensions: ['h5', 'hdf5', 'h5ebsd']
default_extension: 0
writes: False
manufacturer: bruker nano
footprints: ['manufacturer', 'version']
35 changes: 35 additions & 0 deletions kikuchipy/io/plugins/ebsd_directory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2019-2023 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 <http://www.gnu.org/licenses/>.

"""Reader of EBSD patterns from a dictionary of images."""

from kikuchipy.io.plugins.ebsd_directory._api import file_reader


__all__ = ["file_reader"]


def __dir__():
return sorted(__all__)


def __getattr__(name):
if name in __all__:
import importlib

return importlib.import_module("." + name, __name__)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
7 changes: 7 additions & 0 deletions kikuchipy/io/plugins/ebsd_directory/specification.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: ebsd_directory
description: Read support for patterns in image files in a directory
file_extensions: ['tif', 'tiff', 'bmp', 'png']
default_extension: 0
writes: False
manufacturer: None
footprints: []
38 changes: 38 additions & 0 deletions kikuchipy/io/plugins/edax_binary/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2019-2023 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 <http://www.gnu.org/licenses/>.

"""Reader of EBSD data from EDAX TSL UP1/2 files.
The reader is adapted from the EDAX UP1/2 reader in PyEBSDIndex.
"""

from kikuchipy.io.plugins.edax_binary._api import file_reader


__all__ = ["file_reader"]


def __dir__():
return sorted(__all__)


def __getattr__(name):
if name in __all__:
import importlib

return importlib.import_module("." + name, __name__)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
10 changes: 10 additions & 0 deletions kikuchipy/io/plugins/edax_binary/specification.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: edax_binary
description: >
Read support for electron backscatter diffraction patterns stored in a
binary EDAX TSL's UP1/UP2 file extension '.up1' or '.up2'. The reader
is adapted from the EDAX UP1/2 reader in PyEBSDIndex.
file_extensions: ['up1', 'up2']
default_extension: 0
writes: False
manufacturer: edax
footprints: []
Loading

0 comments on commit 6babb59

Please sign in to comment.