def __init__(self, component: Component, shape_info: Dict): self.component = component self.shape_info = shape_info self.warnings = JsonWarningsContainer() self.error_message = "" self.issue_message = "" self.shape = None
def __init__( self, parent_component: Component, children: list, transforms_with_dependencies: Dict[TransformId, Tuple[Transformation, Optional[TransformId]]], ): """ Reads transformations from a JSON dictionary :param parent_component: The parent component that the transformations should be added to :param children: The children of the component entry :param transforms_with_dependencies: TransformationReader appends transforms and depends_on details to this dictionary so that depends_on can be set to the correct Transformation object after all transformations have been loaded """ self.parent_component = parent_component self.children = children self.warnings = JsonWarningsContainer() self._transforms_with_dependencies = transforms_with_dependencies
def __init__(self): self.entry_node: Group = None self.model = Model() self.sample_name: str = "" self.warnings = JsonWarningsContainer() # key: TransformId for transform which has a depends on # value: the Transformation object itself and the TransformId for the Transformation which it depends on # Populated while loading the transformations so that depends_on property of each Transformation can be set # to the appropriate Transformation after all the Transformations have been created, otherwise they would # need to be created in a particular order self._transforms_depends_on: Dict[TransformId, Tuple[Transformation, Optional[TransformId]]] = {} # key: name of the component (uniquely identifies Component) # value: the Component object itself and the TransformId for the Transformation which it depends on # Populated while loading the components so that depends_on property of each Component can be set to the # appropriate Transformation after all the Transformations have been created, otherwise they would # need to be created in a particular order self._components_depends_on: Dict[str, Tuple[Component, Optional[TransformId]]] = {}
class JSONReader: def __init__(self): self.entry_node: Group = None self.model = Model() self.sample_name: str = "" self.warnings = JsonWarningsContainer() # key: TransformId for transform which has a depends on # value: the Transformation object itself and the TransformId for the Transformation which it depends on # Populated while loading the transformations so that depends_on property of each Transformation can be set # to the appropriate Transformation after all the Transformations have been created, otherwise they would # need to be created in a particular order self._transforms_depends_on: Dict[TransformId, Tuple[Transformation, Optional[TransformId]]] = {} # key: name of the component (uniquely identifies Component) # value: the Component object itself and the TransformId for the Transformation which it depends on # Populated while loading the components so that depends_on property of each Component can be set to the # appropriate Transformation after all the Transformations have been created, otherwise they would # need to be created in a particular order self._components_depends_on: Dict[str, Tuple[Component, Optional[TransformId]]] = {} def _set_components_depends_on(self): """ Once all transformations have been loaded we should be able to set each component's depends_on property without worrying that the Transformation dependency has not been created yet """ for ( component_name, ( component, depends_on_id, ), ) in self._components_depends_on.items(): try: # If it has a dependency then find the corresponding Transformation and assign it to # the depends_on property if depends_on_id is not None: component.depends_on = self._transforms_depends_on[ depends_on_id][0] except KeyError: self.warnings.append( TransformDependencyMissing( f"Component {component_name} depends on {depends_on_id.transform_name} in component " f"{depends_on_id.component_name}, but that transform was not successfully loaded from the JSON" )) def _set_transforms_depends_on(self): """ Once all transformations have been loaded we should be able to set their depends_on property without worrying that the Transformation dependency has not been created yet """ for ( transform_id, ( transform, depends_on_id, ), ) in self._transforms_depends_on.items(): try: # If it has a dependency then find the corresponding Transformation and assign it to # the depends_on property if depends_on_id is not None: transform.depends_on = self._transforms_depends_on[ depends_on_id][0] except KeyError: self.warnings.append( TransformDependencyMissing( f"Transformation {transform_id.transform_name} in component {transform_id.component_name} " f"depends on {depends_on_id.transform_name} in component {depends_on_id.component_name}, " f"but that transform was not successfully loaded from the JSON" )) def _append_transformations_to_nx_group(self): """ Correctly adds the transformations in the components, with respect to the instance of TransformationList, to transformation nexus group. """ for component in self.model.get_components(): transformation_list = component.transforms transformation_children = [] for child in component.children: if isinstance(child, Group) and child.nx_class == NX_TRANSFORMATIONS: for transformation in transformation_list: transformation_children.append(transformation) child.children = transformation_children def _set_transformation_links(self): """ Adds transformation links to the components where a link exists. """ for component in self.model.get_components(): nx_transformation_group = None for child in component.children: if isinstance(child, Group) and child.nx_class == NX_TRANSFORMATIONS: for childs_child in child.children: attrs = childs_child.attributes if attrs.get_attribute_value(CommonAttrs.DEPENDS_ON): nx_transformation_group = child component.transforms.has_link = True break break if nx_transformation_group: transformation_list = component.transforms transformation_list.link.parent_node = nx_transformation_group nx_transformation_group.children.append( transformation_list.link) def load_model_from_json(self, filename: str) -> bool: """ Tries to load a model from a JSON file. :param filename: The filename of the JSON file. :return: True if the model was loaded without problems, False otherwise. """ with open(filename, "r") as json_file: try: json_dict = json.load(json_file) except JSONDecodeError as exception: self.warnings.append( InvalidJson( f"Provided file not recognised as valid JSON. Exception: {exception}" )) return False return self._load_from_json_dict(json_dict) def _load_from_json_dict(self, json_dict: Dict) -> bool: self.entry_node = self._read_json_object( json_dict[CommonKeys.CHILDREN][0]) for child in self.entry_node.children: if isinstance(child, (Dataset, Link, Group)): self.model.entry[child.name] = child else: self.model.entry.children.append(child) child.parent_node = self.model.entry self._set_transforms_depends_on() self._set_components_depends_on() self._append_transformations_to_nx_group() self._set_transformation_links() return True def _replace_placeholder(self, placeholder: str): if placeholder in PLACEHOLDER_WITH_NX_CLASSES: nx_class = PLACEHOLDER_WITH_NX_CLASSES[placeholder] name = placeholder.replace("$", "").lower() return { CommonKeys.NAME: name, CommonKeys.TYPE: NodeType.GROUP, CommonKeys.ATTRIBUTES: [{ CommonKeys.NAME: CommonAttrs.NX_CLASS, CommonKeys.DATA_TYPE: "string", CommonKeys.VALUES: nx_class, }], CommonKeys.CHILDREN: [], } return None def _read_json_object(self, json_object: Dict, parent_node: Group = None): """ Tries to create a component based on the contents of the JSON file. :param json_object: A component from the JSON dictionary. :param parent_name: The name of the parent object. Used for warning messages if something goes wrong. """ nexus_object: Union[Group, FileWriterModule] = None use_placeholder = False if isinstance(json_object, str) and json_object in PLACEHOLDER_WITH_NX_CLASSES: json_object = self._replace_placeholder(json_object) if not json_object: return use_placeholder = True if (CommonKeys.TYPE in json_object and json_object[CommonKeys.TYPE] == NodeType.GROUP): try: name = json_object[CommonKeys.NAME] except KeyError: self._add_object_warning(CommonKeys.NAME, parent_node) return None nx_class = _find_nx_class(json_object.get(CommonKeys.ATTRIBUTES)) if nx_class == SAMPLE_CLASS_NAME: self.sample_name = name if not self._validate_nx_class(name, nx_class): self._add_object_warning(f"valid Nexus class {nx_class}", parent_node) if nx_class in COMPONENT_TYPES: nexus_object = Component(name=name, parent_node=parent_node) children_dict = json_object[CommonKeys.CHILDREN] self._add_transform_and_shape_to_component( nexus_object, children_dict) self.model.append_component(nexus_object) else: nexus_object = Group(name=name, parent_node=parent_node) nexus_object.nx_class = nx_class if CommonKeys.CHILDREN in json_object: for child in json_object[CommonKeys.CHILDREN]: node = self._read_json_object(child, nexus_object) if node and isinstance(node, StreamModule): nexus_object.children.append(node) elif node and node.name not in nexus_object: nexus_object[node.name] = node elif CommonKeys.MODULE in json_object and NodeType.CONFIG in json_object: module_type = json_object[CommonKeys.MODULE] if (module_type == WriterModules.DATASET.value and json_object[NodeType.CONFIG][CommonKeys.NAME] == CommonAttrs.DEPENDS_ON): nexus_object = None elif module_type in [x.value for x in WriterModules]: nexus_object = create_fw_module_object( module_type, json_object[NodeType.CONFIG], parent_node) nexus_object.parent_node = parent_node else: self._add_object_warning("valid module type", parent_node) return None elif json_object == USERS_PLACEHOLDER: self.model.entry.users_placeholder = True return None else: self._add_object_warning( f"valid {CommonKeys.TYPE} or {CommonKeys.MODULE}", parent_node) # Add attributes to nexus_object. if nexus_object: json_attrs = json_object.get(CommonKeys.ATTRIBUTES) if json_attrs: attributes = Attributes() for json_attr in json_attrs: if not json_attr[CommonKeys.VALUES]: self._add_object_warning( f"values in attribute {json_attr[CommonKeys.NAME]}", parent_node, ) elif CommonKeys.DATA_TYPE in json_attr: attributes.set_attribute_value( json_attr[CommonKeys.NAME], json_attr[CommonKeys.VALUES], json_attr[CommonKeys.DATA_TYPE], ) elif CommonKeys.NAME in json_attr: attributes.set_attribute_value( json_attr[CommonKeys.NAME], json_attr[CommonKeys.VALUES]) nexus_object.attributes = attributes if (parent_node and isinstance(nexus_object, Dataset) and parent_node.nx_class == ENTRY_CLASS_NAME): self.model.entry[nexus_object.name] = nexus_object if isinstance(nexus_object, Group) and not nexus_object.nx_class: self._add_object_warning( f"valid {CommonAttrs.NX_CLASS}", parent_node, ) elif isinstance(nexus_object, Group) and nexus_object.nx_class == "NXuser": self.model.entry[nexus_object.name] = nexus_object if isinstance(nexus_object, Group): nexus_object.group_placeholder = use_placeholder return nexus_object def _add_object_warning(self, missing_info, parent_node): if parent_node: self.warnings.append( NameFieldMissing(f"Unable to find {missing_info} " f"for child of {parent_node.name}.")) else: self.warnings.append( NameFieldMissing( f"Unable to find object {missing_info} for NXEntry.")) def _validate_nx_class(self, name: str, nx_class: str) -> bool: """ Validates the NXclass by checking if it was found, and if it matches known NXclasses for components. :param name: The name of the component having its nx class validated. :param nx_class: The NXclass string obtained from the dictionary. :return: True if the NXclass is valid, False otherwise. """ if not nx_class: self.warnings.append( NXClassAttributeMissing( f"Unable to determine NXclass of component {name}.")) return False if nx_class not in NX_CLASSES: return False return True def _add_transform_and_shape_to_component(self, component, children_dict): # Add transformations if they exist. transformation_reader = TransformationReader( component, children_dict, self._transforms_depends_on) transformation_reader.add_transformations_to_component() self.warnings += transformation_reader.warnings depends_on_path = _find_depends_on_path(children_dict, component.name) if depends_on_path not in DEPENDS_ON_IGNORE: depends_on_id = TransformId( *get_component_and_transform_name(depends_on_path)) self._components_depends_on[component.name] = (component, depends_on_id) else: self._components_depends_on[component.name] = (component, None) # Add shape if there is a shape. shape_info = _find_shape_information(children_dict) if shape_info: shape_reader = ShapeReader(component, shape_info) shape_reader.add_shape_to_component() try: shape_reader.add_pixel_data_to_component(children_dict) except TypeError: # Will fail if not a detector shape pass self.warnings += shape_reader.warnings return component
def test_json_warning_container_raises_type_error_if_constructor_is_given_NoneType( ): with pytest.raises(TypeError): JsonWarningsContainer(None)
def test_json_warning_container_when_using_iadd_operator_with_incorrect_type(): with pytest.raises(TypeError): this_container = JsonWarningsContainer(JSON_WARN_CONTAINER) this_container += INVALID_CONTAINER_ELEMENT
def test_json_warning_container_if_using_iadd_operator_with_another_empty_json_warning_container( ): this_container = JsonWarningsContainer(JSON_WARN_CONTAINER) this_container += JsonWarningsContainer() assert len(this_container) == 1
def test_json_warning_container_when_using_iadd_operator_with_correct_type(): this_container = JsonWarningsContainer(JSON_WARN_CONTAINER) this_container += JsonWarningsContainer(JSON_WARNING) assert len(this_container) == 2
def test_json_warning_container_when_appending_another_container_containing_one_item( ): this_container = JsonWarningsContainer(JSON_WARNING) this_container.append(JSON_WARN_CONTAINER) assert len(this_container) == 2
def test_json_warning_container_is_correctly_instantiated_when_providing_warning( ): this_container = JsonWarningsContainer(JSON_WARNING) assert this_container
def test_json_warning_container_is_correctly_instantiated_when_providing_correct_other_container( ): this_container = JsonWarningsContainer(JSON_WARN_CONTAINER) assert this_container
def test_json_warnings_container_raises_type_error_when_instantiating_with_wrong_type( ): with pytest.raises(TypeError): JsonWarningsContainer([INVALID_CONTAINER_ELEMENT])
import pytest from nexus_constructor.json.json_warnings import InvalidJson, JsonWarningsContainer JSON_WARN_CONTAINER = JsonWarningsContainer() JSON_WARNING = InvalidJson("Invalid JSON warning") JSON_WARN_CONTAINER.append(JSON_WARNING) INVALID_CONTAINER_ELEMENT = "INVALID" def test_json_warnings_container_raises_type_error_when_instantiating_with_wrong_type( ): with pytest.raises(TypeError): JsonWarningsContainer([INVALID_CONTAINER_ELEMENT]) def test_json_warning_container_is_correctly_instantiated_when_providing_correct_other_container( ): this_container = JsonWarningsContainer(JSON_WARN_CONTAINER) assert this_container def test_json_warning_container_is_correctly_instantiated_when_providing_warning( ): this_container = JsonWarningsContainer(JSON_WARNING) assert this_container def test_json_warning_container_raises_type_error_if_appending_item_of_invalid_type( ): with pytest.raises(TypeError):
class ShapeReader: def __init__(self, component: Component, shape_info: Dict): self.component = component self.shape_info = shape_info self.warnings = JsonWarningsContainer() self.error_message = "" self.issue_message = "" self.shape = None def _get_shape_type(self): """ Tries to determine if the shape is an OFF or Cylindrical geometry. :return: The shape type i attribute if it could be found, otherwise an empty string is returned. """ try: return _find_nx_class(self.shape_info[CommonKeys.ATTRIBUTES]) except KeyError: return "" def add_shape_to_component(self): shape_type = self._get_shape_type() # An error message means the shape object couldn't be made self.error_message = f"Error encountered when constructing {shape_type} for component {self.component.name}:" # An issue message means something didn't add up self.issue_message = f"Issue encountered when constructing {shape_type} for component {self.component.name}:" if shape_type == OFF_GEOMETRY_NX_CLASS: self._add_off_shape_to_component() elif shape_type == CYLINDRICAL_GEOMETRY_NX_CLASS: self._add_cylindrical_shape_to_component() elif shape_type == GEOMETRY_NX_CLASS: for child in self.shape_info[CommonKeys.CHILDREN][0][CommonKeys.CHILDREN]: if ( child[NodeType.CONFIG][CommonKeys.NAME] == SHAPE_GROUP_NAME and child[NodeType.CONFIG][CommonKeys.VALUES] == NX_BOX ): self._add_box_shape_to_component() else: self.warnings.append( InvalidShape( f"Unrecognised shape type for component {self.component.name}. Expected '{OFF_GEOMETRY_NX_CLASS}' or " f"'{CYLINDRICAL_GEOMETRY_NX_CLASS}' but found '{shape_type}'." ) ) def _add_off_shape_to_component(self): """ Attempts to create an OFF Geometry and set this as the shape of the component. If the required information can be found and passes validation then the geometry is created and writen to the component, otherwise the function just returns without changing the component. """ children = self.children if not children: return name = self.name if not isinstance(children, list): self.warnings.append( InvalidShape( f"{self.error_message} Children attribute in shape group is not a list." ) ) return faces_dataset = self._get_shape_dataset_from_list(FACES, children) if not faces_dataset: return vertices_dataset = self._get_shape_dataset_from_list( CommonAttrs.VERTICES, children ) if not vertices_dataset: return winding_order_dataset = self._get_shape_dataset_from_list( WINDING_ORDER, children ) if not winding_order_dataset: return faces_dtype = self._find_and_validate_data_type(faces_dataset, INT_TYPES, FACES) faces_starting_indices = self._find_and_validate_values_list( faces_dataset, INT_TYPES, FACES ) if not faces_starting_indices: return units = self._find_and_validate_units(vertices_dataset) if not units: return self._find_and_validate_data_type( vertices_dataset, FLOAT_TYPES, CommonAttrs.VERTICES ) vertices = self._find_and_validate_values_list( vertices_dataset, FLOAT_TYPES, CommonAttrs.VERTICES ) if not vertices: return vertices = _convert_vertices_to_qvector3d(vertices) winding_order_dtype = self._find_and_validate_data_type( winding_order_dataset, INT_TYPES, WINDING_ORDER ) winding_order = self._find_and_validate_values_list( winding_order_dataset, INT_TYPES, WINDING_ORDER ) if not winding_order: return off_geometry = self.__create_off_geometry( faces_dtype, faces_starting_indices, name, units, vertices, winding_order, winding_order_dtype, ) self.component[name] = off_geometry self.shape = off_geometry @staticmethod def __create_off_geometry( faces_dtype, faces_starting_indices, name, units, vertices, winding_order, winding_order_dtype, ): off_geometry = OFFGeometryNexus(name) off_geometry.nx_class = OFF_GEOMETRY_NX_CLASS off_geometry.vertices = vertices off_geometry.units = units off_geometry.set_field_value(FACES, faces_starting_indices, faces_dtype) off_geometry.set_field_value( WINDING_ORDER, np.array(winding_order), winding_order_dtype ) return off_geometry def _add_box_shape_to_component(self): """ Attempts to create a box geometry and set this as the shape of the component. If the required information can be found and passes validation then the geometry is created and written to the component, otherwise the function just returns without changing the component. """ children = self.children[0][CommonKeys.CHILDREN] name = self.name if not children: return tmp_dict = {} for child in children: if NodeType.CONFIG in child: tmp_dict[child[NodeType.CONFIG][CommonKeys.NAME]] = child[ NodeType.CONFIG ] units = self.__get_units(children) size = tmp_dict[SIZE][CommonKeys.VALUES] box_geometry = BoxGeometry(size[0], size[1], size[2], name, units) self.component[name] = box_geometry self.shape = box_geometry # type:ignore @staticmethod def __get_units(children): for child in children: if CommonKeys.ATTRIBUTES in child: for attr in child[CommonKeys.ATTRIBUTES]: if attr[CommonKeys.NAME] == CommonAttrs.UNITS: return attr[CommonKeys.VALUES] return None def _add_cylindrical_shape_to_component(self): """ Attempts to create a cylindrical geometry and set this as the shape of the component. If the required information can be found and passes validation then the geometry is created and written to the component, otherwise the function just returns without changing the component. """ children = self.children if not children: return name = self.name vertices_dataset = self._get_shape_dataset_from_list( CommonAttrs.VERTICES, children ) if not vertices_dataset: return cylinders_dataset = self._get_shape_dataset_from_list(CYLINDERS, children) if not cylinders_dataset: return units = self._find_and_validate_units(vertices_dataset) if not units: return cylinders_dtype = self._find_and_validate_data_type( cylinders_dataset, INT_TYPES, CYLINDERS ) cylinders_list = self._find_and_validate_values_list( cylinders_dataset, INT_TYPES, CYLINDERS ) if not cylinders_list: return vertices_dtype = self._find_and_validate_data_type( vertices_dataset, FLOAT_TYPES, CommonAttrs.VERTICES ) vertices = self._find_and_validate_values_list( vertices_dataset, FLOAT_TYPES, CommonAttrs.VERTICES ) if not vertices: return cylindrical_geometry = self.__create_cylindrical_geometry( cylinders_dtype, cylinders_list, name, units, vertices, vertices_dtype ) self.component[name] = cylindrical_geometry self.shape = cylindrical_geometry @staticmethod def __create_cylindrical_geometry( cylinders_dtype, cylinders_list, name, units, vertices, vertices_dtype ): cylindrical_geometry = CylindricalGeometry(name) cylindrical_geometry.nx_class = CYLINDRICAL_GEOMETRY_NX_CLASS cylindrical_geometry.set_field_value( CYLINDERS, np.array(cylinders_list), cylinders_dtype ) cylindrical_geometry.set_field_value( CommonAttrs.VERTICES, np.vstack(vertices), vertices_dtype ) cylindrical_geometry[CommonAttrs.VERTICES].attributes.set_attribute_value( CommonAttrs.UNITS, units ) return cylindrical_geometry def _get_shape_dataset_from_list( self, dataset_name: str, children: List[Dict], warning: bool = True ) -> Union[Dict, None]: """ Tries to find a given shape dataset from a list of datasets. :param dataset_name: The name of the dataset that the function will search for. :param children: The children list where we expect to find the dataset. :return: The dataset if it could be found, otherwise None is returned. """ for dataset in children: try: if dataset[NodeType.CONFIG][CommonKeys.NAME] == dataset_name: return dataset except KeyError: pass if warning: self.warnings.append( InvalidShape( f"{self.error_message} Couldn't find {dataset_name} dataset." ) ) return None def _find_and_validate_data_type( self, dataset: Dict, expected_types: List[str], parent_name: str ) -> Union[str, None]: """ Checks if the type in the dataset attribute has an expected value. Failing this check does not stop the geometry creation. :param dataset: The dataset where we expect to find the type information. :param expected_types: The expected type that the dataset type field should contain. :param parent_name: The name of the parent dataset """ try: found_type = None if CommonKeys.DATA_TYPE in dataset[NodeType.CONFIG]: found_type = dataset[NodeType.CONFIG][CommonKeys.DATA_TYPE] elif CommonKeys.TYPE in dataset[NodeType.CONFIG]: found_type = dataset[NodeType.CONFIG][CommonKeys.TYPE] else: self.warnings.append( InvalidShape( f"{self.issue_message} Type attribute for {parent_name} not found." ) ) if found_type is not None and found_type not in expected_types: self.warnings.append( InvalidShape( f"{self.issue_message} Type attribute for {parent_name} does not match expected type(s) " f"{expected_types}." ) ) elif found_type is not None: return found_type except KeyError: self.warnings.append( InvalidShape( f"{self.issue_message} Unable to find type attribute for {parent_name}." ) ) return None def _find_and_validate_units(self, vertices_dataset: Dict) -> Union[str, None]: """ Attempts to retrieve and validate the units data. :param vertices_dataset: The vertices dataset. :return: Th units value if it was found and passed validation, otherwise None is returned. """ try: attributes_list = vertices_dataset[CommonKeys.ATTRIBUTES] except KeyError: self.warnings.append( InvalidShape( f"{self.error_message} Unable to find attributes list in vertices dataset." ) ) return None units = _find_attribute_from_list_or_dict(CommonAttrs.UNITS, attributes_list) if not units: self.warnings.append( InvalidShape( f"{self.error_message} Unable to find units attribute in vertices dataset." ) ) return None if not units_are_recognised_by_pint(units, False): self.warnings.append( InvalidShape( f"{self.error_message} Vertices units are not recognised by pint. Found {units}." ) ) return None if not units_are_expected_dimensionality(units, METRES, False): self.warnings.append( InvalidShape( f"{self.error_message} Vertices units have wrong dimensionality. Expected something that can be " f"converted to metred but found {units}. " ) ) return None if not units_have_magnitude_of_one(units, False): self.warnings.append( InvalidShape( f"{self.error_message} Vertices units do not have magnitude of one. Found {units}." ) ) return None return units def _all_in_list_have_expected_type( self, values: List, expected_types: List[str], list_parent_name: str ) -> bool: """ Checks if all the items in a given list have the expected type. :param values: The list of values. :param expected_types: The expected types. :param list_parent_name: The name of the dataset the list belongs to. :return: True of all the items in the list have the expected type, False otherwise. """ flat_array = np.array(values).flatten() if all( [ type(value) in [ numpy_dtype for human_readable_type, numpy_dtype in VALUE_TYPE_TO_NP.items() if human_readable_type in expected_types ] for value in flat_array ] ): return True self.warnings.append( InvalidShape( f"{self.error_message} Values in {list_parent_name} list do not all have type(s) {expected_types}." ) ) return False def _get_values_attribute( self, dataset: Dict, parent_name: str ) -> Union[List, None]: """ Attempts to get the values attribute in a dataset. Creates an error message if it cannot be found. :param dataset: The dataset we hope to find the values attribute in. :param parent_name: The name of the parent dataset. :return: The values attribute if it could be found, otherwise None is returned. """ try: return dataset[NodeType.CONFIG][CommonKeys.VALUES] except KeyError: self.warnings.append( InvalidShape( f"{self.error_message} Unable to find values in {parent_name} dataset." ) ) return None def _attribute_is_a_list(self, attribute: Any, parent_name: str) -> bool: """ Checks if an attribute has the type list. :param attribute: The attribute to check. :param parent_name: The name of the parent dataset. :return: True if attribute is a list, False otherwise. """ if isinstance(attribute, list): return True self.warnings.append( InvalidShape( f"{self.error_message} values attribute in {parent_name} dataset is not a list." ) ) return False @property def children(self) -> Union[List, None]: """ Attempts to get the children list from the shape dictionary. :return: The children list if it could be found, otherwise None is returned. """ try: return self.shape_info[CommonKeys.CHILDREN] except KeyError: self.warnings.append( InvalidShape( f"{self.error_message} Unable to find children list in shape group." ) ) return None @property def name(self) -> str: """ Attempts to get the name attribute from the shape dictionary. :return: The name if it could be found, otherwise 'shape' is returned. """ try: return self.shape_info[CommonKeys.NAME] except KeyError: self.warnings.append( InvalidShape( f"{self.issue_message} Unable to find name of shape. Will use 'shape'." ) ) return SHAPE_GROUP_NAME def _find_and_validate_values_list( self, dataset: Dict, expected_types: List[str], attribute_name: str ) -> Union[List, None]: """ Attempts to find and validate the contents of the values attribute from the dataset. :param dataset: The dataset containing the values list. :param expected_types: The type(s) we expect the values list to have. :param attribute_name: The name of the attribute. :return: The values list if it was found and passed validation, otherwise None is returned. """ values = self._get_values_attribute(dataset, attribute_name) if not values: return None if not self._attribute_is_a_list(values, attribute_name): return None if not self._all_in_list_have_expected_type( values, expected_types, attribute_name ): return None return values def add_pixel_data_to_component(self, children: List[Dict]): """ Attempts to find and write pixel information to the component. :param children: The JSON children list for the component. """ shape_has_pixel_grid = ( self.shape_info[CommonKeys.NAME] == PIXEL_SHAPE_GROUP_NAME ) self._get_detector_number(children, shape_has_pixel_grid) # return if the shape is not a pixel grid if not shape_has_pixel_grid: # Shape is pixel mapping self._handle_mapping(children) return for offset in [X_PIXEL_OFFSET, Y_PIXEL_OFFSET, Z_PIXEL_OFFSET]: self._find_and_add_pixel_offsets_to_component(offset, children) def _get_detector_number(self, children: List[Dict], shape_has_pixel_grid: bool): """ Attempt to find the detector_number in the component group, and if found apply it the model component. :param children: :param shape_has_pixel_grid: :return: """ detector_number_dataset = self._get_shape_dataset_from_list( DETECTOR_NUMBER, children, shape_has_pixel_grid ) if detector_number_dataset: detector_number_dtype = self._find_and_validate_data_type( detector_number_dataset, INT_TYPES, DETECTOR_NUMBER ) detector_number = self._find_and_validate_values_list( detector_number_dataset, INT_TYPES, DETECTOR_NUMBER ) if detector_number: self.component.set_field_value( DETECTOR_NUMBER, detector_number, detector_number_dtype ) if self.shape and isinstance(self.shape, CylindricalGeometry): self.shape.detector_number = detector_number def _handle_mapping(self, children: List[Dict]): shape_group = self._get_shape_dataset_from_list( SHAPE_GROUP_NAME, children, False ) if shape_group and self.shape: detector_faces_dataset = self._get_shape_dataset_from_list( DETECTOR_FACES, shape_group[CommonKeys.CHILDREN], False ) self.shape.detector_faces = detector_faces_dataset[CommonKeys.VALUES] def _find_and_add_pixel_offsets_to_component( self, offset_name: str, children: List[Dict] ): """ Attempts to find and add pixel offset data to the component. :param offset_name: The name of the pixel offset field. :param children: The JSON children list for the component. """ offset_dataset = self._get_shape_dataset_from_list( offset_name, children, offset_name != Z_PIXEL_OFFSET ) if not offset_dataset: return pixel_offset_dtype = self._find_and_validate_data_type( offset_dataset, FLOAT_TYPES, offset_name ) pixel_offset = self._find_and_validate_values_list( offset_dataset, FLOAT_TYPES, offset_name ) if not pixel_offset: return units = self.__get_units([offset_dataset]) self.component.set_field_value( offset_name, np.array(pixel_offset), pixel_offset_dtype, units if units else "m", )
class TransformationReader: def __init__( self, parent_component: Component, children: list, transforms_with_dependencies: Dict[TransformId, Tuple[Transformation, Optional[TransformId]]], ): """ Reads transformations from a JSON dictionary :param parent_component: The parent component that the transformations should be added to :param children: The children of the component entry :param transforms_with_dependencies: TransformationReader appends transforms and depends_on details to this dictionary so that depends_on can be set to the correct Transformation object after all transformations have been loaded """ self.parent_component = parent_component self.children = children self.warnings = JsonWarningsContainer() self._transforms_with_dependencies = transforms_with_dependencies def add_transformations_to_component(self): """ Attempts to construct Transformation objects using information from the JSON dictionary and then add them to the parent component. """ for item in self.children: if _is_transformation_group(item): try: self._create_transformations(item[CommonKeys.CHILDREN]) except KeyError as e: print("Error:", e) continue def _get_transformation_attribute( self, attribute_name: Union[str, List[str]], json_transformation: dict, transform_name: str = None, failure_value: Any = None, ) -> Any: """ Tries to find a certain attribute of a transformation from dictionary. :param attribute_name: The name of the attribute fields. :param json_transformation: The dictionary to look for the attribute in. :param transform_name: The name of the transformation (if known). :param failure_value: The value to return if the attribute cannot be found. :return: Returns the attribute or converted attribute if this exists in the dictionary, if the attribute is not found in the dictionary then the failure_value is returned. """ try: if isinstance(attribute_name, str): return json_transformation[attribute_name] else: for key in attribute_name: if key in json_transformation: return json_transformation[key] raise KeyError except KeyError: if transform_name: msg = ( f"Cannot find {attribute_name} for transformation in component" f" {transform_name}") f" {self.parent_component.name}." else: msg = f"Cannot find {attribute_name} for transformation in component" f" {self.parent_component.name}." self.warnings.append(TransformDependencyMissing(msg)) return failure_value def _find_attribute_in_list( self, attribute_name: str, transformation_name: str, attributes_list: list, failure_value: Any = None, ) -> Any: """ Searches the dictionaries in a list to see if one of them has a given attribute. :param attribute_name: The name of the attribute that is being looked for. :param transformation_name: The name of the transformation that is being constructed. :param attributes_list: The list of dictionaries. :param failure_value: The value to return if the attribute is not contained in any of the dictionaries. :return: The value of the attribute if is is found in the list, otherwise the failure value is returned. """ attribute = _find_attribute_from_list_or_dict(attribute_name, attributes_list) if not attribute: self.warnings.append( TransformDependencyMissing( f"Unable to find {attribute_name} attribute in transformation" f" {transformation_name} from component {self.parent_component.name}" )) return failure_value return attribute def _parse_dtype(self, dtype: str, transformation_name: str) -> str: """ Sees if the type value from the JSON matches the types on the value type dictionary. :param dtype: The type value obtained from the JSON. :return: The corresponding type from the dictionary if it exists, otherwise an empty string is returned. """ for key in VALUE_TYPE_TO_NP.keys(): if dtype.lower() == key.lower(): return key self.warnings.append( InvalidTransformation( f"Could not recognise dtype {dtype} from transformation" f" {transformation_name} in component {self.parent_component.name}." )) return "" def _parse_transformation_type( self, transformation_type: str, transformation_name: str) -> Union[TransformationType, str]: """ Converts the transformation type in the JSON to one recognised by the NeXus Constructor. :param transformation_type: The transformation type from the JSON. :param transformation_name: The name of the transformation that is being processed. :return: The matching TransformationType class value. """ try: return TRANSFORMATION_MAP[transformation_type.lower()] except KeyError: self.warnings.append( InvalidTransformation( f"Could not recognise transformation type {transformation_type} of" f" transformation {transformation_name} in component" f" {self.parent_component.name}.")) return "" def _create_transformations(self, json_transformations: list): """ Uses the information contained in the JSON dictionary to construct a list of Transformations. :param json_transformations: A list of JSON transformation entries. """ for json_transformation in json_transformations: is_nx_log = (CommonKeys.TYPE in json_transformation and json_transformation[CommonKeys.TYPE] == NodeType.GROUP) if is_nx_log: tmp = json_transformation[CommonKeys.CHILDREN][0] if CommonKeys.ATTRIBUTES in tmp: tmp[CommonKeys.ATTRIBUTES] += json_transformation[ CommonKeys.ATTRIBUTES] else: tmp[CommonKeys.ATTRIBUTES] = json_transformation[ CommonKeys.ATTRIBUTES] tmp[NodeType.CONFIG][CommonKeys.NAME] = json_transformation[ CommonKeys.NAME] json_transformation = tmp config = self._get_transformation_attribute( NodeType.CONFIG, json_transformation) if not config: continue module = self._get_transformation_attribute( CommonKeys.MODULE, json_transformation) if not module: continue name = self._get_transformation_attribute(CommonKeys.NAME, config) dtype = self._get_transformation_attribute( [CommonKeys.DATA_TYPE, CommonKeys.TYPE], config, name, ) if not dtype: continue dtype = self._parse_dtype(dtype, name) if not dtype: continue attributes = self._get_transformation_attribute( CommonKeys.ATTRIBUTES, json_transformation, name) if not attributes: continue units = self._find_attribute_in_list(CommonAttrs.UNITS, name, attributes) if not units: continue transformation_type = self._find_attribute_in_list( CommonAttrs.TRANSFORMATION_TYPE, name, attributes, ) if not transformation_type: continue transformation_type = self._parse_transformation_type( transformation_type, name) if not transformation_type: continue vector = self._find_attribute_in_list(CommonAttrs.VECTOR, name, attributes, [0.0, 0.0, 0.0]) # This attribute is allowed to be missing, missing is equivalent to the value "." which means # depends on origin (end of dependency chain) depends_on = _find_attribute_from_list_or_dict( CommonAttrs.DEPENDS_ON, attributes) if module == DATASET: values = self._get_transformation_attribute( CommonKeys.VALUES, config, name) if values is None: continue angle_or_magnitude = values values = _create_transformation_dataset( angle_or_magnitude, dtype, name) elif module in [writer_mod.value for writer_mod in WriterModules]: values = _create_transformation_datastream_group( json_transformation) angle_or_magnitude = 0.0 else: continue temp_depends_on = None transform = self.parent_component._create_and_add_transform( name=name, transformation_type=transformation_type, angle_or_magnitude=angle_or_magnitude, units=units, vector=QVector3D(*vector), depends_on=temp_depends_on, values=values, ) if depends_on not in DEPENDS_ON_IGNORE: depends_on_id = TransformId( *get_component_and_transform_name(depends_on)) self._transforms_with_dependencies[TransformId( self.parent_component.name, name)] = (transform, depends_on_id) else: self._transforms_with_dependencies[TransformId( self.parent_component.name, name)] = (transform, None)