From b594984335700aa1dd38747c045a812e141777dc Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Thu, 25 Jun 2026 11:50:36 +0200 Subject: [PATCH 1/3] feat: tighter validation on depends_on targets --- src/chexus/validators.py | 35 ++++++++++++++++++++++++++++++-- tests/validators_test.py | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/chexus/validators.py b/src/chexus/validators.py index 951cb29..3af6975 100644 --- a/src/chexus/validators.py +++ b/src/chexus/validators.py @@ -50,7 +50,7 @@ def validate(self, node: Dataset | Group) -> Violation | None: if name not in start.children: return Violation(node.name, f"depends_on target {target} is missing") start = start.children[name] - if not is_transformation(start): + if not is_complete_transformation(start): return Violation( node.name, f"depends_on target {target} is not a transformation" ) @@ -217,6 +217,15 @@ def is_transformation(node: Dataset | Group) -> bool: return "transformation_type" in node.attrs and "vector" in node.attrs +def is_complete_transformation(node: Dataset | Group) -> bool: + return ( + is_transformation(node) + and "depends_on" in node.attrs + # Check that the transformation is type NX_NUMBER + and (isinstance(node, Dataset) or node.attrs.get('NX_class') == 'NXlog') + ) + + class transformation_offset_units_missing(Validator): def __init__(self) -> None: super().__init__( @@ -378,7 +387,7 @@ def validate(self, node: Dataset | Group) -> Violation | None: if not np.isin(event_ids, detector_numbers).all(): diff = np.setdiff1d(event_ids, detector_numbers) return Violation( - node.name, 'event_id:s that are not in detector_number: ' f'{diff}' + node.name, f'event_id:s that are not in detector_number: {diff}' ) @@ -533,6 +542,27 @@ def validate(self, node: Group | Dataset) -> Violation | None: return Violation(node.name, "NXevent_data must have event dataset(s)") +class children_of_nxtransformations_are_nxnumber(Validator): + def __init__(self) -> None: + super().__init__( + "NXtransformations_child_not_NX_NUMBER", + "children of NXtransformations must be NX_NUMBER", + ) + + def applies_to(self, node: Dataset | Group) -> bool: + return ( + isinstance(node, Group) + and node.parent is not None + and node.parent.attrs.get("NX_class") == "NXtransformations" + ) + + def validate(self, node: Dataset | Group) -> Violation | None: + if not node.attrs.get("NX_class") == "NXlog": + return Violation( + node.name, "child of NXtransformation is not NXlog or dataset" + ) + + def base_validators(*, has_scipp=True): validators = [ depends_on_missing(), @@ -554,6 +584,7 @@ def base_validators(*, has_scipp=True): NXdetector_pixel_offsets_are_unambiguous(), event_index_is_eight_bytes(), event_data_group_has_event_datasets(), + children_of_nxtransformations_are_nxnumber(), ] if has_scipp: validators += [ diff --git a/tests/validators_test.py b/tests/validators_test.py index f2f070e..5fafacb 100644 --- a/tests/validators_test.py +++ b/tests/validators_test.py @@ -57,6 +57,30 @@ def test_depends_on_target_missing(): assert result.name == "x/transform" +def test_depends_on_target_requires_depends_on_attribute(): + group = chexus.Group(name="x", attrs={"NX_class": "NXdetector"}) + depends_on = chexus.Dataset( + name="x/depends_on", value="transform", shape=None, dtype=str, parent=group + ) + transform = chexus.Dataset( + name="x/transform", + value=None, + shape=None, + dtype=float, + parent=group, + attrs={ + "transformation_type": "translation", + "vector": [1.0, 0.0, 0.0], + }, + ) + group.children = {"depends_on": depends_on, "transform": transform} + + result = chexus.validators.depends_on_target_missing().validate(depends_on) + + assert isinstance(result, chexus.Violation) + assert result.name == "x/depends_on" + + def test_float_dataset_units_missing(): good = chexus.Dataset( name="x", @@ -905,3 +929,23 @@ def test_event_data_has_event_datasets_bad_empty() -> None: chexus.validators.event_data_group_has_event_datasets().validate(parent), chexus.Violation, ) + + +def test_children_of_nxtransformations_are_nxnumber() -> None: + parent = chexus.Group( + name="transformations", attrs={"NX_class": "NXtransformations"} + ) + good = chexus.Group( + name="transformations/good", attrs={"NX_class": "NXlog"}, parent=parent + ) + bad = chexus.Group( + name="transformations/bad", attrs={"NX_class": "NXcollection"}, parent=parent + ) + + validator = chexus.validators.children_of_nxtransformations_are_nxnumber() + + assert not validator.applies_to(parent) + assert validator.applies_to(good) + assert validator.validate(good) is None + assert validator.applies_to(bad) + assert isinstance(validator.validate(bad), chexus.Violation) From 08dd951bb0a4ad6fe78bb2d5190bedf17b66a288 Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Thu, 25 Jun 2026 11:57:39 +0200 Subject: [PATCH 2/3] fix: also apply validator to dataset children --- src/chexus/validators.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/chexus/validators.py b/src/chexus/validators.py index 3af6975..9868155 100644 --- a/src/chexus/validators.py +++ b/src/chexus/validators.py @@ -551,13 +551,12 @@ def __init__(self) -> None: def applies_to(self, node: Dataset | Group) -> bool: return ( - isinstance(node, Group) - and node.parent is not None + node.parent is not None and node.parent.attrs.get("NX_class") == "NXtransformations" ) def validate(self, node: Dataset | Group) -> Violation | None: - if not node.attrs.get("NX_class") == "NXlog": + if not (isinstance(node, Dataset) or node.attrs.get("NX_class") == "NXlog"): return Violation( node.name, "child of NXtransformation is not NXlog or dataset" ) From 69c3847b3b32f5205388fa8653d8dca3f9a88fee Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Thu, 25 Jun 2026 11:59:00 +0200 Subject: [PATCH 3/3] test --- tests/validators_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/validators_test.py b/tests/validators_test.py index 5fafacb..1c45bfe 100644 --- a/tests/validators_test.py +++ b/tests/validators_test.py @@ -935,6 +935,13 @@ def test_children_of_nxtransformations_are_nxnumber() -> None: parent = chexus.Group( name="transformations", attrs={"NX_class": "NXtransformations"} ) + dataset = chexus.Dataset( + name="transformations/dataset", + value=1.0, + shape=None, + dtype=float, + parent=parent, + ) good = chexus.Group( name="transformations/good", attrs={"NX_class": "NXlog"}, parent=parent ) @@ -945,6 +952,8 @@ def test_children_of_nxtransformations_are_nxnumber() -> None: validator = chexus.validators.children_of_nxtransformations_are_nxnumber() assert not validator.applies_to(parent) + assert validator.applies_to(dataset) + assert validator.validate(dataset) is None assert validator.applies_to(good) assert validator.validate(good) is None assert validator.applies_to(bad)