From 409e12983a0eabd11bd5c52544f1715ce494743f Mon Sep 17 00:00:00 2001 From: Marius Retegan Date: Sun, 15 Mar 2026 23:17:57 +0100 Subject: [PATCH 1/7] Add XAS acquisition modes and detection base classes --- applications/NXxas.nxdl.xml | 159 ++++----- base_classes/NXherfd.nxdl.xml | 523 +++++++++++++++++++++++++++++ base_classes/NXpey.nxdl.xml | 82 +++++ base_classes/NXpfy.nxdl.xml | 95 ++++++ base_classes/NXtey.nxdl.xml | 79 +++++ base_classes/NXtfy.nxdl.xml | 76 +++++ base_classes/NXtrans.nxdl.xml | 124 +++++++ dev_tools/docs/nxdl.py | 203 ++++++----- dev_tools/tests/test_nxdl_utils.py | 440 ++++++++++++------------ 9 files changed, 1397 insertions(+), 384 deletions(-) create mode 100644 base_classes/NXherfd.nxdl.xml create mode 100644 base_classes/NXpey.nxdl.xml create mode 100644 base_classes/NXpfy.nxdl.xml create mode 100644 base_classes/NXtey.nxdl.xml create mode 100644 base_classes/NXtfy.nxdl.xml create mode 100644 base_classes/NXtrans.nxdl.xml diff --git a/applications/NXxas.nxdl.xml b/applications/NXxas.nxdl.xml index f076e9bb42..a5f33f3229 100644 --- a/applications/NXxas.nxdl.xml +++ b/applications/NXxas.nxdl.xml @@ -2,9 +2,9 @@ - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://definition.nexusformat.org/nxdl/3.1 ../nxdl.xsd "> - - The symbol(s) listed here will be used below to coordinate datasets with the same shape. - - - Number of points + + Number of energy data points - + - This is an application definition for raw data from an X-ray absorption spectroscopy experiment. - - This is essentially a scan on energy versus incoming/ - absorbed beam. + Application definition for X-ray absorption spectroscopy (XAS). + + This definition contains the common fields shared by all XAS + measurements: energy axis, processed intensity, element, edge, + and sample information. + + The measurement mode (transmission, fluorescence yield, electron + yield, HERFD, etc.) is described by an optional ``mode`` group + whose NeXus type is one of the mode base classes (NXtrans, + NXtfy, NXpfy, NXherfd, NXtey, NXpey). + The mode group holds all mode-specific raw data, detectors, + and instrument geometry. - - - Official NeXus NXDL schema to which this file conforms + Official NeXus NXDL schema to which this file conforms. - + - - - - - - - - - - - - - - - - - - - - - - - - - - - This data corresponds to the sample signal. - - - - - + + Excited element + + + Absorption edge + + Specify if the data comes from a calculation. + + + + The energy axis of the spectrum. + + + + + + + + The processed absorption spectrum. The precise definition + depends on the acquisition mode (transmission, fluorescence + yield, electron yield, etc.) + + + + + + + + The errors associated with the intensity of the spectrum. + + + + + - Descriptive name of sample - - - - - - Count to a preset value based on either clock time (timer) - or received monitor counts (monitor). - - - - - - - - preset value for time or monitor - - - This field could be a link to ``/NXentry/NXinstrument/incoming_beam:NXdetector/data`` - - - + Descriptive name of the sample - - - - - Detection method used for observing the sample absorption (pick one from the enumerated list and spell exactly) - - - - - - - - + + + + + + + + + + Plot of the X-ray absorption intensity versus energy + + + diff --git a/base_classes/NXherfd.nxdl.xml b/base_classes/NXherfd.nxdl.xml new file mode 100644 index 0000000000..f8dfd10870 --- /dev/null +++ b/base_classes/NXherfd.nxdl.xml @@ -0,0 +1,523 @@ + + + + + + + Number of energy data points + + + + High-energy resolution fluorescence detection (HERFD) is a particular case + of partial fluorescence yield measured with a crystal analyzer + spectrometer with an energy bandwidth of approximately 1-2 eV. + + The HERFD spectrum corresponds to a constant-emission-energy cut + through the Resonant Inelastic X-ray Scattering (RIXS) plane. + The spectral shape depends on the emission energy, making the + emission line and emission energy mandatory metadata. + + .. math:: \mu(E) \propto I_f/I_0 + + The spectrometer uses Rowland circle geometry (Johann or Johansson + type). Multiple crystal analyzers may be arranged at different + horizontal angles around the sample to increase solid angle coverage. + + + + The emission line at which the HERFD spectrum is measured. + + + + + The emission energy at which the spectrometer is set. + + + + + Beamline coordinate system with the sample at the origin: + x along the beam, y horizontal, z opposite to gravity. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Should point to + ``transformations/beam``. + + + + + Two rotations relating the beamline frame to + the NeXus/McStas laboratory frame, plus direction + vectors labeling beam and gravity. + + + + Direction of the incident beam in the beamline + coordinate system. + + + + + + + + + + + + + + + Direction of gravity in the beamline coordinate + system. + + + + + + + + + + + + + + + Active rotation moving gravity from the + beamline direction (-z) to the McStas direction (-y). + + + + + + + + + + + + + + + + + + + + Active rotation moving the beam from the + beamline direction (+x) to the McStas direction (+z). + + + + + + + + + + + + + + Should point to ``.`` (the McStas laboratory + frame). + + + + + + + The incident X-ray beam. + + + Energy of the incident beam at the sample position. + + + + + Should point to + ``beam/transformations/beam_direction``. + + + + + + Beam direction in the beamline coordinate + system. The beam travels along +x. + + + + + + + + + Should point to the beamline coordinate system + or to the sample. + + + + + + + + + + + + + + + + + Detector measuring the incident beam intensity + :math:`I_0`, positioned upstream of the sample along + the beam direction. + + + + + + + + + Should point to + ``i0/transformations/i0_distance``. + + + + + + Distance from the sample to the I0 detector, + measured upstream along the beam (negative x + direction in the beamline frame). + + + + + + + + + + + + + + Should point to the sample. + + + + + + + + Crystal analyzer on the Rowland circle. For + multi-crystal spectrometers, use one group per + crystal (e.g. ``analyzer1``, ``analyzer2``). + + + + Type or material of the analyzer crystal + (Si, Ge, etc.). + + + + + Miller indices (hkl) values of the nominal + reflection. + + + + + + + + The spacing between crystal planes of the + reflection. + + + + + Bragg angle :math:`\theta_B` of the nominal + reflection. + + + + + Bending radius of the spherically bent crystal + analyzer. In Johann geometry this is :math:`2R_R` + (twice the Rowland radius). + + + + + Radius of the Rowland circle :math:`R_R`. The + sample, crystal center, and detector focus all + lie on this circle. + + + + + The energy bandwidth or resolution of the crystal + analyzer. + + + + + The type of crystal analyzer geometry. + + + + + + + + + Diameter of the crystal analyzer wafer. + + + + + Should point to the last transformation in the + chain, i.e. + ``transformations/analyzer_distance``. + + + + + Transformation chain placing the analyzer relative + to the sample: azimuthal angle, polar angle, + then distance. + + + + Sample-to-analyzer distance. + + + + + + + + + + + + + + + + + + + + Polar angle of the analyzer in the vertical + Rowland plane. Elevation from the horizontal + beam plane to the sample-analyzer direction. + + + + + + + + + + + + + + + + + + + + Azimuthal (horizontal) angle of the + spectrometer arm from the incident beam + direction. Rotation around the vertical + z-axis. Typically around 90 degrees to + minimize elastic scattering. + + + + + + + + + + + + + + Should point to the sample. + + + + + + + + Detector measuring the fluorescence intensity + :math:`I_f` diffracted by the crystal analyzer(s). + + + + + + + + + Should point to the last transformation in the + chain, i.e. + ``transformations/detector_distance``. + + + + + Transformation chain placing the detector relative + to the sample: azimuthal angle, polar angle, + then distance. + + + + Distance from the sample to the detector. + + + + + + + + + + + + + + + + + + + + Polar angle of the detector in the vertical + Rowland plane. + + + + + + + + + + + + + + + + + + + + Azimuthal (horizontal) angle of the detector + from the incident beam direction. Should + match the analyzer azimuthal angle for + on-Rowland focusing. + + + + + + + + + + + + + + Should point to the sample. + + + + + + + + Description of how the intensity was obtained from + the raw detector data (i0, if). + + + Name of the program used for processing. + + + Version of the program used for processing. + + + Date and time of processing. + + + diff --git a/base_classes/NXpey.nxdl.xml b/base_classes/NXpey.nxdl.xml new file mode 100644 index 0000000000..ba6dd76951 --- /dev/null +++ b/base_classes/NXpey.nxdl.xml @@ -0,0 +1,82 @@ + + + + + + + Number of energy data points + + + + Partial electron yield (PEY) mode XAS. Electrons above a certain + kinetic energy threshold are collected. A retarding voltage is + applied to discriminate against low-energy secondary electrons: + + .. math:: \mu(E) \propto I_{ey}/I_0 + + + + Detector measuring the incident beam intensity + :math:`I_0`. + + + + + + + + + + Detector measuring the partial electron yield + :math:`I_{ey}`. + + + + + + + + + The retarding voltage (bias) applied to select + electrons above a kinetic energy threshold. + + + + + + Description of how the intensity was obtained from + the raw detector data (i0, iey). + + + Name of the program used for processing. + + + Version of the program used for processing. + + + Date and time of processing. + + + diff --git a/base_classes/NXpfy.nxdl.xml b/base_classes/NXpfy.nxdl.xml new file mode 100644 index 0000000000..afaab464a3 --- /dev/null +++ b/base_classes/NXpfy.nxdl.xml @@ -0,0 +1,95 @@ + + + + + + + Number of energy data points + + + + Partial fluorescence yield (PFY) mode XAS. An energy-dispersive + detector selects a portion of the fluorescence signal around a + chosen emission line: + + .. math:: \mu(E) \propto I_f/I_0 + + + + The emission line(s) selected in the ROI for the partial + fluorescence yield measurement. + + + + + Detector measuring the incident beam intensity + :math:`I_0`. + + + + + + + + + + Energy-dispersive fluorescence detector. The ``data`` + field holds the fluorescence intensity integrated + over the ROI, corrected for dead-time. + + + + + + + + Dead-time correction constant. + + + + Detector live time per energy point. + + + + + + + + + Description of how the intensity was obtained from + the raw detector data (i0, if), including dead-time + correction and any self-absorption correction applied. + + + Name of the program used for processing. + + + Version of the program used for processing. + + + Date and time of processing. + + + diff --git a/base_classes/NXtey.nxdl.xml b/base_classes/NXtey.nxdl.xml new file mode 100644 index 0000000000..c22bef5eee --- /dev/null +++ b/base_classes/NXtey.nxdl.xml @@ -0,0 +1,79 @@ + + + + + + + Number of energy data points + + + + Total electron yield (TEY) mode XAS. The drain current or total + electron current is proportional to the absorption coefficient: + + .. math:: \mu(E) \propto I_{ey}/I_0 + + TEY is inherently surface-sensitive because electrons are readily + absorbed by most materials, limiting the probing depth to a few + nanometers. + + + + Detector measuring the incident beam intensity + :math:`I_0`. + + + + + + + + + + Detector measuring the total electron yield + :math:`I_{ey}` (drain current). + + + + + + + + + + Description of how the intensity was obtained from + the raw detector data (i0, iey). + + + Name of the program used for processing. + + + Version of the program used for processing. + + + Date and time of processing. + + + diff --git a/base_classes/NXtfy.nxdl.xml b/base_classes/NXtfy.nxdl.xml new file mode 100644 index 0000000000..073216892d --- /dev/null +++ b/base_classes/NXtfy.nxdl.xml @@ -0,0 +1,76 @@ + + + + + + + Number of energy data points + + + + Total fluorescence yield (TFY) mode XAS. The absorption coefficient + is proportional to the ratio of the total fluorescence intensity + and the incident beam intensity: + + .. math:: \mu(E) \propto I_f/I_0 + + + + Detector measuring the incident beam intensity + :math:`I_0`. + + + + + + + + + + Detector measuring the total fluorescence emission + :math:`I_f`. + + + + + + + + + + Description of how the intensity was obtained from + the raw detector data (i0, if). + + + Name of the program used for processing. + + + Version of the program used for processing. + + + Date and time of processing. + + + diff --git a/base_classes/NXtrans.nxdl.xml b/base_classes/NXtrans.nxdl.xml new file mode 100644 index 0000000000..fa364cb8a2 --- /dev/null +++ b/base_classes/NXtrans.nxdl.xml @@ -0,0 +1,124 @@ + + + + + + + Number of energy data points + + + + Transmission mode XAS. The absorption coefficient is given by + the Beer-Lambert law: + + .. math:: \mu(E)t = -\ln(I/I_0) + + where :math:`I` is the intensity of the transmitted beam, :math:`I_0` is + the intensity of the incident beam, and :math:`t` is the thickness of the + sample. + + + + + + + + + + + + + + + + + + + The spacing between crystal planes of the reflection + + + + Type or material of monochromating substance + (Si, Ge, Multilayer). + + + + Miller indices (hkl) values of nominal reflection + + + + + + + + + Detector measuring the incident beam intensity + :math:`I_0`. + + + + + + + + + + Detector measuring the transmitted beam intensity + :math:`I`. + + + + + + + + + + Detector measuring the reference intensity + :math:`I_{ref}`, placed after a reference sample + (typically a metal foil). + + + + + + + + + + Description of how the intensity was obtained from + the raw detector data (i0, itrans). + + + Name of the program used for processing. + + + Version of the program used for processing. + + + Date and time of processing. + + + diff --git a/dev_tools/docs/nxdl.py b/dev_tools/docs/nxdl.py index 5b3dc64622..bd8e98ccf2 100644 --- a/dev_tools/docs/nxdl.py +++ b/dev_tools/docs/nxdl.py @@ -364,7 +364,7 @@ def _get_required_or_optional_text(self, node): :returns: formatted text """ tag = node.tag.split("}")[-1] - if tag in ("field", "group"): + if tag in ("field", "group", "choice"): optional_default = not self._use_application_defaults optional = node.get("optional", optional_default) in (True, "true", "1", 1) recommended = node.get("recommended", None) in (True, "true", "1", 1) @@ -624,91 +624,144 @@ def _print_full_tree(self, ns, parent, name, indent, parent_path): :param indent: to keep track of indentation level :param parent_path: NX class path of parent nodes """ - for node in parent.xpath("nx:field", namespaces=ns): - name = node.get("name") - formatted_name = get_rst_formatted_name(node) - index_name = name - dims = self._analyze_dimensions(ns, node) - - optional_text = self._get_required_or_optional_text(node) - self._print(f"{indent}{self._hyperlink_target(parent_path, name, 'field')}") - self._print(f"{indent}.. index:: {index_name} (field)\n") - self._print( - f"{indent}{formatted_name}: " - f"{optional_text}" - f"{self._format_type(node)}" - f"{dims}" - f"{self._format_units(node)}" - f" {self.get_first_parent_ref(f'{parent_path}/{name}', 'field')}" - "\n" - ) + # Process children in document order to preserve XML ordering. + for node in parent.xpath("nx:field|nx:group|nx:choice|nx:link", namespaces=ns): + tag = node.tag.split("}")[-1] + + if tag == "field": + name = node.get("name") + formatted_name = get_rst_formatted_name(node) + index_name = name + dims = self._analyze_dimensions(ns, node) + + optional_text = self._get_required_or_optional_text(node) + self._print( + f"{indent}{self._hyperlink_target(parent_path, name, 'field')}" + ) + self._print(f"{indent}.. index:: {index_name} (field)\n") + self._print( + f"{indent}{formatted_name}: " + f"{optional_text}" + f"{self._format_type(node)}" + f"{dims}" + f"{self._format_units(node)}" + f" {self.get_first_parent_ref(f'{parent_path}/{name}', 'field')}" + "\n" + ) - self._print_if_deprecated(ns, node, indent + self._INDENTATION_UNIT) - self._print_doc_enum(indent, ns, node) + self._print_if_deprecated(ns, node, indent + self._INDENTATION_UNIT) + self._print_doc_enum(indent, ns, node) + + for subnode in node.xpath("nx:attribute", namespaces=ns): + optional = self._get_required_or_optional_text(subnode) + self._print_attribute( + ns, + "field", + subnode, + optional, + indent + self._INDENTATION_UNIT, + parent_path + "/" + name, + ) - for subnode in node.xpath("nx:attribute", namespaces=ns): - optional = self._get_required_or_optional_text(subnode) - self._print_attribute( - ns, - "field", - subnode, - optional, - indent + self._INDENTATION_UNIT, - parent_path + "/" + name, + elif tag == "group": + name = node.get("name", "") + formatted_name = get_rst_formatted_name(node) + typ = node.get("type", "untyped (this is an error; please report)") + + optional_text = self._get_required_or_optional_text(node) + if typ.startswith("NX"): + if name == "": + name = typ.lstrip("NX").upper() + typ = f":ref:`{typ}`" + hTarget = self._hyperlink_target(parent_path, name, "group") + # target = hTarget.replace(".. _", "").replace(":\n", "") + # TODO: https://github.com/nexusformat/definitions/issues/1057 + self._print(f"{indent}{hTarget}") + self._print( + f"{indent}{formatted_name}: {optional_text}{typ} " + f"{self.get_first_parent_ref(f'{parent_path}/{name}', 'group')}\n" ) - for node in parent.xpath("nx:group", namespaces=ns): - name = node.get("name", "") - formatted_name = get_rst_formatted_name(node) - typ = node.get("type", "untyped (this is an error; please report)") - - optional_text = self._get_required_or_optional_text(node) - if typ.startswith("NX"): - if name == "": - name = typ.lstrip("NX").upper() - typ = f":ref:`{typ}`" - hTarget = self._hyperlink_target(parent_path, name, "group") - # target = hTarget.replace(".. _", "").replace(":\n", "") - # TODO: https://github.com/nexusformat/definitions/issues/1057 - self._print(f"{indent}{hTarget}") - self._print( - f"{indent}{formatted_name}: {optional_text}{typ} {self.get_first_parent_ref(f'{parent_path}/{name}', 'group')}\n" - ) - - self._print_if_deprecated(ns, node, indent + self._INDENTATION_UNIT) - self._print_doc_enum(indent, ns, node) + self._print_if_deprecated(ns, node, indent + self._INDENTATION_UNIT) + self._print_doc_enum(indent, ns, node) + + for subnode in node.xpath("nx:attribute", namespaces=ns): + optional = self._get_required_or_optional_text(subnode) + self._print_attribute( + ns, + "group", + subnode, + optional, + indent + self._INDENTATION_UNIT, + parent_path + "/" + name, + ) - for subnode in node.xpath("nx:attribute", namespaces=ns): - optional = self._get_required_or_optional_text(subnode) - self._print_attribute( + nodename = "%s/%s" % (name, node.get("type")) + self._print_full_tree( ns, - "group", - subnode, - optional, + node, + nodename, indent + self._INDENTATION_UNIT, parent_path + "/" + name, ) - nodename = "%s/%s" % (name, node.get("type")) - self._print_full_tree( - ns, - node, - nodename, - indent + self._INDENTATION_UNIT, - parent_path + "/" + name, - ) + elif tag == "choice": + name = node.get("name", "") + hTarget = self._hyperlink_target(parent_path, name, "choice") + self._print(f"{indent}{hTarget}") + optional_text = self._get_required_or_optional_text(node).strip("() ") + self._print( + f"{indent}**{name}**: ({optional_text}) " + "Only one of the following groups may be present:\n" + ) + self._print_doc_enum(indent, ns, node) - for node in parent.xpath("nx:link", namespaces=ns): - name = node.get("name") - formatted_name = get_rst_formatted_name(node) - self._print(f"{indent}{self._hyperlink_target(parent_path, name, 'link')}") - self._print( - f"{indent}{formatted_name}: " - ":ref:`link` " - f"(suggested target: ``{node.get('target')}``)" - "\n" - ) - self._print_doc_enum(indent, ns, node) + # Print each group option within the choice. + for subnode in node.xpath("nx:group", namespaces=ns): + subname = subnode.get("name", "") + typ = subnode.get( + "type", "untyped (this is an error; please report)" + ) + if typ.startswith("NX"): + if subname == "": + subname = typ.lstrip("NX").upper() + typ_ref = f":ref:`{typ}`" + else: + typ_ref = typ + sub_indent = indent + self._INDENTATION_UNIT + subTarget = self._hyperlink_target( + parent_path + "/" + name, subname, "group" + ) + self._print(f"{sub_indent}{subTarget}") + self._print(f"{sub_indent}**{subname}**: {typ_ref}\n") + self._print_doc_enum(sub_indent, ns, subnode) + + # Recursively print any content within this group option. + nodename = "%s/%s" % (subname, subnode.get("type")) + self._print_full_tree( + ns, + subnode, + nodename, + sub_indent + self._INDENTATION_UNIT, + parent_path + "/" + name + "/" + subname, + ) + + elif tag == "link": + name = node.get("name") + formatted_name = get_rst_formatted_name(node) + self._print( + f"{indent}{self._hyperlink_target(parent_path, name, 'link')}" + ) + self._print( + f"{indent}{formatted_name}: " + ":ref:`link` " + f"(suggested target: ``{node.get('target')}``)" + "\n" + ) + self._print_doc_enum(indent, ns, node) + + else: + raise ValueError(f"Unknown node type: {tag}") def _print(self, *args, end="\n"): # TODO: change instances of \t to proper indentation diff --git a/dev_tools/tests/test_nxdl_utils.py b/dev_tools/tests/test_nxdl_utils.py index 96aa90db26..666337a3f6 100644 --- a/dev_tools/tests/test_nxdl_utils.py +++ b/dev_tools/tests/test_nxdl_utils.py @@ -1,220 +1,220 @@ -"""This is a code that performs several tests on nexus tool""" - -from pathlib import Path - -import lxml.etree as ET -import pytest - -from ..utils import nxdl_utils as nexus - - -def test_get_nexus_classes_units_attributes(): - """Check the correct parsing of a separate list for: - Nexus classes (base_classes) - Nexus units (memberTypes) - Nexus attribute type (primitiveTypes) - the tested functions can be found in nexus.py file""" - - # Test 1 - nexus_classes_list = nexus.get_nx_classes() - - assert "NXbeam" in nexus_classes_list - - # Test 2 - nexus_units_list = nexus.get_nx_units() - assert "NX_TEMPERATURE" in nexus_units_list - - # Test 3 - nexus_attribute_list = nexus.get_nx_attribute_type() - assert "NX_FLOAT" in nexus_attribute_list - - -def test_get_node_at_nxdl_path(): - """Test to verify if we receive the right XML element for a given NXDL path""" - local_dir = Path(__file__).resolve().parent - nxdl_file_path = local_dir / "NXtest.nxdl.xml" - elem = ET.parse(nxdl_file_path).getroot() - node = nexus.get_node_at_nxdl_path("/ENTRY/NXODD_name", elem=elem) - assert node.attrib["type"] == "NXdata" - assert node.attrib["name"] == "NXODD_name" - - node = nexus.get_node_at_nxdl_path("/ENTRY/NXODD_name/float_value", elem=elem) - assert node.attrib["type"] == "NX_FLOAT" - assert node.attrib["name"] == "float_value" - - node = nexus.get_node_at_nxdl_path( - "/ENTRY/NXODD_name/AXISNAME/long_name", elem=elem - ) - assert node.attrib["name"] == "long_name" - - nxdl_file_path = local_dir / "../../contributed_definitions/NXiv_temp.nxdl.xml" - - elem = ET.parse(nxdl_file_path).getroot() - node = nexus.get_node_at_nxdl_path( - "/ENTRY/INSTRUMENT/ENVIRONMENT/voltage_controller", elem=elem - ) - assert node.attrib["name"] == "voltage_controller" - - node = nexus.get_node_at_nxdl_path( - "/ENTRY/INSTRUMENT/ENVIRONMENT/voltage_controller/calibration_time", elem=elem - ) - assert node.attrib["name"] == "calibration_time" - - -def test_get_inherited_nodes(): - """Test to verify if we receive the right XML element list for a given NXDL path.""" - local_dir = Path(__file__).resolve().parent - nxdl_file_path = local_dir / "NXtest.nxdl.xml" - - elem = ET.parse(nxdl_file_path).getroot() - _, _, elist = nexus.get_inherited_nodes(nxdl_path="/ENTRY/NXODD_name", elem=elem) - assert len(elist) == 5 - - nxdl_file_path = ( - local_dir.parent.parent / "contributed_definitions" / "NXiv_temp.nxdl.xml" - ) - - elem = ET.parse(nxdl_file_path).getroot() - _, _, elist = nexus.get_inherited_nodes( - nxdl_path="/ENTRY/INSTRUMENT/ENVIRONMENT", elem=elem - ) - assert len(elist) == 4 - - _, _, elist = nexus.get_inherited_nodes( - nxdl_path="/ENTRY/INSTRUMENT/ENVIRONMENT/voltage_controller", elem=elem - ) - assert len(elist) == 6 - - _, _, elist = nexus.get_inherited_nodes( - nxdl_path="/ENTRY/INSTRUMENT/ENVIRONMENT/voltage_controller", - nx_name="NXiv_temp", - ) - assert len(elist) == 6 - - -@pytest.mark.parametrize( - "hdf_name,concept_name, name_type, should_fit", - [ - ("same_name", "same_name", "specified", True), - ("same_name", "same_name", "any", True), - ("same_name", "same_name", "partial", True), - ("source_pump", "source", "specified", False), - ("source_pump", "source", "any", True), - ("source_pump", "source", "partial", False), - ("source_pump", "sourceType", "specified", False), - ("source_pump", "sourceType", "any", True), - ("source_pump", "sourceType", "partial", False), - ("source_pump", "sourceTYPE", "specified", False), - ("source_pump", "sourceTYPE", "any", True), - ("source_pump", "sourceTYPE", "partial", True), - ("source pump", "sourceTYPE", "specified", False), - ("source pump", "sourceTYPE", "any", False), - ("source pump", "sourceTYPE", "partial", False), - ("Name with some whitespaces in it", "ENTRY", "specified", False), - ("Name with some whitespaces in it", "ENTRY", "any", False), - ("Name with some whitespaces in it", "ENTRY", "partial", False), - ("source", "sourceTYPE", "specified", False), - ("source", "sourceTYPE", "any", True), - ("source", "sourceTYPE", "partial", True), - ("SOURCE", "SOURCE", "specified", True), - ("SOURCE", "SOURCE", "any", True), - ("SOURCE", "SOURCE", "partial", True), - ("source123", "SOURCE", "specified", False), - ("source123", "SOURCE", "any", True), - ("source123", "SOURCE", "partial", True), - ("1source", "SOURCE", "specified", False), - ("1source", "SOURCE", "any", True), - ("1source", "SOURCE", "partial", True), - ("_source", "SOURCE", "specified", False), - ("_source", "SOURCE", "any", True), - ("_source", "SOURCE", "partial", True), - ("angular_energy_resolution", "angularNresolution", "specified", False), - ("angular_energy_resolution", "angularNresolution", "any", True), - ("angular_energy_resolution", "angularNresolution", "partial", True), - (".test", "TEST", "specified", False), - (".test", "TEST", "any", False), - (".test", "TEST", "partial", False), - ], -) -def test_namefitting(hdf_name, concept_name, name_type, should_fit): - """Test namefitting of nexus concept names""" - name_any = name_type == "any" - name_partial = name_type == "partial" - - if should_fit: - assert nexus.get_nx_namefit(hdf_name, concept_name, name_any, name_partial) > -1 - else: - assert ( - nexus.get_nx_namefit(hdf_name, concept_name, name_any, name_partial) == -1 - ) - - -@pytest.mark.parametrize( - "hdf_name,concept_name, score", - [ - ("test_name", "TEST_name", 9), - ("te_name", "TEST_name", 7), - ("my_other_name", "TEST_name", 5), - ("test_name", "test_name", 18), - ("test_other", "test_name", -1), - ("my_fancy_yet_long_name", "my_SOME_name", 8), - ("something", "XXXX", 0), - ("something", "OTHER", 1), - ], -) -def test_namefitting_scores(hdf_name, concept_name, score): - """Test namefitting of nexus concept names""" - assert nexus.get_nx_namefit(hdf_name, concept_name, name_partial=True) == score - - -@pytest.mark.parametrize( - "better_fit,better_ref,worse_fit,worse_ref", - [ - ("sourcetype", "sourceTYPE", "source_pump", "sourceTYPE"), - ("source_pump", "sourceTYPE", "source_pump", "TEST"), - ], -) -def test_namefitting_precedence(better_fit, better_ref, worse_fit, worse_ref): - """Test if namefitting follows proper precedence rules""" - - assert nexus.get_nx_namefit( - better_fit, better_ref, name_partial=True - ) > nexus.get_nx_namefit(worse_fit, worse_ref) - - -@pytest.mark.parametrize( - "string_obj, decode, expected", - [ - # Test with lists of bytes and strings - ([b"bytes", "string"], True, ["bytes", "string"]), - ([b"bytes", "string"], False, [b"bytes", "string"]), - ([b"bytes", b"more_bytes", "string"], True, ["bytes", "more_bytes", "string"]), - ( - [b"bytes", b"more_bytes", "string"], - False, - [b"bytes", b"more_bytes", "string"], - ), - ([b"fixed", b"length", b"strings"], True, ["fixed", "length", "strings"]), - ([b"fixed", b"length", b"strings"], False, [b"fixed", b"length", b"strings"]), - # Test with nested lists - ([[b"nested1"], [b"nested2"]], True, [["nested1"], ["nested2"]]), - ([[b"nested1"], [b"nested2"]], False, [[b"nested1"], [b"nested2"]]), - # Test with bytes - (b"single", True, "single"), - (b"single", False, b"single"), - # Test with str - ("single", True, "single"), - ("single", False, "single"), - # Test with int - (123, True, 123), - (123, False, 123), - ], -) -def test_decode_or_not(string_obj, decode, expected): - # Handle normal cases - result = nexus.decode_or_not(elem=string_obj, decode=decode) - if isinstance(expected, list): - assert isinstance(result, list), f"Expected list, but got {type(result)}" - # Handle all other cases - else: - assert result == expected, f"Failed for {string_obj} with decode={decode}" +"""This is a code that performs several tests on nexus tool""" + +from pathlib import Path + +import lxml.etree as ET +import pytest + +from ..utils import nxdl_utils as nexus + + +def test_get_nexus_classes_units_attributes(): + """Check the correct parsing of a separate list for: + Nexus classes (base_classes) + Nexus units (memberTypes) + Nexus attribute type (primitiveTypes) + the tested functions can be found in nexus.py file""" + + # Test 1 + nexus_classes_list = nexus.get_nx_classes() + + assert "NXbeam" in nexus_classes_list + + # Test 2 + nexus_units_list = nexus.get_nx_units() + assert "NX_TEMPERATURE" in nexus_units_list + + # Test 3 + nexus_attribute_list = nexus.get_nx_attribute_type() + assert "NX_FLOAT" in nexus_attribute_list + + +def test_get_node_at_nxdl_path(): + """Test to verify if we receive the right XML element for a given NXDL path""" + local_dir = Path(__file__).resolve().parent + nxdl_file_path = local_dir / "NXtest.nxdl.xml" + elem = ET.parse(nxdl_file_path).getroot() + node = nexus.get_node_at_nxdl_path("/ENTRY/NXODD_name", elem=elem) + assert node.attrib["type"] == "NXdata" + assert node.attrib["name"] == "NXODD_name" + + node = nexus.get_node_at_nxdl_path("/ENTRY/NXODD_name/float_value", elem=elem) + assert node.attrib["type"] == "NX_FLOAT" + assert node.attrib["name"] == "float_value" + + node = nexus.get_node_at_nxdl_path( + "/ENTRY/NXODD_name/AXISNAME/long_name", elem=elem + ) + assert node.attrib["name"] == "long_name" + + nxdl_file_path = local_dir / "../../contributed_definitions/NXiv_temp.nxdl.xml" + + elem = ET.parse(nxdl_file_path).getroot() + node = nexus.get_node_at_nxdl_path( + "/ENTRY/INSTRUMENT/ENVIRONMENT/voltage_controller", elem=elem + ) + assert node.attrib["name"] == "voltage_controller" + + node = nexus.get_node_at_nxdl_path( + "/ENTRY/INSTRUMENT/ENVIRONMENT/voltage_controller/calibration_time", elem=elem + ) + assert node.attrib["name"] == "calibration_time" + + +def test_get_inherited_nodes(): + """Test to verify if we receive the right XML element list for a given NXDL path.""" + local_dir = Path(__file__).resolve().parent + nxdl_file_path = local_dir / "NXtest.nxdl.xml" + + elem = ET.parse(nxdl_file_path).getroot() + _, _, elist = nexus.get_inherited_nodes(nxdl_path="/ENTRY/NXODD_name", elem=elem) + assert len(elist) == 5 + + nxdl_file_path = ( + local_dir.parent.parent / "contributed_definitions" / "NXiv_temp.nxdl.xml" + ) + + elem = ET.parse(nxdl_file_path).getroot() + _, _, elist = nexus.get_inherited_nodes( + nxdl_path="/ENTRY/INSTRUMENT/ENVIRONMENT", elem=elem + ) + assert len(elist) == 4 + + _, _, elist = nexus.get_inherited_nodes( + nxdl_path="/ENTRY/INSTRUMENT/ENVIRONMENT/voltage_controller", elem=elem + ) + assert len(elist) == 6 + + _, _, elist = nexus.get_inherited_nodes( + nxdl_path="/ENTRY/INSTRUMENT/ENVIRONMENT/voltage_controller", + nx_name="NXiv_temp", + ) + assert len(elist) == 6 + + +@pytest.mark.parametrize( + "hdf_name,concept_name, name_type, should_fit", + [ + ("same_name", "same_name", "specified", True), + ("same_name", "same_name", "any", True), + ("same_name", "same_name", "partial", True), + ("source_pump", "source", "specified", False), + ("source_pump", "source", "any", True), + ("source_pump", "source", "partial", False), + ("source_pump", "sourceType", "specified", False), + ("source_pump", "sourceType", "any", True), + ("source_pump", "sourceType", "partial", False), + ("source_pump", "sourceTYPE", "specified", False), + ("source_pump", "sourceTYPE", "any", True), + ("source_pump", "sourceTYPE", "partial", True), + ("source pump", "sourceTYPE", "specified", False), + ("source pump", "sourceTYPE", "any", False), + ("source pump", "sourceTYPE", "partial", False), + ("Name with some whitespaces in it", "ENTRY", "specified", False), + ("Name with some whitespaces in it", "ENTRY", "any", False), + ("Name with some whitespaces in it", "ENTRY", "partial", False), + ("source", "sourceTYPE", "specified", False), + ("source", "sourceTYPE", "any", True), + ("source", "sourceTYPE", "partial", True), + ("SOURCE", "SOURCE", "specified", True), + ("SOURCE", "SOURCE", "any", True), + ("SOURCE", "SOURCE", "partial", True), + ("source123", "SOURCE", "specified", False), + ("source123", "SOURCE", "any", True), + ("source123", "SOURCE", "partial", True), + ("1source", "SOURCE", "specified", False), + ("1source", "SOURCE", "any", True), + ("1source", "SOURCE", "partial", True), + ("_source", "SOURCE", "specified", False), + ("_source", "SOURCE", "any", True), + ("_source", "SOURCE", "partial", True), + ("angular_energy_resolution", "angularNresolution", "specified", False), + ("angular_energy_resolution", "angularNresolution", "any", True), + ("angular_energy_resolution", "angularNresolution", "partial", True), + (".test", "TEST", "specified", False), + (".test", "TEST", "any", False), + (".test", "TEST", "partial", False), + ], +) +def test_namefitting(hdf_name, concept_name, name_type, should_fit): + """Test namefitting of nexus concept names""" + name_any = name_type == "any" + name_partial = name_type == "partial" + + if should_fit: + assert nexus.get_nx_namefit(hdf_name, concept_name, name_any, name_partial) > -1 + else: + assert ( + nexus.get_nx_namefit(hdf_name, concept_name, name_any, name_partial) == -1 + ) + + +@pytest.mark.parametrize( + "hdf_name,concept_name, score", + [ + ("test_name", "TEST_name", 9), + ("te_name", "TEST_name", 7), + ("my_other_name", "TEST_name", 5), + ("test_name", "test_name", 18), + ("test_other", "test_name", -1), + ("my_fancy_yet_long_name", "my_SOME_name", 8), + ("something", "XXXX", 0), + ("something", "OTHER", 1), + ], +) +def test_namefitting_scores(hdf_name, concept_name, score): + """Test namefitting of nexus concept names""" + assert nexus.get_nx_namefit(hdf_name, concept_name, name_partial=True) == score + + +@pytest.mark.parametrize( + "better_fit,better_ref,worse_fit,worse_ref", + [ + ("sourcetype", "sourceTYPE", "source_pump", "sourceTYPE"), + ("source_pump", "sourceTYPE", "source_pump", "TEST"), + ], +) +def test_namefitting_precedence(better_fit, better_ref, worse_fit, worse_ref): + """Test if namefitting follows proper precedence rules""" + + assert nexus.get_nx_namefit( + better_fit, better_ref, name_partial=True + ) > nexus.get_nx_namefit(worse_fit, worse_ref) + + +@pytest.mark.parametrize( + "string_obj, decode, expected", + [ + # Test with lists of bytes and strings + ([b"bytes", "string"], True, ["bytes", "string"]), + ([b"bytes", "string"], False, [b"bytes", "string"]), + ([b"bytes", b"more_bytes", "string"], True, ["bytes", "more_bytes", "string"]), + ( + [b"bytes", b"more_bytes", "string"], + False, + [b"bytes", b"more_bytes", "string"], + ), + ([b"fixed", b"length", b"strings"], True, ["fixed", "length", "strings"]), + ([b"fixed", b"length", b"strings"], False, [b"fixed", b"length", b"strings"]), + # Test with nested lists + ([[b"nested1"], [b"nested2"]], True, [["nested1"], ["nested2"]]), + ([[b"nested1"], [b"nested2"]], False, [[b"nested1"], [b"nested2"]]), + # Test with bytes + (b"single", True, "single"), + (b"single", False, b"single"), + # Test with str + ("single", True, "single"), + ("single", False, "single"), + # Test with int + (123, True, 123), + (123, False, 123), + ], +) +def test_decode_or_not(string_obj, decode, expected): + # Handle normal cases + result = nexus.decode_or_not(elem=string_obj, decode=decode) + if isinstance(expected, list): + assert isinstance(result, list), f"Expected list, but got {type(result)}" + # Handle all other cases + else: + assert result == expected, f"Failed for {string_obj} with decode={decode}" From dbb9a0a1bd537b7e93b12c4f1ec267b3ca3bdd7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:10:28 +0000 Subject: [PATCH 2/7] Bump black from 26.1 to 26.3.1 Bumps [black](https://github.com/psf/black) from 26.1 to 26.3.1. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/26.1.0...26.3.1) --- updated-dependencies: - dependency-name: black dependency-version: 26.3.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ba3eb8068a..2f45a3c7b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,6 @@ h5py pytest # Code style and auto-formatting -black==26.1 +black==26.3.1 flake8==7.3 isort==7.0 From b4fc213d4f1a6cb65af1f1aa224f2b70b8e7b5fc Mon Sep 17 00:00:00 2001 From: Marius Retegan Date: Tue, 17 Mar 2026 13:49:57 +0100 Subject: [PATCH 3/7] Add .readthedocs.yaml --- .gitignore | 1 + .readthedocs.yaml | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.gitignore b/.gitignore index 1e25594542..fbf3fd897e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Hidden files .* !.github +!.readthedocs.yaml # Python byte / compiled / optimized *.py[cod] diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..fe110f1895 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,12 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + commands: + - pip install -r requirements.txt + - make prepare + - make html + - mkdir -p $READTHEDOCS_OUTPUT/html + - cp -r build/manual/build/html/* $READTHEDOCS_OUTPUT/html/ From 6788d1eed49d945aaf81bef0ec988db16b5978de Mon Sep 17 00:00:00 2001 From: Marius Retegan Date: Tue, 17 Mar 2026 16:38:34 +0100 Subject: [PATCH 4/7] Remove iref from NXtrans --- base_classes/NXtrans.nxdl.xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/base_classes/NXtrans.nxdl.xml b/base_classes/NXtrans.nxdl.xml index fa364cb8a2..346b06cf2f 100644 --- a/base_classes/NXtrans.nxdl.xml +++ b/base_classes/NXtrans.nxdl.xml @@ -94,18 +94,6 @@ - - - Detector measuring the reference intensity - :math:`I_{ref}`, placed after a reference sample - (typically a metal foil). - - - - - - - Description of how the intensity was obtained from From ab0425e9a53647cbbbfb16a8968f5891defd4653 Mon Sep 17 00:00:00 2001 From: Marius Retegan Date: Tue, 17 Mar 2026 22:21:19 +0100 Subject: [PATCH 5/7] Remove deprecated NXxas_mode --- base_classes/NXxas_mode.nxdl.xml | 145 ------------------------------- 1 file changed, 145 deletions(-) delete mode 100644 base_classes/NXxas_mode.nxdl.xml diff --git a/base_classes/NXxas_mode.nxdl.xml b/base_classes/NXxas_mode.nxdl.xml deleted file mode 100644 index 02da979ea5..0000000000 --- a/base_classes/NXxas_mode.nxdl.xml +++ /dev/null @@ -1,145 +0,0 @@ - - - - - XAS measurement mode - - - X-ray absorption spectroscopy (XAS) is a technique that measures the absorption coefficient :math:`\mu(E)` of a material as a function of energy. - - The name of the XAS mode indicates the type of process being monitored to obtain the spectrum. Below is a description of the available modes, with emphasis on the expected values for the `intensity` and `monitor` fields. - - 1. Transmission - - The absorption coefficient is obtained by measuring the intensity of the incident :math:`I_0` and transmitted beam :math:`I`. - - .. math:: - \mu(E) = -\ln(I/I_0) - - 2. Total fluorescence yield (TFY) - - The absorption coefficient is obtained by measuring the intensity of the emitted fluorescence :math:`I_f` and the incident beam :math:`I_0`. - - .. math:: - \mu(E) \propto I_f/I_0 - - 3. Partial fluorescence yield (PFY) - - 4. Inverse partial fluorescence yield (IPFY) - - 5. High-energy resolution fluorescence detection (HERFD) - - 6. Total electron yield (TEY) - - 7. Partial electron yield (PEY) - - 8. Electron energy loss (EELS) - - 9. X-ray Raman Scattering (XRS) - - 10. Diffraction Anomalous Fine Structure (DAFS) - - 11. X-ray Excited Optical Luminescence (XEOL) - - 12. Grazing Angle Reflection Extended X-ray Absorption Fine Structure (ReflEXAFS) - - 13. Other - - - - - Transmission - - - - - Total Fluorescence Yield - - - - - Partial Fluorescence Yield - - - - - Inverse Partial Fluorescence Yield - - - - - High Energy Resolution Fluorescence Detected - - - - - Total Electron Yield - - - - - Partial Electron Yield - - - - - Electron Energy Loss - - - - - X-ray Raman Scattering - - - - - Diffraction Anomalous Fine Structure - - - - - X-ray Excited Optical Luminescence - - - - - Grazing Angle Reflection Extended X-ray Absorption Fine Structure - - - - - Other - - - - - - - Collection of emission lines detected or used in this measurement. - - - - - From f0abab21d8d2adb0c66a6dac460947477450ca94 Mon Sep 17 00:00:00 2001 From: Marius Retegan Date: Tue, 17 Mar 2026 22:31:40 +0100 Subject: [PATCH 6/7] Add an optional NXcollection for generic data --- applications/NXxas.nxdl.xml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/applications/NXxas.nxdl.xml b/applications/NXxas.nxdl.xml index a5f33f3229..10ef59dfd6 100644 --- a/applications/NXxas.nxdl.xml +++ b/applications/NXxas.nxdl.xml @@ -28,6 +28,9 @@ Number of energy data points + + Number of raw data channels + Application definition for X-ray absorption spectroscopy (XAS). @@ -103,6 +106,25 @@ - + + + Table like data structure common in the XAS domain. + + + + + + + + + + + + + + + + + From 3b7faf21a084e428a500538700e8730be359eb6b Mon Sep 17 00:00:00 2001 From: Marius Retegan Date: Tue, 17 Mar 2026 22:32:06 +0100 Subject: [PATCH 7/7] Remove deprecated NXxas_new --- applications/NXxas_new.nxdl.xml | 162 -------------------------------- 1 file changed, 162 deletions(-) delete mode 100644 applications/NXxas_new.nxdl.xml diff --git a/applications/NXxas_new.nxdl.xml b/applications/NXxas_new.nxdl.xml deleted file mode 100644 index 1c2df512ee..0000000000 --- a/applications/NXxas_new.nxdl.xml +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - The symbol(s) listed here will be used below to coordinate datasets with the same shape. - - - Number of energy data points - - - Number of electronic transitions - - - - This is an application definition for X-ray absorption spectroscopy. - - - - Official NeXus NXDL schema to which this file conforms. TODO: replace NXxas - - - - - - - - - Excited element - - - Absorption edge - - - Specify if the data commes from a calculation - - - TODO - - - - - - TODO - - - - - - TODO - - - - - - - Descriptive name of the sample - - - - - Description on how :ref:`energy </NXxas_new/ENTRY/energy-field>` - and :ref:`intensity </NXxas_new/ENTRY/intensity-field>` were obtained - from the raw data. - - - - - - - - - - - - - - - - - - - - - spacing between crystal planes of the reflection - - - Type or material of monochromating substance (Si, Ge, Multilayer). - - - Miller indices (hkl) values of nominal reflection - - - - - - - - - - - - - - - - - - - - - - - XAS intensity versus energy plot - - - - - - Table like data structure common in the XAS domain. - - - - - - - - - - - - - - - - - - -