def create_transformation(trans_type: TransformationType): file = NexusWrapper(str(uuid.uuid4())) ds = file.nexus_file.create_dataset("transform", data=8) t = Transformation(file, ds) t.type = trans_type t.vector = QVector3D(1, 0, 0) return t
def test_dependents_list_is_created_by_instrument(file, nexus_wrapper): """ The dependents list for transforms is stored in the "dependent_of" attribute, which is not part of the NeXus standard, we therefore cannot rely on it being present and correct in a file we load. This test makes sure that instrument generates this information in the wrapped NeXus file it is given. """ # Create minimal test file with some transformations but no "dependent_of" attributes entry_group = file.create_group("entry") entry_group.attrs["NX_class"] = "NXentry" instrument_group = entry_group.create_group("instrument") instrument_group.attrs["NX_class"] = "NXinstrument" transforms_group = instrument_group.create_group("transformations") transforms_group.attrs["NX_class"] = "NXtransformations" transform_1 = transforms_group.create_dataset("transform_1", data=42) transform_2 = transforms_group.create_dataset("transform_2", data=42) transform_3 = transforms_group.create_dataset("transform_3", data=42) transform_4 = transforms_group.create_dataset("transform_4", data=42) transform_2.attrs["depends_on"] = transform_1.name transform_3.attrs["depends_on"] = transform_2.name transform_4.attrs["depends_on"] = transform_2.name nexus_wrapper.load_file(entry_group, file) Instrument(nexus_wrapper, NX_CLASS_DEFINITIONS) transform_1_loaded = Transformation(nexus_wrapper, transform_1) assert ( len(transform_1_loaded.dependents) == 1 ), "Expected transform 1 to have a registered dependent (transform 2)" transform_2_loaded = Transformation(nexus_wrapper, transform_2) assert ( len(transform_2_loaded.dependents) == 2 ), "Expected transform 2 to have 2 registered dependents (transforms 3 and 4)"
def test_transform_dependents_depends_on_are_updated_when_transformation_name_is_changed( nexus_wrapper, ): test_name = "slartibartfast" test_value = 42 test_vector = QVector3D(1.0, 0.0, 0.0) test_type = "Translation" transform_dataset = _add_transform_to_file(nexus_wrapper, test_name, test_value, test_vector, test_type) component = nexus_wrapper.create_nx_group("test", "NXaperture", nexus_wrapper.nexus_file) component.create_dataset("depends_on", data=transform_dataset.name) transform = Transformation(nexus_wrapper, transform_dataset) transform.register_dependent(Component(nexus_wrapper, component)) new_name = test_name + "1" transform.name = new_name assert transform.name == new_name assert str(component["depends_on"][()], encoding="UTF-8") == transform.dataset.name
def add_rotation( self, axis: QVector3D, angle: float, name: str = None, depends_on: Transformation = None, ) -> Transformation: """ Note, currently assumes angle is in degrees :param axis: axis :param angle: :param name: Name of the rotation group (Optional) :param depends_on: existing transformation which the new one depends on (otherwise relative to origin) """ transforms_group = self.file.create_transformations_group_if_does_not_exist( self.group) if name is None: name = _generate_incremental_name(TransformationType.ROTATION, transforms_group) field = self.file.set_field_value(transforms_group, name, angle, float) self.file.set_attribute_value(field, CommonAttrs.UNITS, "degrees") self.file.set_attribute_value(field, CommonAttrs.VECTOR, qvector3d_to_numpy_array(axis)) self.file.set_attribute_value(field, CommonAttrs.TRANSFORMATION_TYPE, TransformationType.ROTATION) rotation_transform = Transformation(self.file, field) rotation_transform.depends_on = depends_on rotation_transform.ui_value = angle return rotation_transform
def add_translation(self, vector: QVector3D, name: str = None, depends_on: Transformation = None) -> Transformation: """ Note, currently assumes translation is in metres :param vector: direction and magnitude of translation as a 3D vector :param name: name of the translation group (Optional) :param depends_on: existing transformation which the new one depends on (otherwise relative to origin) """ transforms_group = self.file.create_transformations_group_if_does_not_exist( self.group) if name is None: name = _generate_incremental_name(TransformationType.TRANSLATION, transforms_group) unit_vector, magnitude = _normalise(vector) field = self.file.set_field_value(transforms_group, name, magnitude, float) self.file.set_attribute_value(field, CommonAttrs.UNITS, "m") self.file.set_attribute_value(field, CommonAttrs.VECTOR, qvector3d_to_numpy_array(unit_vector)) self.file.set_attribute_value(field, CommonAttrs.TRANSFORMATION_TYPE, TransformationType.TRANSLATION) translation_transform = Transformation(self.file, field) translation_transform.ui_value = magnitude translation_transform.depends_on = depends_on return translation_transform
def _get_transform( self, depends_on: str, transforms: List[Transformation], local_only: bool = False, ): """ Recursive function, appends each transform in depends_on chain to transforms list :param depends_on: The next depends_on string to find the next transformation in the chain :param transforms: The list to populate with transformations :param local_only: If True then only add transformations which are stored within this component """ if depends_on is not None and depends_on != ".": transform_dataset = self.file.nexus_file[depends_on] if (local_only and transform_dataset.parent.parent.name != self.absolute_path): # We're done, the next transformation is not stored in this component return new_transform = Transformation(self.file, transform_dataset) new_transform.parent = transforms transforms.append(new_transform) if CommonAttrs.DEPENDS_ON in transform_dataset.attrs.keys(): self._get_transform( self.file.get_attribute_value(transform_dataset, CommonAttrs.DEPENDS_ON), transforms, local_only, )
def refresh_depends_on(_, node): """ Refresh the depends_on attribute of each transformation, which also results in registering dependents """ if isinstance(node, h5py.Group): if CommonAttrs.NX_CLASS in node.attrs.keys(): if node.attrs[CommonAttrs.NX_CLASS] == "NXtransformations": for transformation_name in node: transform = Transformation( self.nexus, node[transformation_name] ) transform.depends_on = transform.depends_on
def depends_on(self, transformation: Transformation): existing_depends_on = self.file.get_attribute_value( self.group, CommonAttrs.DEPENDS_ON) if existing_depends_on is not None: Transformation( self.file, self.file[existing_depends_on]).deregister_dependent(self) if transformation is None: self.file.set_field_value(self.group, CommonAttrs.DEPENDS_ON, ".", str) else: self.file.set_field_value(self.group, CommonAttrs.DEPENDS_ON, transformation.absolute_path, str) transformation.register_dependent(self)
def test_can_get_rotation_as_4_by_4_matrix(nexus_wrapper): test_value = 45.0 # degrees test_vector = QVector3D(0.0, 1.0, 0.0) # around y-axis test_type = "Rotation" dataset = _add_transform_to_file(nexus_wrapper, "test_transform", test_value, test_vector, test_type) transformation = Transformation(nexus_wrapper, dataset) test_matrix = transformation.qmatrix # for a rotation around the y-axis: test_value_radians = np.deg2rad(test_value) expected_matrix = np.array(( np.cos(-test_value_radians), 0, np.sin(-test_value_radians), 0, 0, 1, 0, 0, -np.sin(-test_value_radians), 0, np.cos(-test_value_radians), 0, 0, 0, 0, 1, )) assert np.allclose(expected_matrix, np.array(test_matrix.data()), atol=1.0e-7)
def create_transform(nexus_file, name): initial_value = 42 initial_vector = QVector3D(1.0, 0.0, 0.0) initial_type = "Translation" dataset = _add_transform_to_file(nexus_file, name, initial_value, initial_vector, initial_type) return Transformation(nexus_file, dataset)
def test_dependee_of_contains_both_components_when_generating_dependee_of_chain_with_mixture_of_absolute_and_relative_paths( file, # noqa: F811 ): entry_group = file.create_group("entry") entry_group.attrs["NX_class"] = "NXentry" instrument_group = entry_group.create_group("instrument") instrument_group.attrs["NX_class"] = "NXinstrument" component_a = instrument_group.create_group("a") component_a.attrs["NX_class"] = "NXaperture" transforms_group = component_a.create_group("Transforms1") transform_1 = transforms_group.create_dataset("transform1", data=1.0) # Relative path to transform component_a.create_dataset("depends_on", data="Transforms1/transform1") component_b = instrument_group.create_group("b") component_b.attrs["NX_class"] = "NXaperture" # Absolute path to transform component_b.create_dataset( "depends_on", data="/entry/instrument/a/Transforms1/transform1") nexus_wrapper = NexusWrapper("test_dependent_transforms_1") nexus_wrapper.load_file(entry_group, file) Instrument(nexus_wrapper, NX_CLASS_DEFINITIONS) transform_1_loaded = Transformation(nexus_wrapper, transform_1) # Check both relative and absolute are in dependee_of list assert component_a.name in transform_1_loaded.dataset.attrs[ "NCdependee_of"] assert component_b.name in transform_1_loaded.dataset.attrs[ "NCdependee_of"]
def test_multiple_relative_transform_paths_are_converted_to_absolute_path_in_dependee_of_field( file, nexus_wrapper): component_name = "component_1" component1 = add_component_to_file(nexus_wrapper, component_name=component_name) # make depends_on point to relative transformations group component1.group["depends_on"] = "transformations/transform1" transformations_group = component1.group.create_group("transformations") transform1_name = "transform1" transform1_dataset = transformations_group.create_dataset(transform1_name, data=1) transform1_dataset.attrs[CommonAttrs.VECTOR] = qvector3d_to_numpy_array( QVector3D(1, 0, 0)) transform1_dataset.attrs[ CommonAttrs.TRANSFORMATION_TYPE] = TransformationType.TRANSLATION transform2_name = "transform2" # make transform1 depends_on point to relative transform in same directory transform1_dataset.attrs["depends_on"] = transform2_name transform2_dataset = transformations_group.create_dataset(transform2_name, data=2) transform2_dataset.attrs[CommonAttrs.VECTOR] = qvector3d_to_numpy_array( QVector3D(1, 1, 0)) transform2_dataset.attrs[ CommonAttrs.TRANSFORMATION_TYPE] = TransformationType.TRANSLATION # make sure the depends_on points to the absolute path of the transform it depends on in the file assert (Transformation( nexus_wrapper, transform1_dataset).depends_on.dataset.name == transform2_dataset.name)
def test_can_get_transform_properties(nexus_wrapper): test_name = "slartibartfast" test_value = 42 test_vector = QVector3D(1.0, 0.0, 0.0) test_type = "Translation" transform_dataset = _add_transform_to_file(nexus_wrapper, test_name, test_value, test_vector, test_type) transform = Transformation(nexus_wrapper, transform_dataset) assert ( transform.name == test_name ), "Expected the transform name to match what was in the NeXus file" assert ( transform.ui_value == test_value ), "Expected the transform value to match what was in the NeXus file" assert ( transform.vector == test_vector ), "Expected the transform vector to match what was in the NeXus file" assert ( transform.type == test_type ), "Expected the transform type to match what was in the NeXus file"
def test_transform_type_is_capitalised(test_input, nexus_wrapper): test_name = "slartibartfast" test_value = 42 test_vector = QVector3D(1.0, 0.0, 0.0) transform_dataset = _add_transform_to_file(nexus_wrapper, test_name, test_value, test_vector, test_input) transform = Transformation(nexus_wrapper, transform_dataset) assert transform.type == "Translation"
def test_can_get_translation_as_4_by_4_matrix(nexus_wrapper): test_value = 42.0 # Note, it should not matter if this is not set to a unit vector test_vector = QVector3D(2.0, 0.0, 0.0) test_type = "Translation" dataset = _add_transform_to_file(nexus_wrapper, "test_transform", test_value, test_vector, test_type) transformation = Transformation(nexus_wrapper, dataset) test_matrix = transformation.qmatrix expected_matrix = np.array( (1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, test_value, 0, 0, 1)) assert np.allclose(expected_matrix, np.array(test_matrix.data()))
def test_ui_value_for_transform_with_array_magnitude_returns_first_value( nexus_wrapper): transform_name = "transform1" array = [1.1, 2.2, 3.3] transform_value = np.asarray(array, dtype=float) transform_dataset = _add_transform_to_file( nexus_wrapper, transform_name, transform_value, QVector3D(1, 0, 0), TransformationType.TRANSLATION, ) transformation = Transformation(nexus_wrapper, transform_dataset) assert transformation.ui_value == array[0]
def test_ui_value_for_transform_with_array_magnitude_of_strings_returns_zero( nexus_wrapper, ): transform_name = "transform1" array = ["a1", "b1", "c1"] transform_value = np.asarray(array, dtype=h5py.special_dtype(vlen=str)) transform_dataset = _add_transform_to_file( nexus_wrapper, transform_name, transform_value, QVector3D(1, 0, 0), TransformationType.TRANSLATION, ) transformation = Transformation(nexus_wrapper, transform_dataset) assert transformation.ui_value == 0
def remove_transformation(self, transform: Transformation): if not self._transform_is_in_this_component(transform): raise PermissionError( "Transform is not in this component, do not have permission to delete" ) dependents = transform.get_dependents() if dependents: raise DependencyError( f"Cannot delete transformation, it is a dependency of {dependents}" ) # Remove whole transformations group if this is the only transformation in it if len(transform._dataset.parent.keys()) == 1: self.file.delete_node(transform._dataset.parent) # Otherwise just remove the transformation from the group else: self.file.delete_node(transform._dataset)
def test_dependent_is_created_by_instrument_if_depends_on_is_relative( file, nexus_wrapper): entry_group = file.create_group("entry") entry_group.attrs["NX_class"] = "NXentry" monitor_group = entry_group.create_group("monitor1") monitor_group.attrs["NX_class"] = "NXmonitor" monitor_group.create_dataset("depends_on", data=b"transformations/translation1") transformations_group = monitor_group.create_group("transformations") transformations_group.attrs["NX_class"] = "NXtransformations" transform_1 = transformations_group.create_dataset("translation1", data=1) nexus_wrapper.load_file(entry_group, file) Instrument(nexus_wrapper, NX_CLASS_DEFINITIONS) transform_1_loaded = Transformation(nexus_wrapper, transform_1) assert transform_1_loaded.dataset.attrs["NCdependee_of"][ 0] == "/entry/monitor1"
def test_transforms_with_no_dependees_return_None_for_depends_on( file, nexus_wrapper): component_name = "component_1" component1 = add_component_to_file(nexus_wrapper, component_name=component_name) # make depends_on point to relative transformations group component1.group["depends_on"] = "transformations/transform1" transformations_group = component1.group.create_group("transformations") transform1_name = "transform1" transform1_dataset = transformations_group.create_dataset(transform1_name, data=1) transform1_dataset.attrs[CommonAttrs.VECTOR] = qvector3d_to_numpy_array( QVector3D(1, 0, 0)) transform1_dataset.attrs[ CommonAttrs.TRANSFORMATION_TYPE] = TransformationType.TRANSLATION transform1_dataset.attrs["depends_on"] = "." transformation = Transformation(nexus_wrapper, transform1_dataset) assert not transformation.depends_on
def depends_on(self): depends_on_path = self.file.get_field_value(self.group, CommonAttrs.DEPENDS_ON) if depends_on_path is None: return None return Transformation(self.file, self.file.nexus_file[depends_on_path])