コード例 #1
0
 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
コード例 #2
0
 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
コード例 #3
0
    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]]] = {}
コード例 #4
0
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
コード例 #5
0
def test_json_warning_container_raises_type_error_if_constructor_is_given_NoneType(
):
    with pytest.raises(TypeError):
        JsonWarningsContainer(None)
コード例 #6
0
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
コード例 #7
0
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
コード例 #8
0
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
コード例 #9
0
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
コード例 #10
0
def test_json_warning_container_is_correctly_instantiated_when_providing_warning(
):
    this_container = JsonWarningsContainer(JSON_WARNING)
    assert this_container
コード例 #11
0
def test_json_warning_container_is_correctly_instantiated_when_providing_correct_other_container(
):
    this_container = JsonWarningsContainer(JSON_WARN_CONTAINER)
    assert this_container
コード例 #12
0
def test_json_warnings_container_raises_type_error_when_instantiating_with_wrong_type(
):
    with pytest.raises(TypeError):
        JsonWarningsContainer([INVALID_CONTAINER_ELEMENT])
コード例 #13
0
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):
コード例 #14
0
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",
        )
コード例 #15
0
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)