Source code for qiskit_metal.qlibrary.core.qroute

# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

import numpy as np
from qiskit_metal import draw, Dict
from .base import QComponent
from numpy.linalg import norm
from typing import List, Tuple, Union, AnyStr
from collections.abc import Mapping
from qiskit_metal.toolbox_metal import math_and_overrides as mao
import math
import re


[docs] class QRoutePoint: """A convenience wrapper class to define an point with orientation, with a 2D position and a 2D direction (XY plane). All values stored as np.ndarray of parsed floats. """ def __init__(self, position: np.array, direction: np.array = None): """ Args: position (np.ndarray of 2 points): Center point of the pin. direction (np.ndarray of 2 points): *Normal vector* This is the normal vector to the surface on which the pin mates. Defines which way it points outward. Has unit norm. Defaults to None. """ self.position = position if isinstance(position, list): if len(position[-1]) == 2: self.position = position[-1] self.direction = direction def __str__(self): return "position: " + str(self.position) + " : direction:" + str( self.direction) + "."
[docs] class QRoute(QComponent): """Super-class implementing routing methods that are valid irrespective of the number of pins (>=1). The route is stored in a n array of planar points (x,y coordinates) and one direction, which is that of the last point in the array. Values are stored as np.ndarray of parsed floats or np.array float pair. Inherits `QComponent` class Default Options: * pin_inputs: Dict * start_pin: Dict -- Component and pin string pair. Define which pin to start from * component: '' -- Name of component to start from, which has a pin * pin: '' -- Name of pin used for pin_start * end_pin=Dict -- Component and pin string pair. Define which pin to start from * component: '' -- Name of component to end on, which has a pin * pin: '' -- Name of pin used for pin_end * lead: Dict * start_straight: '0mm' -- Lead-in, defined as the straight segment extension from start_pin. Defaults to 0.1um. * end_straight: '0mm' -- Lead-out, defined as the straight segment extension from end_pin. Defaults to 0.1um. * start_jogged_extension: '' -- Lead-in, jogged extension of lead-in. Described as list of tuples * end_jogged_extension: '' -- Lead-out, jogged extension of lead-out. Described as list of tuples * fillet: '0' * total_length: '7mm' * trace_width: 'cpw_width' -- Defines the width of the line. Defaults to 'cpw_width'. How to specify \*_jogged_extensions for the QRouteLeads: \*_jogged_extensions have to be specified in an OrderedDict with incremental keys. the value of each key specifies the direction of the jog and the extension past the jog. For example: .. code-block:: python :linenos: jogs = OrderedDict() jogs[0] = ["R", '200um'] jogs[1] = ["R", '200um'] jogs[2] = ["L", '200um'] jogs[3] = ["L", '500um'] jogs[4] = ["R", '200um'] jogs_other = .... options = {'lead': { 'start_straight': '0.3mm', 'end_straight': '0.3mm', 'start_jogged_extension': jogs, 'end_jogged_extension': jogs_other }} The jog direction can be specified in several ways. Feel free to pick the one more convenient for your coding style: >> "L", "L#", "R", "R#", #, "#", "A,#", "left", "left#", "right", "right#" where # is any signed or unsigned integer or floating point value. For example the following will all lead to the same turn: >> "L", "L90", "R-90", 90, "90", "A,90", "left", "left90", "right-90" """ component_metadata = Dict(short_name='route', _qgeometry_table_path='True') """Component metadata""" default_options = Dict( pin_inputs=Dict( start_pin=Dict( # QRoute also supports single pin routes component='', # Name of component to start from, which has a pin pin=''), # Name of pin used for pin_start end_pin=Dict( component='', # Name of component to end on, which has a pin pin='') # Name of pin used for pin_end ), fillet='0', lead=Dict(start_straight='0mm', end_straight='0mm', start_jogged_extension='', end_jogged_extension=''), total_length='7mm', trace_width='cpw_width') """Default options""" TOOLTIP = """QRoute""" def __init__(self, design, name: str = None, options: Dict = None, type: str = "CPW", **kwargs): """Initializes all Routes. Calls the QComponent __init__() to create a new Metal component. Before that, it adds the variables that are needed to support routing. Args: design (QDesign): The parent design. name (str): Name of the component. Auto-named if possible. options (dict): User options that will override the defaults. Defaults to None. type (string): Supports Route (single layer trace) and CPW (adds the gap around it). Defaults to "CPW". """ # Class key Attributes: # * head (QRouteLead()): Stores sequential points to start the route. # * tail (QRouteLead()): (optional) Stores sequential points to terminate the route. # * intermediate_pts: (list or numpy Nx2 or dict) Sequence of points between and other # than head and tail. Defaults to None. Type could be either list or numpy Nx2, # or dict/OrderedDict nesting lists or numpy Nx2. # * start_pin_name (string): Head pin name. Defaults to "start". # * end_pin_name (string): Tail pin name. Defaults to "end". self.head = QRouteLead() self.tail = QRouteLead() # keep track of all points so far in the route from both ends self.intermediate_pts = np.empty((0, 2), float) # will be numpy Nx2 # supported pin names (constants) self.start_pin_name = "start" self.end_pin_name = "end" self.type = type.upper().strip() # # add default_options that are QRoute type specific: options = self._add_route_specific_options(options) # regular QComponent boot, including the run of make() super().__init__(design, name, options, **kwargs) def _add_route_specific_options(self, options): """Enriches the default_options to support different types of route styles. Args: options (dict): User options that will override the defaults Return: A modified options dictionary Raises: Exception: Unsupported route type """ if self.type == "ROUTE": # all the defaults are fine as-is None elif self.type == "CPW": # add the variable to define the space between the route and the ground plane cpw_options = Dict(trace_gap='cpw_gap') if options: if "trace_gap" not in options: # user did not pass the trace_gap, so add it options.update(cpw_options) else: # user did not pass custom options, so create it to add trace_gap options["options"] = cpw_options else: raise Exception("Unsupported Route type: " + self.type + " The only supported types are CPW and route") return options def _get_connected_pin(self, pin_data: Dict): """Recovers a pin from the dictionary. Args: pin_data: dict {component: string, pin: string} Return: The actual pin object. """ return self.design.components[pin_data.component].pins[pin_data.pin]
[docs] def set_pin(self, name: str) -> QRoutePoint: """Defines the CPW pins and returns the pin coordinates and normal direction vector. Args: name: String (supported pin names are: start, end) Return: QRoutePoint: Last point (for now the single point) in the QRouteLead Raises: Exception: Ping name is not supported """ # First define which pin/lead you intend to initialize if name == self.start_pin_name: options_pin = self.options.pin_inputs.start_pin lead = self.head elif name == self.end_pin_name: options_pin = self.options.pin_inputs.end_pin lead = self.tail else: raise Exception("Pin name \"" + name + "\" is not supported for this CPW." + " The only supported pins are: start, end.") # grab the reference component pin reference_pin = self._get_connected_pin(options_pin) # create the cpw pin and document the connections to the reference_pin in the netlist self.add_pin(name, reference_pin.points[::-1], self.p.trace_width) self.design.connect_pins( self.design.components[options_pin.component].id, options_pin.pin, self.id, name) # anchor the correct lead to the pin and return its position and direction return lead.seed_from_pin(reference_pin)
[docs] def set_lead(self, name: str) -> QRoutePoint: """Defines the lead_extension by adding a point to the self.head/tail. Args: name: String (supported pin names are: start, end) Return: QRoutePoint: Last point in the QRouteLead (self.head/tail) Raises: Exception: Ping name is not supported """ p = self.parse_options() # First define which lead you intend to modify if name == self.start_pin_name: options_lead = p.lead.start_straight lead = self.head jogged_lead = self.p.lead.start_jogged_extension elif name == self.end_pin_name: options_lead = p.lead.end_straight lead = self.tail jogged_lead = self.p.lead.end_jogged_extension else: raise Exception("Pin name \"" + name + "\" is not supported for this CPW." + " The only supported pins are: start, end.") # then change the lead by adding a point in the same direction of the seed pin # minimum lead, to be able to jog correctly lead_length = max(options_lead, self.p.trace_width / 2.0) lead.go_straight(lead_length) # then add all the jogged lead information if jogged_lead: self.set_lead_extension(name) # consider merging with set_lead # return the last QRoutePoint of the lead return lead.get_tip()
[docs] def set_lead_extension(self, name: str) -> QRoutePoint: """Defines the jogged lead_extension by adding a series of turns to the self.head/tail. Args: name: String (supported pin names are: start, end) Return: QRoutePoint: Last point in the QRouteLead (self.head/tail) Raises: Exception: Ping name is not supported Exception: Dictionary error """ p = self.parse_options() # First define which lead you intend to modify if name == self.start_pin_name: options_lead = p.lead.start_jogged_extension lead = self.head elif name == self.end_pin_name: options_lead = p.lead.end_jogged_extension lead = self.tail else: raise Exception("Pin name \"" + name + "\" is not supported for this CPW." + " The only supported pins are: start, end.") # then change the lead by adding points for turn, length in options_lead.values(): if isinstance(turn, (float, int)): # turn is a number indicating the angle lead.go_angle(length, turn) elif re.search(r'^[-+]?(\d+\.\d+|\d+)$', turn): # turn is a string of a number indicating the angle lead.go_angle(length, float(turn)) elif turn in ("left", "L"): # implicit turn -90 degrees lead.go_left(length) elif turn in ("right", "R"): # implicit turn 90 degrees lead.go_right(length) elif turn in ("straight", "D", "S"): # implicit 0 degrees movement lead.go_straight(length) elif re.search(r'^(left|L)[-+]?(\d+\.\d+|\d+)$', turn): # left turn by the specified int/float degrees. can be signed angle = re.sub(r'^(left|L)', "", turn) lead.go_angle(length, float(angle)) elif re.search(r'^(right|R)[-+]?(\d+\.\d+|\d+)$', turn): # right turn by the specified int/float degrees. can be signed angle = re.sub(r'^(right|R)', "", turn) lead.go_angle(length, -1 * float(angle)) elif ('A' or 'angle') in turn: # turn by the specified int/float degrees. Positive numbers turn left. turn, angle = turn.split(',') lead.go_angle(length, float(angle)) else: raise Exception( f"\nThe input string {turn} is not supported. Please specify the jog turn " "using one of the supported formats:\n\"L\", \"L#\", \"R\", \"R#\", #, " "\"#\", \"A,#\", \"left\", \"left#\", \"right\", \"right#\"" "\nwhere # is any signed or unsigned integer or floating point value.\n" "For example the following will all lead to the same turn:\n" "\"L\", \"L90\", \"R-90\", 90, " "\"90\", \"A,90\", \"left\", \"left90\", \"right-90\"") # return the last QRoutePoint of the lead return lead.get_tip()
def _get_lead2pts_array(self, arr) -> Tuple: """Return the last "diff pts" of the array. If the array is one dimensional or has only identical points, return -1 for tip_pt_minus_1. Return: Tuple: Of two np.ndarray. the arrays could be -1 instead, if point not found """ pt = pt_minus_1 = None if len(arr) == 1: pt = arr[0] elif len(arr) > 1: if not isinstance(arr, np.ndarray) and len(arr) == 2 and len( arr[0]) == 1: # array 2,1 pt = arr else: # array N,2 pt = arr[-1] prev_id = -2 pt_minus_1 = arr[prev_id] while (pt_minus_1 == pt).all() and prev_id > -len(arr): prev_id -= 1 pt_minus_1 = arr[prev_id] if (pt_minus_1 == pt).all(): pt_minus_1 = None return pt, pt_minus_1
[docs] def get_tip(self) -> QRoutePoint: """Access the last element in the QRouteLead. Return: QRoutePoint: Last point in the QRouteLead The values are numpy arrays with two float points each. """ if self.intermediate_pts is None: # no points in between, so just grab the last point from the lead-in return self.head.get_tip() tip_pt = tip_pt_minus_1 = None if isinstance(self.intermediate_pts, list) or isinstance( self.intermediate_pts, np.ndarray): tip_pt, tip_pt_minus_1 = self._get_lead2pts_array( self.intermediate_pts) elif isinstance(self.intermediate_pts, Mapping): # then it is either a dict or a OrderedDict # this method relies on the keys to be numerical integer. Will use the last points # assumes that the "value" associated with each key is some "not empty" list/array sorted_keys = sorted(self.intermediate_pts.keys(), reverse=True) for key in sorted_keys: pt0, pt_minus1 = self._get_lead2pts_array( self.intermediate_pts[key]) if pt0 is None: continue if tip_pt_minus_1 is None: tip_pt_minus_1 = pt0 if tip_pt is None: tip_pt, tip_pt_minus_1 = tip_pt_minus_1, tip_pt tip_pt_minus_1 = pt_minus1 else: print("unsupported type for self.intermediate_pts", type(self.intermediate_pts)) return if tip_pt is None: # no point in the intermediate array return self.head.get_tip() if tip_pt_minus_1 is None: # no "previous" point in the intermediate array tip_pt_minus_1 = self.head.get_tip().position return QRoutePoint(tip_pt, tip_pt - tip_pt_minus_1)
[docs] def del_colinear_points(self, inarray): """Delete colinear points from the given array. Args: inarray (list): List of points Returns: list: List of points without colinear points """ if len(inarray) <= 1: return else: outarray = list() #outarray = np.empty(shape=[0, 2]) pts = [None, None, inarray[0]] for idxnext in range(1, len(inarray)): pts = pts[1:] + [inarray[idxnext]] # delete identical points if np.allclose(*pts[1:]): pts = [None] + pts[0:2] continue # compare points once you have 3 unique points in pts if pts[0] is not None: # if all(mao.round(i[1]) == mao.round(pts[0][1]) for i in pts) \ # or all(mao.round(i[0]) == mao.round(pts[0][0]) for i in pts): if mao.aligned_pts(pts): pts = [None] + [pts[0]] + [pts[2]] # save a point once you successfully establish the three are not aligned, # and before it gets dropped in the next loop cycle if pts[0] is not None: outarray.append(pts[0]) # save the remainder non-aligned points if pts[1] is not None: outarray.extend(pts[1:]) else: outarray.append(pts[2]) return np.array(outarray)
[docs] def get_points(self) -> np.ndarray: """Assembles the list of points for the route by concatenating: head_pts + intermediate_pts, tail_pts. Returns: np.ndarray: ((H+N+T)x2) all points (x,y) of the CPW """ # cover case where there is no intermediate points (straight connection between lead ends) if self.intermediate_pts is None: beginning = self.head.pts else: beginning = np.concatenate([self.head.pts, self.intermediate_pts], axis=0) # cover case where there is no tail defined (floating end) if self.tail is None: polished = beginning else: polished = np.concatenate([beginning, self.tail.pts[::-1]], axis=0) polished = self.del_colinear_points(polished) return polished
[docs] def get_unit_vectors(self, start: QRoutePoint, end: QRoutePoint, snap: bool = False) -> Tuple: """Return the unit and target vector in which the CPW should process as its coordinate sys. Args: start (QRoutePoint): Reference start point (direction from here) end (QRoutePoint): Reference end point (direction to here) snap (bool): True to snap to grid. Defaults to False. Returns: array: straight and 90 deg CCW rotated vecs 2D (array([1., 0.]), array([0., 1.])) """ # handle chase when start and end are same? v = end.position - start.position direction = v / norm(v) if snap: direction = draw.Vector.snap_unit_vector(direction, flip=False) normal = draw.Vector.rotate(direction, np.pi / 2) return direction, normal
@property def length(self) -> float: """Sum of all segments length, including the head. Return: length (float): Full point_array length """ # get the final points (get_point also eliminate co-linear and short edges) points = self.get_points() # get the length without the corner rounding radius adjustment length_estimate = sum( norm(points[i + 1] - points[i]) for i in range(len(points) - 1)) # compensate for corner rounding length_estimate -= self.length_excess_corner_rounding(points) return length_estimate
[docs] def length_excess_corner_rounding(self, points) -> float: """Computes how much length to deduce for compensating the fillet settings. Args: points (list or array): List of vertices that will be receiving the corner rounding radius Return: length_excess (float): Corner rounding radius excess multiplied by the number of points """ # deduct the corner rounding (WARNING: assumes fixed fillet for all corners) length_arch = 0.5 * self.p.fillet * math.pi length_corner = 2 * self.p.fillet length_excess = length_corner - length_arch # the start and and point are the pins, so no corner rounding return (len(points) - 2) * length_excess
[docs] def assign_direction_to_anchor(self, ref_pt: QRoutePoint, anchor_pt: QRoutePoint): """Method to assign a direction to a point. Currently assigned as the max(x,y projection) of the direct path between the reference point and the anchor. Method directly modifies the anchor_pt.direction, thus there is no return value. Args: ref_pt (QRoutePoint): Reference point anchor_pt (QRoutePoint): Anchor point. if it already has a direction, the method will not overwrite it """ if anchor_pt.direction is not None: # anchor_pt already has a direction (not an anchor?), so do nothing return # Current rule: stop_direction aligned with longer edge of the rectangle connecting ref_pt and anchor_pt ref = ref_pt.position anchor = anchor_pt.position # Absolute value of displacement between ref and anchor in x direction offsetx = abs(anchor[0] - ref[0]) # Absolute value of displacement between ref and anchor in y direction offsety = abs(anchor[1] - ref[1]) if offsetx >= offsety: # "Wide" rectangle -> anchor_arrow points along x assigned_direction = np.array([ref[0] - anchor[0], 0]) else: # "Tall" rectangle -> anchor_arrow points along y assigned_direction = np.array([0, ref[1] - anchor[1]]) anchor_pt.direction = assigned_direction / norm(assigned_direction)
[docs] def make_elements(self, pts: np.ndarray): """Turns the CPW points into design elements, and add them to the design object. Args: pts (np.ndarray): Array of points """ # prepare the routing track line = draw.LineString(pts) # compute actual final length p = self.p self.options._actual_length = str( line.length - self.length_excess_corner_rounding(line.coords) ) + ' ' + self.design.get_units() # expand the routing track to form the substrate core of the cpw self.add_qgeometry('path', {'trace': line}, width=p.trace_width, fillet=p.fillet, layer=p.layer) if self.type == "CPW": # expand the routing track to form the two gaps in the substrate # final gap will be form by this minus the trace above self.add_qgeometry('path', {'cut': line}, width=p.trace_width + 2 * p.trace_gap, fillet=p.fillet, layer=p.layer, subtract=True)
[docs] class QRouteLead: """A simple class to define a an array of points with some properties, defines 2D positions and some of the 2D directions (XY plane). All values stored as np.ndarray of parsed floats. """ def __init__(self, *args, **kwargs): """QRouteLead is a simple sequence of points. Used to accurately control one of the QRoute termination points Before that, it adds the variables that are needed to support routing. Attributes: pts (numpy Nx2): Sequence of points. Defaults to None. direction (numpy 2x1): Normal from the last point of the array. Defaults to None. """ # keep track of all points so far in the route from both ends self.pts = None # will be numpy Nx2 # keep track of the direction of the tip of the lead (last point) self.direction = None # will be numpy 2x1
[docs] def seed_from_pin(self, pin: Dict) -> QRoutePoint: """Initialize the QRouteLead by giving it a starting point and a direction. Args: pin: object describing the "reference_pin" (not cpw_pin) this is attached to. this is currently (8/4/2020) a dictionary Return: QRoutePoint: Last point (for now the single point) in the QRouteLead The values are numpy arrays with two float points each. """ position = pin['middle'] direction = pin['normal'] self.direction = direction self.pts = np.array([position]) return QRoutePoint(position, direction)
[docs] def go_straight(self, length: float): """Add a point ot 'length' distance in the same direction. Args: length (float) : How much to move by """ self.pts = np.append(self.pts, [self.pts[-1] + self.direction * length], axis=0)
[docs] def go_left(self, length: float): """Straight line 90deg counter-clock-wise direction w.r.t. lead tip direction. Args: length (float): How much to move by """ self.direction = draw.Vector.rotate(self.direction, np.pi / 2) self.pts = np.append(self.pts, [self.pts[-1] + self.direction * length], axis=0)
[docs] def go_right(self, length: float): """Straight line 90deg clock-wise direction w.r.t. lead tip direction. Args: length (float): How much to move by """ self.direction = draw.Vector.rotate(self.direction, -1 * np.pi / 2) self.pts = np.append(self.pts, [self.pts[-1] + self.direction * length], axis=0)
[docs] def go_right45(self, length: float): """Straight line at 45 angle clockwise w.r.t lead tip direction. Args: length(float): How much to move by """ self.direction = draw.Vector.rotate(self.direction, -1 * np.pi / 4) self.pts = np.append(self.pts, [self.pts[-1] + self.direction * length], axis=0)
[docs] def go_left45(self, length: float): """Straight line at 45 angle counter-clockwise w.r.t lead tip direction. Args: length(float): How much to move by """ self.direction = draw.Vector.rotate(self.direction, np.pi / 4) self.pts = np.append(self.pts, [self.pts[-1] + self.direction * length], axis=0)
[docs] def go_angle(self, length: float, angle: float): """ Straight line at any angle w.r.t lead tip direction. Args: length(float): How much to move by angle(float): rotation angle w.r.t lead tip direction """ self.direction = draw.Vector.rotate(self.direction, np.pi / 180 * angle) self.pts = np.append(self.pts, [self.pts[-1] + self.direction * length], axis=0)
@property def length(self): """Sum of all segments length, including the head. Return: length (float): Full point_array length """ return sum( norm(self.pts[i + 1] - self.pts[i]) for i in range(len(self.pts) - 1))
[docs] def get_tip(self) -> QRoutePoint: """Access the last element in the QRouteLead. Return: QRoutePoint: Last point in the QRouteLead The values are numpy arrays with two float points each. """ if self.pts.ndim == 1: return QRoutePoint(self.pts, self.direction) return QRoutePoint(self.pts[-1], self.direction)