Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions src/chexus/nexus_base_classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
"""NeXus base class names.

Source: https://github.com/nexusformat/definitions/tree/main/base_classes
Regenerated by ``tools/refresh_nexus_base_classes.py``; do not edit by hand.

Includes deprecated classes (NXgeometry, NXorientation, NXshape, NXtranslation),
which are flagged separately by NX_class_is_legacy.
"""

nexus_base_classes = frozenset(
[
"NXaberration",
"NXactivity",
"NXactuator",
"NXaperture",
"NXapm_charge_state_analysis",
"NXapm_event_data",
"NXapm_instrument",
"NXapm_measurement",
"NXapm_ranging",
"NXapm_reconstruction",
"NXapm_simulation",
"NXatom",
"NXattenuator",
"NXbeam",
"NXbeam_stop",
"NXbeam_transfer_matrix_table",
"NXbending_magnet",
"NXcalibration",
"NXcapillary",
"NXcg_alpha_complex",
"NXcg_cylinder",
"NXcg_ellipsoid",
"NXcg_face_list_data_structure",
"NXcg_grid",
"NXcg_half_edge_data_structure",
"NXcg_hexahedron",
"NXcg_parallelogram",
"NXcg_point",
"NXcg_polygon",
"NXcg_polyhedron",
"NXcg_polyline",
"NXcg_primitive",
"NXcg_roi",
"NXcg_tetrahedron",
"NXcg_triangle",
"NXcg_unit_normal",
"NXchemical_composition",
"NXcircuit",
"NXcite",
"NXcollection",
"NXcollectioncolumn",
"NXcollimator",
"NXcomponent",
"NXcoordinate_system",
"NXcorrector_cs",
"NXcrystal",
"NXcs_computer",
"NXcs_filter_boolean_mask",
"NXcs_memory",
"NXcs_prng",
"NXcs_processor",
"NXcs_profiling",
"NXcs_profiling_event",
"NXcs_storage",
"NXcylindrical_geometry",
"NXdata",
"NXdeflector",
"NXdetector",
"NXdetector_channel",
"NXdetector_group",
"NXdetector_module",
"NXdisk_chopper",
"NXdistortion",
"NXebeam_column",
"NXelectromagnetic_lens",
"NXelectron_detector",
"NXelectronanalyzer",
"NXem_ebsd",
"NXem_eds",
"NXem_eels",
"NXem_event_data",
"NXem_img",
"NXem_instrument",
"NXem_interaction_volume",
"NXem_measurement",
"NXem_optical_system",
"NXem_simulation",
"NXenergydispersion",
"NXentry",
"NXenvironment",
"NXevent_data",
"NXfabrication",
"NXfermi_chopper",
"NXfilter",
"NXfit",
"NXfit_function",
"NXflipper",
"NXfresnel_zone_plate",
"NXgeometry",
"NXgrating",
"NXguide",
"NXhistory",
"NXibeam_column",
"NXimage",
"NXinsertion_device",
"NXinstrument",
"NXlog",
"NXmanipulator",
"NXmirror",
"NXmoderator",
"NXmonitor",
"NXmonochromator",
"NXnote",
"NXobject",
"NXoff_geometry",
"NXoptical_lens",
"NXoptical_window",
"NXorientation",
"NXparameters",
"NXpdb",
"NXpeak",
"NXphase",
"NXpid_controller",
"NXpinhole",
"NXpolarizer",
"NXpositioner",
"NXprocess",
"NXprogram",
"NXpump",
"NXreflections",
"NXregistration",
"NXresolution",
"NXroi_process",
"NXroot",
"NXrotations",
"NXsample",
"NXsample_component",
"NXscan_controller",
"NXsensor",
"NXshape",
"NXslit",
"NXsource",
"NXspectrum",
"NXspindispersion",
"NXsubentry",
"NXtransformations",
"NXtranslation",
"NXunit_cell",
"NXuser",
"NXvelocity_selector",
"NXwaveplate",
"NXxraylens",
]
)
28 changes: 27 additions & 1 deletion src/chexus/validators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
import numpy as np

from .nexus_base_classes import nexus_base_classes
from .tree import Dataset, Group
from .validate import Validator, Violation

Expand Down Expand Up @@ -73,6 +74,30 @@ def validate(self, node: Dataset | Group) -> Violation | None:
return Violation(node.name, f"NX_class {nx_class} is deprecated")


# Application/contributed classes expected in raw ESS files.
# Add to this set if a new application class is encountered.
nexus_application_classes = frozenset(["NXcanSAS"])

valid_nx_classes = nexus_base_classes | nexus_application_classes


class NX_class_invalid(Validator):
def __init__(self) -> None:
super().__init__(
"NX_class_invalid",
"NX_class is not a known NeXus class name "
"(typos like NXPositioner instead of NXpositioner)",
)

def applies_to(self, node: Dataset | Group) -> bool:
return isinstance(node, Group) and "NX_class" in node.attrs

def validate(self, node: Dataset | Group) -> Violation | None:
nx_class = node.attrs["NX_class"]
if nx_class not in valid_nx_classes:
return Violation(node.name, f"Unknown NX_class {nx_class!r}")


class group_has_units(Validator):
def __init__(self) -> None:
super().__init__("group_has_units", "Group should not have units attribute")
Expand Down Expand Up @@ -497,6 +522,7 @@ def base_validators(*, has_scipp=True):
mask_has_units(),
non_numeric_dataset_has_units(),
NX_class_attr_missing(),
NX_class_invalid(),
NX_class_is_legacy(),
transformation_depends_on_missing(),
transformation_offset_units_missing(),
Expand Down
27 changes: 27 additions & 0 deletions tests/validators_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,33 @@ def test_NX_class_attr_missing():
assert result.name == "x"


@pytest.mark.parametrize(
"nx_class",
["NXpositioner", "NXdisk_chopper", "NXgeometry", "NXcanSAS"],
)
def test_NX_class_invalid_accepts_known(nx_class: str):
group = chexus.Group(name="x", attrs={"NX_class": nx_class})
assert chexus.validators.NX_class_invalid().applies_to(group)
assert chexus.validators.NX_class_invalid().validate(group) is None


@pytest.mark.parametrize(
"nx_class",
["NXPositioner", "NXDetector", "NXfoo", "positioner"],
)
def test_NX_class_invalid_rejects_unknown(nx_class: str):
group = chexus.Group(name="x", attrs={"NX_class": nx_class})
assert chexus.validators.NX_class_invalid().applies_to(group)
result = chexus.validators.NX_class_invalid().validate(group)
assert isinstance(result, chexus.Violation)
assert result.name == "x"


def test_NX_class_invalid_does_not_apply_without_attr():
group = chexus.Group(name="x", attrs={})
assert not chexus.validators.NX_class_invalid().applies_to(group)


def test_NX_class_is_legacy():
good = chexus.Group(name="x", attrs={"NX_class": "NXtransformations"})
assert chexus.validators.NX_class_is_legacy().validate(good) is None
Expand Down
77 changes: 77 additions & 0 deletions tools/refresh_nexus_base_classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
"""Regenerate ``src/chexus/nexus_base_classes.py`` from the upstream NeXus
definitions repository.

Requires the GitHub CLI (``gh``) to be installed and authenticated.

Usage:
python tools/refresh_nexus_base_classes.py
"""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path

REPO = "nexusformat/definitions"
DIRECTORY = "base_classes"
SUFFIX = ".nxdl.xml"
TARGET = (
Path(__file__).resolve().parent.parent / "src" / "chexus" / "nexus_base_classes.py"
)

HEADER = '''# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
"""NeXus base class names.

Source: https://github.com/nexusformat/definitions/tree/main/base_classes
Regenerated by ``tools/refresh_nexus_base_classes.py``; do not edit by hand.

Includes deprecated classes (NXgeometry, NXorientation, NXshape, NXtranslation),
which are flagged separately by NX_class_is_legacy.
"""

nexus_base_classes = frozenset(
[
'''

FOOTER = """ ]
)
"""


def fetch_names() -> list[str]:
result = subprocess.run(
["gh", "api", f"repos/{REPO}/contents/{DIRECTORY}", "--jq", ".[].name"],
capture_output=True,
text=True,
check=True,
)
names = [
line.removesuffix(SUFFIX)
for line in result.stdout.splitlines()
if line.endswith(SUFFIX)
]
return sorted(names)


def render(names: list[str]) -> str:
body = "".join(f' "{name}",\n' for name in names)
return HEADER + body + FOOTER


def main() -> int:
names = fetch_names()
if not names:
print("No base classes returned from gh api", file=sys.stderr)
return 1
TARGET.write_text(render(names))
print(f"Wrote {len(names)} base classes to {TARGET}")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading