diff --git a/rsciio/bruker/__init__.py b/rsciio/bruker/__init__.py index 40459e88..2529287a 100644 --- a/rsciio/bruker/__init__.py +++ b/rsciio/bruker/__init__.py @@ -1,7 +1,9 @@ from ._api import file_reader +from ._utils import export_metadata __all__ = [ "file_reader", + "export_metadata", ] diff --git a/rsciio/bruker/_api.py b/rsciio/bruker/_api.py index 44333f18..a156dc38 100644 --- a/rsciio/bruker/_api.py +++ b/rsciio/bruker/_api.py @@ -62,11 +62,11 @@ x2d = XmlToDict(dub_attr_pre_str="XmlClass", tags_to_flatten="ClassInstance") -class Container(object): +class Container: pass -class SFSTreeItem(object): +class SFSTreeItem: """Class to manage one internal sfs file. Reading, reading in chunks, reading and extracting, reading without @@ -285,7 +285,7 @@ def get_as_BytesIO_string(self): return data -class SFS_reader(object): +class SFS_reader: """Class to read sfs file. SFS is AidAim software's(tm) single file system. The class provides basic reading capabilities of such container. @@ -450,7 +450,7 @@ def get_file(self, path): return item -class EDXSpectrum(object): +class EDXSpectrum: def __init__(self, spectrum): """ Wrap the objectified bruker EDS spectrum xml part @@ -499,7 +499,9 @@ def __init__(self, spectrum): "ElevationAngle": xrf_header_dict["ExcitationAngle"], } # USED: - self.hv = self.esma_metadata["PrimaryEnergy"] + self.hv = self.esma_metadata.get("PrimaryEnergy", None) + if self.hv is None: + _logger.warning("The beam energy couldn't be found.") self.elev_angle = self.esma_metadata["ElevationAngle"] date_time = gen_iso_date_time(spectrum_header) if date_time is not None: @@ -527,7 +529,7 @@ def energy_to_channel(self, energy, kV=True): return int(round((en_temp - self.offset) / self.scale)) -class HyperHeader(object): +class HyperHeader: """Wrap Bruker HyperMaping xml header into python object. Arguments: @@ -585,7 +587,7 @@ def _set_microscope(self, root): semData = root.find("./ClassInstance[@Type='TRTSEMData']") self.sem_metadata = x2d.dictionarize(semData) # parse values for use in hspy metadata: - self.hv = self.sem_metadata.get("HV", 0.0) # in kV + self.hv = self.sem_metadata.get("HV", None) # in kV # image/hypermap resolution in um/pixel: if "DX" in self.sem_metadata: self.units = "µm" @@ -610,7 +612,9 @@ def get_acq_instrument_dict(self, detector=False, **kwargs): """return python dictionary with aquisition instrument mandatory data """ - acq_inst = {"beam_energy": self.hv} + acq_inst = {} + if self.hv is not None: + acq_inst["beam_energy"] = self.hv if "Mag" in self.sem_metadata: acq_inst["magnification"] = self.sem_metadata["Mag"] if detector: @@ -620,7 +624,8 @@ def get_acq_instrument_dict(self, detector=False, **kwargs): acq_inst["Detector"] = det # In case of XRF, the primary energy is only defined in # the spectrum metadata - acq_inst["beam_energy"] = eds_metadata.hv + if eds_metadata.hv is not None: + acq_inst["beam_energy"] = eds_metadata.hv return acq_inst @@ -744,9 +749,9 @@ def get_consistent_min_channels(self, index=0): optimal channel number """ eds_max_energy = self.spectra_data[index].amplification / 1000 # in kV - if hasattr(self, "hv") and (self.hv > 0) and (self.hv < eds_max_energy): + if self.hv and self.hv > 0 and self.hv < eds_max_energy: return self.spectra_data[index].energy_to_channel(self.hv) - if (not hasattr(self, "hv")) or (self.hv == 0): + if self.hv is None: logging.warn( "bcf header contains no node for electron beam " "voltage or such node is absent.\n" @@ -1542,16 +1547,18 @@ def guess_mode(hv): was used from metadata: TEM or SEM. However simple guess can be made using the acceleration voltage, assuming that SEM is <= 30kV or TEM is >30kV""" - if hv > 30.0: + if hv is not None and hv > 30.0: mode = "TEM" else: mode = "SEM" - _logger.info( - "Guessing that the acquisition instrument is %s " % mode - + "because the beam energy is %i keV. If this is wrong, " % hv - + "please provide the right instrument using the 'instrument' " - + "keyword." - ) + + if hv is not None: + _logger.info( + "Guessing that the acquisition instrument is %s " % mode + + "because the beam energy is %i keV. If this is wrong, " % hv + + "please provide the right instrument using the 'instrument' " + + "keyword." + ) return mode diff --git a/rsciio/bruker/_utils.py b/rsciio/bruker/_utils.py new file mode 100644 index 00000000..187590e6 --- /dev/null +++ b/rsciio/bruker/_utils.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2007-2024 The HyperSpy developers +# +# This library 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. +# +# This library 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 any project and source this library is coupled. +# If not, see . + +import xml.etree.ElementTree as ET + +from rsciio._docstrings import FILENAME_DOC +from rsciio.utils.tools import sanitize_msxml_float + +from ._api import SFS_reader + + +def export_metadata(filename, output_filename=None): + """ + Export the metadata from a bcf file to a xml file. + + Parameters + ---------- + %s + output_filename : str, pathlib.Path or None + The filename of the exported xml file. + If ``None``, use "header.xml" as default. + + """ + sfs = SFS_reader(filename) + # all file items in this singlefilesystem class instance is held inside + # dictionary hierarchy, we fetch the header: + header = sfs.vfs["EDSDatabase"]["HeaderData"] + xml_str = sanitize_msxml_float(header.get_as_BytesIO_string().getvalue()) + xml = ET.ElementTree(ET.fromstring(xml_str)) + + if output_filename is None: # pragma: no cover + output_filename = "header.xml" + xml.write(output_filename) + + +export_metadata.__doc__ %= FILENAME_DOC diff --git a/rsciio/tests/test_bruker.py b/rsciio/tests/test_bruker.py index 397d0398..d54dab49 100644 --- a/rsciio/tests/test_bruker.py +++ b/rsciio/tests/test_bruker.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from rsciio.bruker import file_reader +from rsciio.bruker import export_metadata, file_reader from rsciio.utils.tests import assert_deep_almost_equal hs = pytest.importorskip("hyperspy.api", reason="hyperspy not installed") @@ -324,3 +324,9 @@ def test_bruker_XRF(): def test_unsupported_extension(): with pytest.raises(ValueError): file_reader("fname.unsupported_extension") + + +def test_export_xml(tmp_path): + export_metadata( + TEST_DATA_DIR / test_files[0], output_filename=tmp_path / "header.xml" + ) diff --git a/rsciio/tests/test_import.py b/rsciio/tests/test_import.py index 796595ce..cddc201c 100644 --- a/rsciio/tests/test_import.py +++ b/rsciio/tests/test_import.py @@ -127,7 +127,9 @@ def test_dir_plugins(plugin): pytest.importorskip("h5py") plugin_module = importlib.import_module(plugin_string) - if plugin["name"] == "MSA": + if plugin["name"] == "Bruker": + assert dir(plugin_module) == ["export_metadata", "file_reader"] + elif plugin["name"] == "MSA": assert dir(plugin_module) == [ "file_reader", "file_writer", @@ -142,6 +144,7 @@ def test_dir_plugins(plugin): ] elif plugin["name"] == "DigitalSurf": assert dir(plugin_module) == ["file_reader", "file_writer", "parse_metadata"] + elif plugin["writes"] is False: assert dir(plugin_module) == ["file_reader"] else: diff --git a/upcoming_changes/326.bugfix.rst b/upcoming_changes/326.bugfix.rst new file mode 100644 index 00000000..e720e94a --- /dev/null +++ b/upcoming_changes/326.bugfix.rst @@ -0,0 +1 @@ +Raise a warning instead of an error when the beam energy can't be found in :ref:`bruker-format` xrf files. \ No newline at end of file diff --git a/upcoming_changes/326.enhancements.rst b/upcoming_changes/326.enhancements.rst new file mode 100644 index 00000000..061b8482 --- /dev/null +++ b/upcoming_changes/326.enhancements.rst @@ -0,0 +1 @@ +Add :func:`~.bruker.export_metadata` utility function for exporting metadata from :ref:`bruker-format` file. \ No newline at end of file