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")
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
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)
class Device: def __init__(self, json=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 = [] # Store Raw JSON Objects for now self.xspan: Optional[int] = None self.yspan: Optional[int] = None self.G = nx.MultiDiGraph() if json: self.parse_from_json(json) self.generate_network() def add_component(self, component: Component): """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) else: raise Exception( "Could not add component since its not an instance of parchmint:Component" ) def add_connection(self, connection: Connection): """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): self.connections.append(connection) else: raise Exception( "Could not add component since its not an instance of parchmint:Connection" ) 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 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 = dict() for layer in netlist.layers: if layer not in self.layers: self.add_layer(layer) layer_mapping[layer] = layer else: 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) -> None: """Returns the json dict Returns: dict: dictionary that can be used in json.dumps() """ self.name = json["name"] # First always add the layers for layer in json["layers"]: self.add_layer(Layer(layer)) # Loop through the components for component in json["components"]: self.add_component(Component(component, self)) for connection in json["connections"]: self.add_connection(Connection(connection, self)) if "params" in json.keys(): self.params = Params(json["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") 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 generate_network(self) -> None: """Generates the underlying graph""" for component in self.components: self.G.add_node(component.ID, component_ref=component) for connection in self.connections: sourceref = connection.source._component for sink in connection.sinks: sinkref = sink._component self.G.add_edge( sourceref, sinkref, source_port=connection.source, sink_port=sink, connection_ref=connection, ) def get_name_from_id(self, id: str) -> Optional[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 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 __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 = dict() 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 @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("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("------")