コード例 #1
0
class Component:
    def __init__(self, json=None, device_ref: Device = None):
        """Creates a new Component object

        Args:
            json (dict, optional): json dict after json.loads(). Defaults to None.
            device_ref (Device, optional): pointer for the Device object. Defaults to None.

        Raises:
            Exception: [description]
        """
        self.name: str = ""
        self.ID: str = ""
        self.params = Params()
        self.entity: str = ""
        self.xspan: int = -1
        self.yspan: int = -1
        self.ports: List[Port] = []
        self.layers: List[Layer] = []

        if json is not None:
            if device_ref is None:
                raise Exception(
                    "Cannot Parse Component from JSON with no Device Reference, check device_ref parameter in constructor "
                )
            self.parse_from_json(json, device_ref)

    @property
    def xpos(self) -> int:
        """returns the x coordinate of the component

        Raises:
            KeyError: when no position parameter object is found for the parchmint object

        Returns:
            int: x-coordinate
        """
        try:
            return self.params.get_param("position")[0]
        except Exception:
            print("Could not find xpos for component")
            raise KeyError

    @xpos.setter
    def xpos(self, value) -> None:
        """Sets the x-coordinate for the component

        Args:
            value (int): x coordianate of the object
        """
        if self.params.exists("position"):
            pos = self.params.get_param("position")
            pos[0] = value
            self.params.set_param("position", pos)
        else:
            self.params.set_param("position", [value, -1])

    @property
    def ypos(self) -> int:
        """Returns the y-coordinate in the parchmint object

        Raises:
            KeyError: When no position parameter is found in the parchmint object

        Returns:
            int: y coordinate of the component
        """
        try:
            return self.params.get_param("position")[1]
        except Exception:
            print("Could not find xpos for component")
            raise KeyError

    @ypos.setter
    def ypos(self, value) -> None:
        """Sets the y-coordinate of the component

        Args:
            value (int): y coordinate
        """
        if self.params.exists("position"):
            pos = self.params.get_param("position")
            pos[1] = value
            self.params.set_param("position", pos)
        else:
            self.params.set_param("position", [-1, value])

    def add_component_ports(self, ports: List[Port]) -> None:
        """Adds component ports to the component

        Args:
            ports (List[Port]): list of port objects
        """
        for port in ports:
            self.ports.append(port)

    def parse_from_json(self, json, device_ref=None):
        """Parses from the json dict

        Args:
            json (dict): json dict after json.loads()
        """
        if device_ref is None:
            raise Exception(
                "Cannot Parse Component from JSON with no Device Reference, check device_ref parameter in constructor "
            )
        self.name = json["name"]
        self.ID = json["id"]
        self.entity = json["entity"]
        self.xspan = json["x-span"]
        self.yspan = json["y-span"]
        self.params = Params(json["params"])
        self.layers = [
            device_ref.get_layer(layer_id) for layer_id in json["layers"]
        ]

        for port in json["ports"]:
            self.ports.append(Port(port))

        if self.params:
            if self.params.exists("position"):
                self.xpos = self.params.get_param("position")[0]
                self.ypos = self.params.get_param("position")[1]

    def __str__(self):
        return str(self.__dict__)

    def __repr__(self):
        return str(self.__dict__)

    def to_parchmint_v1(self):
        """Returns the json dict

        Returns:
            dict: dictionary that can be used in json.dumps()
        """

        ret = {
            "name": self.name,
            "id": self.ID,
            "layers": [layer.ID for layer in self.layers],
            "params": self.params.to_parchmint_v1(),
            "ports": [p.to_parchmint_v1() for p in self.ports],
            "entity": self.entity,
            "x-span": int(self.xspan),
            "y-span": int(self.yspan),
        }

        return ret

    def __eq__(self, obj):
        if isinstance(obj, Component):
            return obj.ID == self.ID
        else:
            return False

    def get_port(self, label: str) -> Port:
        """Returns a port in the component identified by the corresponding label

        Args:
            label (str): label of the componentport

        Raises:
            Exception: if there is no component port with the corresponding label is found

        Returns:
            Port: component port
        """
        for port in self.ports:
            if label == port.label:
                return port
        raise Exception("Could not find port with the label: {}".format(label))

    def get_absolute_port_coordinates(self,
                                      port_label: str) -> Tuple[float, float]:
        """Gets the absolute coordinates of the component port identified by the label

        Args:
            port_label (str): unique identifier for the component port

        Returns:
            Tuple[float, float]: coordinates of the component port
        """
        port = self.get_port(port_label)
        x = self.xpos + port.x
        y = self.ypos + port.y
        return (x, y)
コード例 #2
0
class Device:
    """The device object is the top level object for describing a microfluidic device.
    It contains the entire list of components, connections and all the relationships
    between them"""

    def __init__(self, json_data=None):
        """Creates a new device object

        Args:
            json (dict, optional): json dict after json.loads(). Defaults to None.
        """
        self.name: str = ""
        self.components: List[Component] = []
        self.connections: List[Connection] = []
        self.layers: List[Layer] = []
        self.params: Params = Params()
        self.features: List[Feature] = []  # Store Raw JSON Objects for now
        self.G = nx.MultiDiGraph()

        # Stores the valve / connection mappings
        self._valve_map: Dict[Component, Connection] = {}
        self._valve_type_map: Dict[Component, ValveType] = {}

        if json_data:
            self.parse_from_json(json_data)

    @property
    def xspan(self) -> Optional[int]:
        """Returns the x span of the device
        Returns:
            int: x span of the device
        """
        return self.params.get_param("x-span") if self.params.exists("x-span") else None

    @xspan.setter
    def xspan(self, xspan: int) -> None:
        """Sets the x span of the device
        Args:
            xspan (int): x span of the device
        """
        self.params.set_param("x-span", xspan)

    @property
    def yspan(self) -> Optional[int]:
        """Returns the y span of the device
        Returns:
            int: y span of the device
        """
        return self.params.get_param("y-span") if self.params.exists("y-span") else None

    @yspan.setter
    def yspan(self, yspan: int) -> None:
        """Sets the y span of the device
        Args:
            yspan (int): y span of the device
        """
        self.params.set_param("y-span", yspan)

    def get_feature(self, feature_id: str) -> Feature:
        """Returns the feature object with the given name

        Args:
            name (str): name of the feature

        Returns:
            Feature: Feature object with the given name
        """
        for feature in self.features:
            if feature.ID == feature_id:
                return feature
        raise Exception("Feature not found")

    @property
    def valves(self) -> List[Component]:
        """Returns the valve components in the device

        Returns:
            List[Component]: List of valve components in the device
        """
        return list(self._valve_map.keys())

    def map_valve(
        self,
        valve: Component,
        connection: Connection,
        type_info: ValveType = ValveType.NORMALLY_OPEN,
    ) -> None:
        """Maps the valve to a connection in the device

        Args:
            valve (Component): valve component
            connection (Connection): connection on which the valve is mapped
            type_info (Optional[ValveType]): Type informaiton of the valve

        """
        self._valve_map[valve] = connection
        if type_info is not None:
            self.update_valve_type(valve, type_info)

    def get_valves(self) -> List[Component]:
        """Returns the list of valves in the device

        Returns:
            List[Component]: Valve Component Objects
        """
        return list(self._valve_map.keys())

    def get_valve_connection(self, valve: Component) -> Connection:
        """Returns the connection associated with the valve object

        Args:
            valve (Component): Valve object for which we are finding the connection

        Returns:
            Connection: connection object on which the valve is placed
        """
        return self._valve_map[valve]

    def update_valve_type(self, valve: Component, type_info: ValveType) -> None:
        """Updates the type of the valve to normally closed  or normally open

        Args:
            valve (Component): Valve object we want to update
            type_info (ValveType): Valve Type

        Raises:
            KeyError: Raises the error if the valve object is not mapped as a valve in the device
        """
        if valve in list(self._valve_map.keys()):
            self._valve_type_map[valve] = type_info
        else:
            raise KeyError(
                "Could not update type for valve: {} since it is not found in the valveMap of they device".format(
                    valve.ID
                )
            )

    def remove_valve(self, valve_id) -> None:
        """Removes the valve entry from the device, also deletes the component from the device's components

        Args:
            valve_id (str): ID of the valve to be removed
        """
        for valve in self.valves:
            if valve.ID == valve_id:
                self._valve_map.pop(valve)
                self._valve_type_map.pop(valve)
                break

        self.remove_component(valve_id)

    def compare(self, device: Device, ignore_parameter_diffs: bool = True) -> bool:
        """compare against the input device. Return true if they are semnatcally feasible.

        Args:
            device (Device): expected device
            ignore_parameter_diffs (bool): ignore parameter differences. Defaults to True.

        Returns:
            bool: If semntically feasible, return true. Else false.
        """

        SM = SimilarityMatcher(self, device)

        is_same = SM.is_isomorphic()
        SM.print_params_diff()
        SM.print_layers_diff()
        SM.print_port_diff()
        SM.print_in_edges_diff()
        SM.print_out_edges_diff()

        if is_same:
            print("Match!")
        else:
            print("Not Match!")

        return is_same

    def add_feature(self, feature: Feature) -> None:
        """Adds a feature to the device
        Args:
            feature (Feature): Feature object to be added
        """
        self.features.append(feature)

    def remove_feature(self, feature_id: str) -> None:
        """Removes a feature from the device
        Args:
            feature_id (str): ID of the feature to be removed
        Raises:
            Exception: Raises the error if the feature is not found in the device
        """
        for feature in self.features:
            if feature.ID == feature_id:
                self.features.remove(feature)
                return
        raise Exception("Feature not found")

    def add_component(self, component: Component) -> None:
        """Adds a component object to the device

        Args:
            component (Component): component to eb added

        Raises:
            Exception: if the passed object is not a Component instance
        """
        if isinstance(component, Component):
            # Check if Component Exists, if it does ignore it
            if self.component_exists(component.ID):
                print(
                    "Component {} already present in device, "
                    "hence skipping the component".format(component.name)
                )
            self.components.append(component)
            self.G.add_node(component.ID)
        else:
            raise Exception(
                "Could not add component since its not an instance of parchmint:Component"
            )

    def remove_component(self, component_id: str) -> None:
        """Removes a component object from the device

        Args:
            component_id (str): ID of the component to be removed

        Raises:
            Exception: Raises the error if the component is not found in the device
        """
        for component in self.components:
            if component.ID == component_id:
                self.components.remove(component)
                self.G.remove_node(component_id)
                return
        raise Exception("Component not found")

    def add_connection(self, connection: Connection) -> None:
        """Adds a connection object to the device

        Args:
            connection (Connection): connectin to add

        Raises:
            Exception: if the arg is not a Connection type object
        """
        if isinstance(connection, Connection):
            # Check if the source component is present in the device
            if connection.source is None:
                raise Exception("Connection source is not defined")

            if self.component_exists(connection.source.component) is False:
                raise Exception(
                    "Source component {} not found in the device while adding connection: {}".format(
                        connection.source, connection.ID
                    )
                )

            # Check if the connection sinks are defined / exist in the device
            if len(connection.sinks) == 0:
                print(
                    "Warning: No sinks defined for connection {}".format(
                        connection.name
                    )
                )

            for sink in connection.sinks:
                if self.component_exists(sink.component) is False:
                    raise Exception(
                        "Sink component {} not found in the device while adding connection: {}".format(
                            sink, connection.name
                        )
                    )

            self.connections.append(connection)
            # Connect the components associated here on the nx graph
            for sink in connection.sinks:
                self.G.add_edge(
                    connection.source.component,
                    sink.component,
                    source_port=connection.source,
                    sink_port=sink,
                    connection_ref=connection,
                    connection_id=connection.ID,
                )
        else:
            raise Exception(
                "Could not add component since its not an instance of parchmint:Connection"
            )

    def remove_connection(self, connection_id: str) -> None:
        """Removes a connection object from the device

        Args:
            connection_id (str): ID of the connection to be removed

        Raises:
            Exception: Raises the error if the connection is not found in the device
        """
        for connection in self.connections:
            if connection.ID == connection_id:
                self.connections.remove(connection)
                if connection.source is not None:
                    for sink in connection.sinks:
                        self.G.remove_edge(connection.source.component, sink.component)
                return
        raise Exception("Connection not found")

    def add_layer(self, layer: Layer) -> None:
        """Adds a layer to the device

        Args:
            layer (Layer): layer to be added to the device
        """
        if isinstance(layer, Layer):
            self.layers.append(layer)

    def remove_layer(self, layer_id: str) -> None:
        """Removes a layer from the device, also removes all the components and connections corresponding to the layer

        Args:
            layer_id (str): ID of the layer to be removed
        """
        layer_to_delete = None
        for layer in self.layers:
            if layer.ID == layer_id:
                layer_to_delete = layer

        if layer_to_delete is None:
            raise Exception("Layer not found")

        # Remove all the components and connections associated with the layer
        for component in self.components:
            if set([layer.ID for layer in component.layers]) == set(layer_to_delete.ID):
                self.remove_component(component.ID)
            else:
                warn(
                    "Skipped removing component {} from the device".format(component.ID)
                )

        for connection in self.connections:
            if connection.layer is None:
                continue
            if layer_to_delete.ID == connection.layer.ID:
                self.remove_connection(connection.ID)

    def get_layer(self, id: str) -> Layer:
        """Returns the layer with the corresponding id

        Args:
            id (str): id of the layer

        Raises:
            Exception: if a layer with the corresponding id is not present

        Returns:
            Layer: layer with the corresponding id
        """
        for layer in self.layers:
            if layer.ID == id:
                return layer
        raise Exception("Could not find the layer {}".format(id))

    def merge_netlist(self, netlist: Device) -> None:
        """Merges two netlists together. Currently assumes that both
        devices have the same ordering of layers

        Args:
            netlist (Device): netlist to merge
        """
        # TODO - Figure out how to merge the layers later
        # First create a map of layers
        layer_mapping = {}
        for layer in netlist.layers:
            if layer not in self.layers:
                self.add_layer(layer)
                layer_mapping[layer] = layer
            else:
                assert layer.ID is not None
                layer_mapping[layer] = self.get_layer(layer.ID)

        for component in netlist.components:
            new_layers = []
            for layer in component.layers:
                new_layers.append(layer_mapping[layer])
            component.layers = new_layers
            self.add_component(component)

        for connection in netlist.connections:
            connection.layer = layer_mapping[connection.layer]
            self.add_connection(connection)

    def parse_from_json(self, json_data) -> None:
        """Returns the json dict

        Returns:
            dict: dictionary that can be used in json.dumps()
        """
        self.name = json_data["name"]

        # First always add the layers
        if "layers" in json_data.keys():
            for layer in json_data["layers"]:
                self.add_layer(Layer(layer))
        else:
            print("no layers found")

        # Loop through the components
        if "components" in json_data.keys():
            for component in json_data["components"]:
                self.add_component(Component(json_data=component, device_ref=self))
        else:
            print("no components found")

        if "connections" in json_data.keys():
            for connection in json_data["connections"]:
                self.add_connection(Connection(json_data=connection, device_ref=self))
        else:
            print("no connections found")

        if "params" in json_data.keys():
            self.params = Params(json_data=json_data["params"])

            if self.params.exists("xspan"):
                self.xspan = self.params.get_param("xspan")
            elif self.params.exists("width"):
                self.xspan = self.params.get_param("width")
            elif self.params.exists("x-span"):
                self.xspan = self.params.get_param("x-span")

            if self.params.exists("yspan"):
                self.yspan = self.params.get_param("yspan")
            elif self.params.exists("length"):
                self.yspan = self.params.get_param("length")
            elif self.params.exists("y-span"):
                self.yspan = self.params.get_param("y-span")
        else:
            print("no params found")

        if "valveMap" in json_data.keys():
            valve_map = json_data["valveMap"]

            for key, value in valve_map.items():
                self._valve_map[self.get_component(key)] = self.get_connection(value)

        if "valveTypeMap" in json_data.keys():
            valve_type_map = json_data["valveTypeMap"]

            for key, value in valve_type_map.items():
                if value is ValveType.NORMALLY_OPEN:
                    self._valve_type_map[
                        self.get_component(key)
                    ] = ValveType.NORMALLY_OPEN
                else:
                    self._valve_type_map[
                        self.get_component(key)
                    ] = ValveType.NORMALLY_CLOSED

    def get_components(self) -> List[Component]:
        """Returns the components in the device

        Returns:
            List[Component]: list of components in the device
        """
        return self.components

    def get_connections(self) -> List[Connection]:
        """Returns the connections in the device

        Returns:
            List[Connection]: list of connections in the device
        """
        return self.connections

    def get_connection_between_components(self, source, sink) -> Connection:
        """Returns the connection between two components

        Args:
            source (Component): source component
            sink (Component): sink component

        Returns:
            Connection: connection between the two components
        """
        return self.G.get_edge_data(source, sink)["connection_ref"]

    def get_name_from_id(self, id: str) -> str:
        """Returns the name of the component with the corresponding id

        Args:
            id (str): id of the object

        Returns:
            Optional[str]: name of the corresponding object
        """
        for component in self.components:
            if component.ID == id:
                return component.name

        raise Exception("Could not find component with ID: {}".format(id))

    def component_exists(self, component_id: str) -> bool:
        """checks if component exists in the device

        Args:
            component_id (str): id of the component

        Returns:
            bool: true if the component exists
        """
        for component in self.components:
            if component_id == component.ID:
                return True

        return False

    def connection_exists(self, connection_id: str) -> bool:
        """checks if connection exists in the device

        Args:
            connection_id (str): id of the connection

        Returns:
            bool: true if the connection exists
        """
        for connection in self.connections:
            if connection_id == connection.ID:
                return True

        return False

    def get_component(self, id: str) -> Component:
        """Returns the component with the corresponding ID

        Args:
            id (str): id of the component

        Raises:
            Exception: if the component is not found

        Returns:
            Component: component with the corresponding id
        """
        for component in self.components:
            if component.ID == id:
                return component
        raise Exception("Could not find component with id {}".format(id))

    def get_connection(self, id: str) -> Connection:
        """Returns the connection with the corresponding id

        Args:
            id (str): id of the connection

        Raises:
            Exception: if the connection is not found

        Returns:
            Connection: connection with the corresponding id
        """
        for connection in self.connections:
            if connection.ID == id:
                return connection
        raise Exception("Could not find connection with id {}".format(id))

    def get_connections_for_edge(
        self, source: Component, sink: Component
    ) -> List[Connection]:
        """Returns the connections for the given edge

        Args:
            source (Component): source component
            sink (Component): sink component

        Returns:
            List[Connection]: list of connections for the given edge
        """
        try:
            return [
                edge["connection_ref"]
                for edge in list((self.G[source.ID][sink.ID]).values())
            ]
        except KeyError:
            print(
                "Warning ! - No connections found between {} and {}".format(
                    source, sink
                )
            )
            return []

    def get_connections_for_component(self, component: Component) -> List[Connection]:
        """Returns the connections for the given component

        Args:
            component (Component): component

        Returns:
            List[Connection]: list of connections for the given component
        """
        edge_list = list(self.G.in_edges(component.ID))
        edge_list.extend(list(self.G.out_edges(component.ID)))
        connections = [self.G.get_edge_data(*e)[0]["connection_ref"] for e in edge_list]
        return connections

    def __str__(self):
        return str(self.__dict__)

    def __repr__(self):
        return str(self.__dict__)

    def to_parchmint_v1(self):
        """Returns the json dict

        Returns:
            dict: dictionary that can be used in json.dumps()
        """
        ret = {}
        ret["name"] = self.name
        ret["components"] = [c.to_parchmint_v1() for c in self.components]
        ret["connections"] = [c.to_parchmint_v1() for c in self.connections]
        ret["params"] = self.params.to_parchmint_v1()
        ret["layers"] = [layer.to_parchmint_v1() for layer in self.layers]
        ret["version"] = 1

        return ret

    def to_parchmint_v1_x(self) -> Dict:
        """Generating the parchmint v1.2 of the device

        Returns:
            Dict: dictionary that can be used in json.dumps()
        """
        self.params.set_param("x-span", self.xspan)
        self.params.set_param("y-span", self.yspan)
        ret = {}
        ret["name"] = self.name
        ret["components"] = [c.to_parchmint_v1() for c in self.components]
        ret["connections"] = [c.to_parchmint_v1_x() for c in self.connections]
        ret["params"] = self.params.to_parchmint_v1()
        ret["layers"] = [layer.to_parchmint_v1() for layer in self.layers]

        ret["features"] = [feature.to_parchmint_v1_x() for feature in self.features]

        # Modify the version of the parchmint
        ret["version"] = "1.2"

        # Add the valvemap information
        valve_map = {}
        valve_type_map = {}
        for valve, connection in self._valve_map.items():
            valve_map[valve.ID] = connection.ID
        ret["valveMap"] = valve_map
        for valve, valve_type in self._valve_type_map.items():
            valve_type_map[valve.ID] = str(valve_type)
        ret["valveTypeMap"] = valve_type_map
        return ret

    @staticmethod
    def validate_V1(json_str: str) -> None:
        """Validates the json string against the schema

        Args:
            json_str (str): json string
        """
        schema_path = PROJECT_DIR.joinpath("schemas").joinpath("parchmint_v1.json")
        with open(schema_path) as json_file:
            schema = json.load(json_file)
            json_data = json.loads(json_str)
            validator = jsonschema.Draft7Validator(schema)

            errors = validator.iter_errors(json_data)  # get all validation errors

            for error in errors:
                print(error)
                print("------")
            else:
                print("No errors found")

    @staticmethod
    def validate_V1_2(json_str: str) -> None:
        """Validates the json string against the schema

        Args:
            json_str (str): json string
        """
        schema_path = PROJECT_DIR.joinpath("schemas").joinpath("parchmint_v1_2.json")
        with open(schema_path) as json_file:
            schema = json.load(json_file)
            json_data = json.loads(json_str)
            validator = jsonschema.Draft7Validator(schema)

            errors = validator.iter_errors(json_data)  # get all validation errors

            for error in errors:
                print(error)
                print("------")
            else:
                print("No errors found")
コード例 #3
0
ファイル: component.py プロジェクト: CIDARLAB/pyparchmint
class Component:
    """The component class describes all the components in the device."""
    def __init__(self, json_data=None, device_ref: Device = None):
        """Creates a new Component object

        Args:
            json (dict, optional): json dict after json.loads(). Defaults to None.
            device_ref (Device, optional): pointer for the Device object. Defaults to None.

        Raises:
            Exception: [description]
        """
        self.name: str = ""
        self.ID: str = ""
        self.params = Params()
        self.entity: str = ""
        self.xspan: int = -1
        self.yspan: int = -1
        self._ports: List[Port] = []
        self.layers: List[Layer] = []

        if json_data is not None:
            if device_ref is None:
                raise Exception(
                    "Cannot Parse Component from JSON with no Device Reference, check device_ref parameter in constructor "
                )
            self.parse_from_json(json_data, device_ref)

    @property
    def ports(self) -> List[Port]:
        """Returns the ports of the component

        Returns:
            List[Port]: list of ports
        """
        return self._ports

    @property
    def component_spacing(self) -> float:
        """Returns the component spacing

        Returns:
            float: component spacing
        """
        return self.params.get_param("componentSpacing")

    @component_spacing.setter
    def component_spacing(self, value: float):
        """Sets the component spacing

        Args:
            value (float): component spacing
        """
        self.params.set_param("componentSpacing", value)

    @property
    def xpos(self) -> int:
        """returns the x coordinate of the component

        Raises:
            KeyError: when no position parameter object is found for the parchmint object

        Returns:
            int: x-coordinate
        """
        try:
            return self.params.get_param("position")[0]
        except Exception:
            print("Could not find xpos for component")
            raise KeyError

    @xpos.setter
    def xpos(self, value) -> None:
        """Sets the x-coordinate for the component

        Args:
            value (int): x coordianate of the object
        """
        if self.params.exists("position"):
            pos = self.params.get_param("position")
            pos[0] = value
            self.params.set_param("position", pos)
        else:
            self.params.set_param("position", [value, -1])

    @property
    def ypos(self) -> int:
        """Returns the y-coordinate in the parchmint object

        Raises:
            KeyError: When no position parameter is found in the parchmint object

        Returns:
            int: y coordinate of the component
        """
        try:
            return self.params.get_param("position")[1]
        except Exception:
            print("Could not find xpos for component")
            raise KeyError

    @ypos.setter
    def ypos(self, value) -> None:
        """Sets the y-coordinate of the component

        Args:
            value (int): y coordinate
        """
        if self.params.exists("position"):
            pos = self.params.get_param("position")
            pos[1] = value
            self.params.set_param("position", pos)
        else:
            self.params.set_param("position", [-1, value])

    @property
    def rotation(self) -> float:
        """Returns the rotation of the component

        Raises:
            KeyError: when no rotation parameter is found

        Returns:
            int: rotation of the component
        """
        try:
            return self.params.get_param("rotation")
        except Exception:
            print("Could not find rotation for component")
            raise KeyError

    @rotation.setter
    def rotation(self, value):
        """Sets the rotation of the component

        Args:
            value (int): rotation of the component
        """
        self.params.set_param("rotation", value)

    def add_component_ports(self, ports: List[Port]) -> None:
        """Adds component ports to the component

        Args:
            ports (List[Port]): list of port objects
        """
        for port in ports:
            self.add_component_port(port)

    def add_component_port(self, port: Port) -> None:
        """Adds a component port to the component

        Args:
            port (Port): port object
        """
        self._ports.append(port)

    def parse_from_json(self, json, device_ref=None):
        """Parses from the json dict

        Args:
            json (dict): json dict after json.loads()
        """
        if device_ref is None:
            raise Exception(
                "Cannot Parse Component from JSON with no Device Reference, check device_ref parameter in constructor "
            )
        self.name = json["name"]
        self.ID = json["id"]
        self.entity = json["entity"]
        self.xspan = json["x-span"]
        self.yspan = json["y-span"]
        self.params = Params(json["params"])
        self.layers = [
            device_ref.get_layer(layer_id) for layer_id in json["layers"]
        ]

        for port in json["ports"]:
            self.add_component_port(Port(port))

        if self.params:
            if self.params.exists("position"):
                self.xpos = self.params.get_param("position")[0]
                self.ypos = self.params.get_param("position")[1]

    def __str__(self):
        return str(self.__dict__)

    def __repr__(self):
        return str(self.__dict__)

    def to_parchmint_v1(self):
        """Returns the json dict

        Returns:
            dict: dictionary that can be used in json.dumps()
        """
        # Set the position parameter if it doesnt exist, set it to -1, -1
        if not self.params.exists("position"):
            self.params.set_param("position", [-1, -1])
        ret = {
            "name": self.name,
            "id": self.ID,
            "layers": [layer.ID for layer in self.layers],
            "params": self.params.to_parchmint_v1(),
            "ports": [p.to_parchmint_v1() for p in self._ports],
            "entity": self.entity,
            "x-span": int(self.xspan),
            "y-span": int(self.yspan),
        }

        return ret

    def __eq__(self, obj):
        if isinstance(obj, Component):
            return obj.ID == self.ID
        else:
            return False

    def get_port(self, label: str) -> Port:
        """Returns a port in the component identified by the corresponding label

        Args:
            label (str): label of the componentport

        Raises:
            Exception: if there is no component port with the corresponding label is found

        Returns:
            Port: component port
        """
        for port in self._ports:
            if label == port.label:
                return port
        raise Exception("Could not find port with the label: {}".format(label))

    def get_absolute_port_coordinates(self,
                                      port_label: str) -> Tuple[float, float]:
        """Gets the absolute coordinates of the component port identified by the label

        Args:
            port_label (str): unique identifier for the component port

        Returns:
            Tuple[float, float]: coordinates of the component port
        """
        port = self.get_port(port_label)
        x = self.xpos + port.x
        y = self.ypos + port.y
        return (x, y)

    def __hash__(self) -> int:
        return hash(repr(self))

    def rotate_point(self, xpos: float, ypos: float,
                     angle: float) -> Tuple[float, float]:
        """Rotates a point around the topleft corner of the component clockwise

        Args:
            xpos (float): x coordinate of the point
            ypos (float): y coordinate of the point
            angle (float): angle of rotation in degrees

        Returns:
            Tuple[float, float]: A tuple containing the rotated coordinates
        """
        # Setup the center to be used the translation matrices
        center_x = self.xspan / 2
        center_y = self.yspan / 2

        # Setup all the corner points
        old_topLeft = np.array((0, 0, 1)).transpose()
        old_topRight = np.array((self.xspan, 0, 1)).transpose()
        old_bottomLeft = np.array((0, self.yspan, 1)).transpose()
        old_bottomRight = np.array((self.xspan, self.yspan, 1)).transpose()

        pos = np.array(((xpos), (ypos), (1)))

        T1 = np.array(((1, 0, -center_x), (0, 1, -center_y), (0, 0, 1)))

        theta = np.radians(angle)
        c, s = np.cos(theta), np.sin(theta)
        R = np.array(((c, -s, 0), (s, c, 0), (0, 0, 1)))
        T2 = np.array(((1, 0, center_x), (0, 1, center_y), (0, 0, 1)))

        # Rotate the topRight corner and the bottomLeft corner about the center
        rotated_topLeft = T2.dot(R.dot(T1.dot(old_bottomLeft)))
        rotated_topRight = T2.dot(R.dot(T1.dot(old_topLeft)))
        rotated_bottomRight = T2.dot(R.dot(T1.dot(old_topRight)))
        rotated_bottomLeft = T2.dot(R.dot(T1.dot(old_bottomRight)))

        # Find the new position of the topleft corner by finding the min of all the corner points
        xmin = min(
            rotated_topLeft[0],
            rotated_topRight[0],
            rotated_bottomLeft[0],
            rotated_bottomRight[0],
        )
        ymin = min(
            rotated_topLeft[1],
            rotated_topRight[1],
            rotated_bottomLeft[1],
            rotated_bottomRight[1],
        )

        T3 = np.array(((1, 0, -xmin), (0, 1, -ymin), (0, 0, 1)))

        new_pos = T3.dot(T2.dot(R.dot(T1.dot(pos))))
        return (round(new_pos[0]), round(new_pos[1]))

    def rotate_point_around_center(self, xpos: float, ypos: float,
                                   angle: float) -> Tuple[float, float]:
        """Rotates a point around the component center clockwise

        Args:
            xpos (float): x coordinate of the point
            ypos (float): y coordinate of the point
            angle (float): angle of rotation in degrees

        Returns:
            Tuple[float, float]: A tuple containing the rotated coordinates
        """
        # Setup the center to be used the translation matrices
        center_x = self.xpos + self.xspan / 2
        center_y = self.ypos + self.yspan / 2

        pos = np.array(((xpos), (ypos), (1)))

        T1 = np.array(((1, 0, -center_x), (0, 1, -center_y), (0, 0, 1)))

        theta = np.radians(angle)
        c, s = np.cos(theta), np.sin(theta)
        R = np.array(((c, -s, 0), (s, c, 0), (0, 0, 1)))
        T2 = np.array(((1, 0, center_x), (0, 1, center_y), (0, 0, 1)))

        new_pos = T2.dot(R.dot(T1.dot(pos)))
        return (round(new_pos[0]), round(new_pos[1]))

    def get_rotated_component_definition(self, angle: int) -> Component:
        """Returns a new component with the same parameters but rotated by the given angle

        Args:
            angle (int): angle of rotation

        Returns:
            Component: [description]
        """
        new_topLeft = self.rotate_point(0, 0, angle)
        new_topRight = self.rotate_point(self.xspan, 0, angle)
        new_bottomLeft = self.rotate_point(0, self.yspan, angle)
        new_bottomRight = self.rotate_point(self.xspan, self.yspan, angle)

        # Find xmin, ymin, xmax, ymax for all the corner points
        xmin = min(new_topLeft[0], new_topRight[0], new_bottomLeft[0],
                   new_bottomRight[0])
        ymin = min(new_topLeft[1], new_topRight[1], new_bottomLeft[1],
                   new_bottomRight[1])
        xmax = max(new_topLeft[0], new_topRight[0], new_bottomLeft[0],
                   new_bottomRight[0])
        ymax = max(new_topLeft[1], new_topRight[1], new_bottomLeft[1],
                   new_bottomRight[1])

        # Find the new xspan and yspan
        new_xspan = abs(xmax - xmin)
        new_yspan = abs(ymax - ymin)

        # Create a new component with the rotated coordinates
        rotated_component = Component()
        rotated_component.name = self.name
        rotated_component.ID = self.ID
        rotated_component.layers = self.layers
        rotated_component.params = self.params
        rotated_component.xpos = xmin
        rotated_component.ypos = ymin
        rotated_component.entity = self.entity

        # Add the x and y spans
        rotated_component.xspan = int(new_xspan)
        rotated_component.yspan = int(new_yspan)

        # Set the rotation angle to 0 to ensure that future operations don't mistake this to not have a rotation
        rotated_component.rotation = 0

        # Create new ports with new rotated coordinates
        for port in self._ports:
            new_port = Port()
            new_port.label = port.label
            new_location = self.rotate_point(port.x, port.y, angle)
            new_port.x = new_location[0]
            new_port.y = new_location[1]
            rotated_component.add_component_port(new_port)

        return rotated_component

    def rotate_component(self) -> None:
        """Returns a new component with the same parameters but rotated by the given angle

        Args:
            None

        Returns:
            None
        """

        # first rotate ports before everything gets confusion
        for port in self._ports:
            print(port.label, port.x, port.y)
            new_location = self.rotate_point(port.x, port.y, self.rotation)
            port.x = new_location[0]
            port.y = new_location[1]

        new_topLeft = self.rotate_point_around_center(self.xpos + 0,
                                                      self.ypos + 0,
                                                      self.rotation)
        new_topRight = self.rotate_point_around_center(self.xpos + self.xspan,
                                                       self.ypos + 0,
                                                       self.rotation)
        new_bottomLeft = self.rotate_point_around_center(
            self.xpos + 0, self.ypos + self.yspan, self.rotation)
        new_bottomRight = self.rotate_point_around_center(
            self.xpos + self.xspan, self.ypos + self.yspan, self.rotation)

        # Find xmin, ymin, xmax, ymax for all the corner points
        xmin = min(new_topLeft[0], new_topRight[0], new_bottomLeft[0],
                   new_bottomRight[0])
        ymin = min(new_topLeft[1], new_topRight[1], new_bottomLeft[1],
                   new_bottomRight[1])
        xmax = max(new_topLeft[0], new_topRight[0], new_bottomLeft[0],
                   new_bottomRight[0])
        ymax = max(new_topLeft[1], new_topRight[1], new_bottomLeft[1],
                   new_bottomRight[1])

        # Find the new xspan and yspan
        new_xspan = abs(xmax - xmin)
        new_yspan = abs(ymax - ymin)
        self.xspan = int(new_xspan)
        self.yspan = int(new_yspan)

        # Create a new component with the rotated coordinates
        self.xpos = xmin
        self.ypos = ymin