# -*- coding: utf-8 -*- from __future__ import division, print_function, absolute_import import numpy as np import itertools from numpy import sqrt, pi, cos, sin, log, exp, sinh # from scipy.interpolate import interp1d import gdspy from gdspy import clipper from phidl.device_layout import Device, Port, Polygon, CellArray from phidl.device_layout import _parse_layer, DeviceReference import copy as python_copy from collections import OrderedDict import pickle import json import warnings from functools import update_wrapper from phidl.constants import _glyph,_width,_indent ##### Categories: # Polygons / shapes # Boolean functions # Lithography test structures # Utility functions (copying, importing, extracting) # Pads # Taper # Text # Wafer / Die # Waveguide # Packer tool / Fill tool # Photonics # Optimal (current-crowding) curves # Superconducting devices #============================================================================== # # Polygons / Shapes # #============================================================================== def rectangle(size = (4,2), layer = 0): """Generate rectangle geometry. Parameters ---------- size : tuple Width and height of rectangle. layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with a single rectangle in it """ D = Device(name = 'rectangle') points = [[size[0], size[1]], [size[0], 0], [0, 0], [0, size[1]]] D.add_polygon(points, layer = layer) return D def bbox(bbox = [(-1,-1),(3,4)], layer = 0): """ Creates a bounding box rectangle from coordinates, to allow creation of a rectangle bounding box directly form another shape. Parameters ---------- bbox : list of tuples Coordinates of the box [(x1,y1),(x2,y2)]. layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with a single rectangle in it Examples -------- >>> D = pg.bbox(anothershape.bbox) """ D = Device(name = 'bbox') (a,b),(c,d) = bbox points = ((a,b), (c,b), (c,d), (a,d)) D.add_polygon(points, layer = layer) return D def cross(length = 10, width = 3, layer = 0): """Generates a right-angle cross (+ shape, symmetric) from two rectangles of specified length and width. Parameters ---------- length : float Length of the cross from one end to the other. width : float Width of the arms of the cross. layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with a cross in it """ D = Device(name = 'cross') R = rectangle(size = (width, length), layer = layer) r1 = D.add_ref(R).rotate(90) r2 = D.add_ref(R) r1.center = (0,0) r2.center = (0,0) return D def ellipse(radii = (10,5), angle_resolution = 2.5, layer = 0): """Generate an ellipse geometry. Parameters ---------- radii : tuple Semimajor (x) and semiminor (y) axis lengths of the ellipse. angle_resolution : float Resolution of the curve of the ring (# of degrees per point). layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with an ellipse polygon in it """ D = Device(name = 'ellipse') a = radii[0] b = radii[1] t = np.linspace(0, 360, int(np.ceil(360/angle_resolution) + 1))*pi/180 r = a*b/(sqrt((b*cos(t))**2 + (a*sin(t))**2)) xpts = r*cos(t) ypts = r*sin(t) D.add_polygon(points = (xpts,ypts), layer = layer) return D def circle(radius = 10, angle_resolution = 2.5, layer = 0): """Generate a circle geometry. Parameters ---------- radius : float Radius of the circle. angle_resolution : float Resolution of the curve of the ring (# of degrees per point). layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with an circle polygon in it """ D = Device(name = 'circle') t = np.linspace(0, 360, int(np.ceil(360/angle_resolution) + 1))*pi/180 xpts = (radius*cos(t)).tolist() ypts = (radius*sin(t)).tolist() D.add_polygon(points = (xpts,ypts), layer = layer) return D def ring(radius = 10, width = 0.5, angle_resolution = 2.5, layer = 0): """Generate a ring geometry. Parameters ---------- radius : float Middle radius of the ring. width : float Width of the ring. angle_resolution : float Resolution of the curve of the ring (# of degrees per point). layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with an ring polygon in it Notes ----- The ring is formed by taking the radius out to the specified value, and then constructing the thickness by dividing the width in half and adding that value to either side of the radius. The angle_resolution alters the precision of the curve of the ring. Larger values yield lower resolution. """ D = Device(name = 'ring') inner_radius = radius - width/2 outer_radius = radius + width/2 n = int(np.round(360/angle_resolution)) t = np.linspace(0, 360, n+1)*pi/180 inner_points_x = (inner_radius*cos(t)).tolist() inner_points_y = (inner_radius*sin(t)).tolist() outer_points_x = (outer_radius*cos(t)).tolist() outer_points_y = (outer_radius*sin(t)).tolist() xpts = inner_points_x + outer_points_x[::-1] ypts = inner_points_y + outer_points_y[::-1] D.add_polygon(points = (xpts,ypts), layer = layer) return D def arc(radius = 10, width = 0.5, theta = 45, start_angle = 0, angle_resolution = 2.5, layer = 0): """ Creates an arc of arclength ``theta`` starting at angle ``start_angle`` Parameters ---------- radius : float Radius of the arc centerline. width : float Width of the arc. theta : float Total angle coverage of the arc. start_angle : float Starting angle. angle_resolution : float Resolution of the curve of the arc. layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with an arc polygon in it, and two ports (`1` and `2`) on either end Notes ----- Theta = 0 is located along the positive x-axis relative to the centre of the arc. Ports are added to each end of the arc to facilitate connecting those ends to other geometries. """ inner_radius = radius-width/2 outer_radius = radius+width/2 angle1 = (start_angle)*pi/180 angle2 = (start_angle + theta)*pi/180 t = np.linspace(angle1, angle2, int(np.ceil(abs(theta)/angle_resolution))) inner_points_x = (inner_radius*cos(t)).tolist() inner_points_y = (inner_radius*sin(t)).tolist() outer_points_x = (outer_radius*cos(t)).tolist() outer_points_y = (outer_radius*sin(t)).tolist() xpts = inner_points_x + outer_points_x[::-1] ypts = inner_points_y + outer_points_y[::-1] D = Device('arc') D.add_polygon(points = (xpts,ypts), layer = layer) D.add_port(name = 1, midpoint = (radius*cos(angle1), radius*sin(angle1)), width = width, orientation = start_angle - 90 + 180*(theta<0)) D.add_port(name = 2, midpoint = (radius*cos(angle2), radius*sin(angle2)), width = width, orientation = start_angle + theta + 90 - 180*(theta<0)) D.info['length'] = (abs(theta)*pi/180)*radius return D def turn(port, radius = 10, angle = 270, angle_resolution = 2.5, layer = 0): """ Starting form a port, create a arc which connects to the port """ D = arc(radius = radius, width = port.width, theta = angle, start_angle = 0, angle_resolution = angle_resolution, layer = layer) D.rotate(angle = 180 + port.orientation - D.ports[1].orientation, center = D.ports[1].midpoint) D.move(origin = D.ports[1], destination = port) return D def straight(size = (4,2), layer = 0): """Generates a rectangular wire geometry with ports on the length edges. Parameters ---------- size : tuple The length and width of the rectangle. layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with an rectangle polygon in it, and two ports (`1` and `2`) on either end Notes ----- Ports are included on both sides of the length edge (i.e. size[0]) of the geometry. """ D = Device(name = 'wire') points = [[size[0], size[1]], [size[0], 0], [0, 0], [0, size[1]]] D.add_polygon(points, layer = layer) D.add_port(name = 1, midpoint = (size[0]/2, size[1]), width = size[0], orientation = 90) D.add_port(name = 2, midpoint = (size[0]/2, 0), width = size[0], orientation = -90) return D def L(width = 1, size = (10,20) , layer = 0): """Generates an "L" geometry with ports on both ends. Parameters ---------- width : float Width of the L. size : tuple Lengths of the base and height of the L, respectively. layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with an L-shaped polygon in it, and two ports (`1` and `2`) on either end of the L """ D = Device(name = 'L') w = width/2 s1, s2 = size points = [(-w,-w), (s1,-w), (s1,w), (w,w), (w,s2), (-w,s2), (-w,-w)] D.add_polygon(points, layer = layer) D.add_port(name = 1, midpoint = (0,s2), width = width, orientation = 90) D.add_port(name = 2, midpoint = (s1, 0), width = width, orientation = 0) return D def C(width = 1, size = (10,20) , layer = 0): """Generates a "C" geometry with ports on both ends. Parameters ---------- width : float Width of the C. size : tuple Lengths of the base + top edges and the height of the C, respectively. layer : int, array-like[2], or set Specific layer(s) to put polygon geometry on. Returns ------- A Device with an [-bracket-shaped polygon in it, and two ports (`1` and `2`) on either end of the [ shape """ D = Device(name = 'C') w = width/2 s1, s2 = size points = [(-w,-w), (s1,-w), (s1,w), (w,w), (w,s2-w), (s1,s2-w), (s1,s2+w), (-w, s2+w), (-w,-w)] D.add_polygon(points, layer = layer) D.add_port(name = 1, midpoint = (s1,s2), width = width, orientation = 0) D.add_port(name = 2, midpoint = (s1, 0), width = width, orientation = 0) return D #============================================================================== # # Boolean functions # #============================================================================== def offset(elements, distance = 0.1, join_first = True, precision = 1e-4, num_divisions = [1,1], join='miter', tolerance=2, max_points = 4000, layer = 0): if type(elements) is not list: elements = [elements] polygons_to_offset = [] for e in elements: if isinstance(e, (Device, DeviceReference)): polygons_to_offset += e.get_polygons(by_spec = False) elif isinstance(e, (Polygon, gdspy.Polygon)): polygons_to_offset.append(e) if len(polygons_to_offset) == 0: return Device('offset') polygons_to_offset = _merge_floating_point_errors(polygons_to_offset, tol = precision/1000) gds_layer, gds_datatype = _parse_layer(layer) if all(np.array(num_divisions) == np.array([1,1])): p = gdspy.offset(polygons_to_offset, distance = distance, join=join, tolerance=tolerance, precision=precision, join_first=join_first, max_points=max_points, layer=gds_layer, datatype = gds_datatype) else: p = _offset_polygons_parallel( polygons_to_offset, distance = distance, num_divisions = num_divisions, join_first = join_first, precision = precision, join = join, tolerance = tolerance, ) D = Device('offset') polygons = D.add_polygon(p, layer=layer) [polygon.fracture(max_points = max_points, precision = precision) for polygon in polygons] return D def boolean(A, B, operation, precision = 1e-4, num_divisions = [1,1], max_points=4000, layer = 0): """ Performs boolean operations between 2 Device/DeviceReference objects, or lists of Devices/DeviceReferences. ``operation`` should be one of {'not', 'and', 'or', 'xor', 'A-B', 'B-A', 'A+B'}. Note that 'A+B' is equivalent to 'or', 'A-B' is equivalent to 'not', and 'B-A' is equivalent to 'not' with the operands switched """ D = Device('boolean') A_polys = [] B_polys = [] if type(A) is not list: A = [A] if type(B) is not list: B = [B] for e in A: if isinstance(e, Device): A_polys += e.get_polygons() elif isinstance(e, DeviceReference): A_polys += e.get_polygons() for e in B: if isinstance(e, Device): B_polys += e.get_polygons() elif isinstance(e, DeviceReference): B_polys += e.get_polygons() gds_layer, gds_datatype = _parse_layer(layer) operation = operation.lower().replace(' ','') if operation == 'a-b': operation = 'not' elif operation == 'b-a': operation = 'not' A_polys, B_polys = B_polys, A_polys elif operation == 'a+b': operation = 'or' elif operation not in ['not', 'and', 'or', 'xor', 'a-b', 'b-a', 'a+b']: raise ValueError("[PHIDL] phidl.geometry.boolean() `operation` parameter not recognized, must be one of the following: 'not', 'and', 'or', 'xor', 'A-B', 'B-A', 'A+B'") # Check for trivial solutions if (len(A_polys) == 0) or (len(B_polys) == 0): if (operation == 'not'): if len(A_polys) == 0: p = None elif len(B_polys) == 0: p = A_polys elif (operation == 'and'): p = None elif (operation == 'or') or (operation == 'xor'): if (len(A_polys) == 0) and (len(B_polys) == 0): p = None elif len(A_polys) == 0: p = B_polys elif len(B_polys) == 0: p = A_polys else: # If no trivial solutions, run boolean operation either in parallel or straight if all(np.array(num_divisions) == np.array([1,1])): p = gdspy.boolean(operand1 = A_polys, operand2 = B_polys, operation = operation, precision=precision, max_points=max_points, layer=gds_layer, datatype=gds_datatype) else: p = _boolean_polygons_parallel(polygons_A = A_polys, polygons_B = B_polys, num_divisions = num_divisions, operation = operation, precision = precision) if p is not None: polygons = D.add_polygon(p, layer = layer) [polygon.fracture(max_points = max_points, precision = precision) for polygon in polygons] return D def outline(elements, distance = 1, precision = 1e-4, num_divisions = [1,1], join = 'miter', tolerance = 2, join_first = True, max_points = 4000, layer = 0): """ Creates an outline around all the polygons passed in the `elements` argument. `elements` may be a Device, Polygon, or list of Devices """ D = Device('outline') if type(elements) is not list: elements = [elements] for e in elements: if isinstance(e, Device): D.add_ref(e) else: D.add(e) gds_layer, gds_datatype = _parse_layer(layer) D_bloated = offset(D, distance = distance, join_first = join_first, num_divisions = num_divisions, precision = precision, max_points = max_points, join = join, tolerance = tolerance, layer = layer) Outline = boolean(A = D_bloated, B = D, operation = 'A-B', num_divisions = num_divisions, max_points = max_points, precision = precision, layer = layer) return Outline def inset(elements, distance = 0.1, join_first = True, precision = 1e-4, layer = 0): raise ValueError('[PHIDL] pg.inset() is deprecated, please use pg.offset()') def invert(elements, border = 10, precision = 1e-4, num_divisions = [1,1], max_points = 4000, layer = 0): """ Creates an inverted version of the input shapes with an additional border around the edges """ Temp = Device() if type(elements) is not list: elements = [elements] for e in elements: if isinstance(e, Device): Temp.add_ref(e) else: Temp.add(e) gds_layer, gds_datatype = _parse_layer(layer) # Build the rectangle around the device D R = rectangle(size = (Temp.xsize + 2*border, Temp.ysize + 2*border)) R.center = Temp.center D = boolean(A = R, B = Temp, operation = 'A-B', precision = precision, num_divisions = num_divisions, max_points = max_points, layer = layer) return D def xor_diff(A,B, precision = 1e-4): """ Given two Devices A and B, performs the layer-by-layer XOR difference between A and B, and returns polygons representing the differences between A and B. """ D = Device() A_polys = A.get_polygons(by_spec = True) B_polys = B.get_polygons(by_spec = True) A_layers = A_polys.keys() B_layers = B_polys.keys() all_layers = set() all_layers.update(A_layers ) all_layers.update(B_layers) for layer in all_layers: if (layer in A_layers) and (layer in B_layers): p = gdspy.boolean(operand1 = A_polys[layer], operand2 = B_polys[layer], operation = 'xor', precision=precision, max_points=4000, layer=layer[0], datatype=layer[1]) elif (layer in A_layers): p = A_polys[layer] elif (layer in B_layers): p = B_polys[layer] if p is not None: D.add_polygon(p, layer = layer) return D def union(D, by_layer = False, precision = 1e-4, join_first = True, max_points = 4000, layer = 0): U = Device() if by_layer == True: all_polygons = D.get_polygons(by_spec = True) for layer, polygons in all_polygons.items(): unioned_polygons = _union_polygons(polygons, precision = precision, max_points=max_points) U.add_polygon(unioned_polygons, layer = layer) else: all_polygons = D.get_polygons(by_spec = False) unioned_polygons = _union_polygons(all_polygons, precision = precision, max_points=max_points) U.add_polygon(unioned_polygons, layer = layer) return U def _union_polygons(polygons, precision = 1e-4, max_points = 4000): polygons = _merge_floating_point_errors(polygons, tol = precision/1000) unioned = gdspy.boolean(polygons, [], operation = 'or', precision=precision, max_points=max_points) return unioned def _merge_floating_point_errors(polygons, tol = 1e-10): stacked_polygons = np.vstack(polygons) x = stacked_polygons[:,0] y = stacked_polygons[:,1] polygon_indices = np.cumsum([len(p) for p in polygons]) xfixed = _merge_nearby_floating_points(x, tol = tol) yfixed = _merge_nearby_floating_points(y, tol = tol) stacked_polygons_fixed = np.vstack([xfixed, yfixed]).T polygons_fixed = np.vsplit(stacked_polygons_fixed, polygon_indices[:-1]) return polygons_fixed def _merge_nearby_floating_points(x, tol = 1e-10): """ Takes an array `x` and merges any values within the tolerance `tol` So if given >>> x = [-2, -1, 0, 1.0001, 1.0002, 1.0003, 4, 5, 5.003, 6, 7, 8] >>> _merge_nearby_floating_points(x, tol = 1e-3) will then return: >>> [-2, -1, 0, 1.0001, 1.0001, 1.0001, 4, 5, 5.003, 6, 7, 8] """ xargsort = np.argsort(x) xargunsort = np.argsort(xargsort) xsort = x[xargsort] xsortthreshold = (np.diff(xsort) < tol) xsortthresholdind = np.argwhere(xsortthreshold) # Merge nearby floating point values for xi in xsortthresholdind: xsort[xi+1] = xsort[xi] return xsort[xargunsort] def _crop_region(polygons, left, bottom, right, top, precision): """ Given a rectangular boundary defined by left/bottom/right/top, this takes a list of polygons and cuts them at the boundary, discarding parts of the polygons outside the rectangle. """ cropped_polygons = [] for p in polygons: clipped_polys = clipper._chop(p, [top, bottom], 1, 1 / precision) # polygon, [cuts], axis, scale for cp in clipped_polys[1]: result = clipper._chop(cp, [left, right], 0, 1 / precision) cropped_polygons += list(result[1]) return cropped_polygons def _crop_edge_polygons(all_polygons, bboxes, left, bottom, right, top, precision): """ Parses out which polygons are along the edge of the rectangle and need to be cropped and which are deep inside the rectangle region and can be left alone, then crops only those polygons along the edge """ polygons_in_rect_i = _find_bboxes_in_rect(bboxes, left, bottom, right, top) polygons_edge_i = _find_bboxes_on_rect_edge(bboxes, left, bottom, right, top) polygons_in_rect_no_edge_i = polygons_in_rect_i & (~polygons_edge_i) # Crop polygons along the edge and recombine them with polygons inside the rectangle polygons_edge = all_polygons[polygons_edge_i] polygons_in_rect_no_edge = all_polygons[polygons_in_rect_no_edge_i].tolist() polygons_edge_cropped = _crop_region(polygons_edge, left, bottom, right, top, precision = precision) polygons_to_process = polygons_in_rect_no_edge + polygons_edge_cropped return polygons_to_process def _find_bboxes_in_rect(bboxes, left, bottom, right, top): """ Given a list of polygon bounding boxes and a rectangle defined by left/bottom/right/top, this function returns those polygons which overlap the rectangle. """ result = (bboxes[:,0] <= right) & (bboxes[:,2] >= left) & \ (bboxes[:,1] <= top) & (bboxes[:,3] >= bottom) return result # _find_bboxes_on_rect_edge def _find_bboxes_on_rect_edge(bboxes, left, bottom, right, top): """ Given a list of polygon bounding boxes and a rectangular boundary defined by left/bottom/right/top, this function returns those polygons which intersect the rectangular boundary. """ bboxes_left = _find_bboxes_in_rect(bboxes, left, bottom, left, top) bboxes_right = _find_bboxes_in_rect(bboxes, right, bottom, right, top) bboxes_top = _find_bboxes_in_rect(bboxes, left, top, right, top) bboxes_bottom = _find_bboxes_in_rect(bboxes, left, bottom, right, bottom) result = bboxes_left | bboxes_right | bboxes_top | bboxes_bottom return result def _offset_region(all_polygons, bboxes, left, bottom, right, top, distance = 5, join_first = True, precision = 1e-4, join = 'miter', tolerance = 2, ): """ Taking a region of e.g. size (x,y) which needs to be offset by distance d, this function crops out a region (x+2*d, y+2*d) large, offsets that region, then crops it back to size (x,y) to create a valid result """ # Mark out a region slightly larger than the final desired region # FIXME: Necessary? d = distance*1.01 polygons_to_offset = _crop_edge_polygons(all_polygons, bboxes, left-d, bottom-d, right+d, top+d, precision = precision) # Offset the resulting cropped polygons and recrop to final desired size polygons_offset = clipper.offset(polygons_to_offset, distance, join, tolerance, 1/precision, int(join_first)) polygons_offset_cropped = _crop_region(polygons_offset, left, bottom, right, top, precision = precision) return polygons_offset_cropped def _polygons_to_bboxes(polygons): # Build bounding boxes bboxes = np.empty([len(polygons),4]) for n,p in enumerate(polygons): try: left,bottom = np.min(p,axis = 0) except: import pdb pdb.set_trace() right,top = np.max(p,axis = 0) bboxes[n] = [left, bottom, right, top] return bboxes def _offset_polygons_parallel( polygons, distance = 5, num_divisions = [10,10], join_first = True, precision = 1e-4, join = 'miter', tolerance = 2, ): # Build bounding boxes polygons = np.asarray(polygons) bboxes = _polygons_to_bboxes(polygons) xmin,ymin = np.min(bboxes[:,0:2], axis = 0) - distance xmax,ymax = np.max(bboxes[:,2:4], axis = 0) + distance xsize = xmax - xmin ysize = ymax - ymin xdelta = xsize/num_divisions[0] ydelta = ysize/num_divisions[1] xcorners = xmin + np.arange(num_divisions[0])*xdelta ycorners = ymin + np.arange(num_divisions[1])*ydelta offset_polygons = [] for n,xc in enumerate(xcorners): for m,yc in enumerate(ycorners): left = xc right = xc+xdelta bottom = yc top = yc+ydelta _offset_region_polygons = _offset_region(polygons, bboxes, left, bottom, right, top, distance = distance, join_first = join_first, precision = precision, join = join, tolerance = tolerance, ) offset_polygons += _offset_region_polygons return offset_polygons def _boolean_region(all_polygons_A, all_polygons_B, bboxes_A, bboxes_B, left, bottom, right, top, operation = 'and', precision = 1e-4, ): """ Taking a region of e.g. size (x,y) which needs to be booleaned, this function crops out a region (x, y) large from each set of polygons (A and B), booleans that cropped region and returns the result""" polygons_to_boolean_A = _crop_edge_polygons(all_polygons_A, bboxes_A, left, bottom, right, top, precision) polygons_to_boolean_B = _crop_edge_polygons(all_polygons_B, bboxes_B, left, bottom, right, top, precision) polygons_boolean = clipper.clip(polygons_to_boolean_A, polygons_to_boolean_B, operation, 1/precision) return polygons_boolean def _boolean_polygons_parallel( polygons_A, polygons_B, num_divisions = [10,10], operation = 'and', precision = 1e-4, ): # Build bounding boxes polygons_A = np.asarray(polygons_A) polygons_B = np.asarray(polygons_B) bboxes_A = _polygons_to_bboxes(polygons_A) bboxes_B = _polygons_to_bboxes(polygons_B) xmin,ymin = np.min([np.min(bboxes_A[:,0:2], axis = 0), np.min(bboxes_B[:,0:2], axis = 0)], axis = 0) xmax,ymax = np.max([np.max(bboxes_A[:,2:4], axis = 0), np.max(bboxes_B[:,2:4], axis = 0)], axis = 0) xsize = xmax - xmin ysize = ymax - ymin xdelta = xsize/num_divisions[0] ydelta = ysize/num_divisions[1] xcorners = xmin + np.arange(num_divisions[0])*xdelta ycorners = ymin + np.arange(num_divisions[1])*ydelta boolean_polygons = [] for n,xc in enumerate(xcorners): for m,yc in enumerate(ycorners): left = xc right = xc+xdelta bottom = yc top = yc+ydelta _boolean_region_polygons = _boolean_region(polygons_A, polygons_B, bboxes_A, bboxes_B, left, bottom, right, top, operation = operation, precision = precision, ) boolean_polygons += _boolean_region_polygons return boolean_polygons #============================================================================== # # Lithography test structures # #============================================================================== def litho_steps( line_widths = [1,2,4,8,16], line_spacing = 10, height = 100, layer = 0 ): """ Produces a positive + negative tone linewidth test, used for lithography resolution test patterning """ D = Device('litho_steps') height = height / 2 T1 = text(text = '%s' % str(line_widths[-1]), size = height, justify = 'center', layer = layer) t1 = D.add_ref(T1).rotate(90).movex(-height/10) R1 = rectangle(size = (line_spacing, height), layer = layer) r1 = D.add_ref(R1).movey(-height) count = 0 for i in reversed(line_widths): count += line_spacing + i R2 = rectangle(size = (i, height), layer = layer) r1 = D.add_ref(R1).movex(count).movey(-height) r2 = D.add_ref(R2).movex(count - i) return(D) def litho_star( num_lines = 20, line_width = 2, diameter = 200, layer = 0 ): """ Creates a circular-star shape from lines, used as a lithographic resolution test pattern """ D = Device('litho_star') degree = 180 / num_lines R1 = rectangle(size = (line_width, diameter), layer = layer) for i in range(num_lines): r1 = D.add_ref(R1).rotate(degree * i) r1.center = (0,0) return(D) def litho_calipers( notch_size = [2,5], notch_spacing = 2, num_notches = 11, offset_per_notch = 0.1, row_spacing = 0, layer1 = 1, layer2 = 2): """ Creates a vernier caliper structure for lithography alignment tests. Vernier structure is made horizontally. """ D = Device('litho_calipers') num_notches_total = num_notches*2+1 centre_notch = num_notches R1 = rectangle(size = (notch_size), layer = layer1) R2 = rectangle(size = (notch_size), layer = layer2) for i in range(num_notches_total): if i == centre_notch: r1 = D.add_ref(R1).movex(i * (notch_size[0] + notch_spacing)).movey(notch_size[1]) r2 = D.add_ref(R2).movex(i * (notch_size[0] + notch_spacing) + offset_per_notch * (centre_notch - i)).movey(-2 * notch_size[1] - row_spacing) r1 = D.add_ref(R1).movex(i * (notch_size[0] + notch_spacing)) r2 = D.add_ref(R2).movex(i * (notch_size[0] + notch_spacing) + offset_per_notch * (centre_notch - i)).movey(-notch_size[1] - row_spacing) return(D) #============================================================================== # # Utility functions # #============================================================================== def extract(D, layers = [0,1]): D_extracted = Device('extract') if type(layers) not in (list, tuple): raise ValueError('[PHIDL] pg.extract() Argument `layers` needs to be passed a list or tuple') poly_dict = D.get_polygons(by_spec = True) parsed_layer_list = [_parse_layer(layer) for layer in layers] for layer, polys in poly_dict.items(): if _parse_layer(layer) in parsed_layer_list: D_extracted.add_polygon(polys, layer = layer) return D_extracted def copy(D): D_copy = Device(name = D._internal_name) D_copy.info = python_copy.deepcopy(D.info) for ref in D.references: new_ref = DeviceReference(device = ref.parent, origin = ref.origin, rotation = ref.rotation, magnification = ref.magnification, x_reflection = ref.x_reflection) new_ref.owner = D_copy D_copy.add(new_ref) for alias_name, alias_ref in D.aliases.items(): if alias_ref == ref: D_copy.aliases[alias_name] = new_ref for port in D.ports.values(): D_copy.add_port(port = port) for poly in D.polygons: D_copy.add_polygon(poly) for label in D.labels: D_copy.add_label(text = label.text, position = label.position, layer = (label.layer, label.texttype)) return D_copy def deepcopy(D): D_copy = python_copy.deepcopy(D) D_copy.uid = Device._next_uid Device._next_uid += 1 D_copy._internal_name = D._internal_name D_copy.name = '%s%06d' % (D_copy._internal_name[:20], D_copy.uid) # Write name e.g. 'Unnamed000005' # Make sure _bb_valid is set to false for these new objects so new # bounding boxes are created in the cache for D in D_copy.get_dependencies(True): D._bb_valid = False D_copy._bb_valid = False return D_copy def copy_layer(D, layer = 1, new_layer = 2): D_copied_layer = extract(D, layers = [layer]) D_copied_layer.flatten(single_layer = new_layer) return D_copied_layer def import_gds(filename, cellname = None, flatten = False): gdsii_lib = gdspy.GdsLibrary() gdsii_lib.read_gds(filename) top_level_cells = gdsii_lib.top_level() if cellname is not None: if cellname not in gdsii_lib.cells: raise ValueError('[PHIDL] import_gds() The requested cell (named %s) \ is not present in file %s' % (cellname,filename)) topcell = gdsii_lib.cells[cellname] elif cellname is None and len(top_level_cells) == 1: topcell = top_level_cells[0] elif cellname is None and len(top_level_cells) > 1: raise ValueError('[PHIDL] import_gds() There are multiple top-level cells, \ you must specify `cellname` to select of one of them') if flatten == False: D_list = [] c2dmap = {} for cell in gdsii_lib.cells.values(): D = Device(name = cell.name) D.polygons = cell.polygons D.references = cell.references D.name = cell.name D.labels = cell.labels c2dmap.update({cell:D}) D_list += [D] for D in D_list: # First convert each reference so it points to the right Device converted_references = [] for e in D.references: ref_device = c2dmap[e.ref_cell] if isinstance(e, gdspy.CellReference): dr = DeviceReference( device = ref_device, origin = e.origin, rotation = e.rotation, magnification = e.magnification, x_reflection = e.x_reflection, ) dr.owner = D converted_references.append(dr) elif isinstance(e, gdspy.CellArray): dr = CellArray( device = ref_device, columns = e.columns, rows = e.rows, spacing = e.spacing, origin = e.origin, rotation = e.rotation, magnification = e.magnification, x_reflection = e.x_reflection, ) dr.owner = D converted_references.append(dr) D.references = converted_references # Next convert each Polygon temp_polygons = list(D.polygons) D.polygons = [] for p in temp_polygons: D.add_polygon(p) topdevice = c2dmap[topcell] return topdevice elif flatten == True: D = Device('import_gds') polygons = topcell.get_polygons(by_spec = True) for layer_in_gds, polys in polygons.items(): D.add_polygon(polys, layer = layer_in_gds) return D def _translate_cell(c): D = Device(name = c.name) for e in c.elements: if isinstance(e, gdspy.PolygonSet): for n, points in enumerate(e.polygons): polygon_layer = _parse_layer((e.layers[n], e.datatypes[n])) D.add_polygon(points = points, layer = polygon_layer) elif isinstance(e, gdspy.CellReference): dr = DeviceReference(device = _translate_cell(e.ref_cell), origin = e.origin, rotation = e.rotation, magnification = None, x_reflection = e.x_reflection) D.elements.append(dr) D.labels = c.labels return D def preview_layerset(ls, size = 100, spacing = 100): """ Generates a preview Device with representations of all the layers, used for previewing LayerSet color schemes in quickplot or saved .gds files """ D = Device() scale = size/100 num_layers = len(ls._layers) matrix_size = int(np.ceil(np.sqrt(num_layers))) sorted_layers = sorted(ls._layers.values(), key = lambda x: (x.gds_layer, x.gds_datatype)) for n, layer in enumerate(sorted_layers): R = rectangle(size = (100*scale, 100*scale), layer = layer) T = text( text = '%s\n%s / %s' % (layer.name, layer.gds_layer, layer.gds_datatype), size = 20*scale, justify = 'center', layer = layer) T.move((50*scale,-20*scale)) xloc = n % matrix_size yloc = int(n // matrix_size) D.add_ref(R).movex((100+spacing) * xloc *scale).movey(-(100+spacing) * yloc*scale) D.add_ref(T).movex((100+spacing) * xloc *scale).movey(-(100+spacing) * yloc*scale) return D class device_lru_cache: def __init__(self, fn): self.maxsize = 32 self.fn = fn self.memo = OrderedDict() update_wrapper(self, fn) def __call__(self, *args, **kwargs): pickle_str = pickle.dumps(args, 1) + pickle.dumps(kwargs, 1) if pickle_str not in self.memo.keys(): new_cache_item = self.fn(*args, **kwargs) if not isinstance(new_cache_item, Device): raise ValueError('[PHIDL] @device_lru_cache can only be used on functions which return a Device') if len(self.memo) > self.maxsize: self.memo.popitem(last = False) # Remove oldest item from cache # Add a deepcopy of new item to cache so that if we change the # returned device, our stored cache item is not changed self.memo[pickle_str] = python_copy.deepcopy(new_cache_item) return new_cache_item else: # if found in cache # Pop cache item out and put it back on the top of the cache cached_output = self.memo.pop(pickle_str) self.memo[pickle_str] = cached_output # Then return a copy of the cached Device return deepcopy(cached_output) def _convert_port_to_geometry(port, layer = 0): ''' Converts a Port to a label and a triangle Device that are then added to the parent. The Port must start with a parent. ''' if port.parent is None: raise ValueError('Port {}: Port needs a parent in which to draw'.format(port.name)) if isinstance(port.parent, DeviceReference): device = port.parent.parent else: device = port.parent # A visual marker triangle_points = [[0, 0]] * 3 triangle_points[0] = port.endpoints[0] triangle_points[1] = port.endpoints[1] triangle_points[2] = (port.midpoint + (port.normal - port.midpoint) * port.width / 10)[1] device.add_polygon(triangle_points, layer) # Label carrying actual information that will be recovered label_contents = (str(port.name), # port.midpoint, # rather than put this in the text, use the label position float(np.round(port.width, decimals=3)), # this can have rounding errors that are less than a nanometer float(port.orientation), # device, # this is definitely not serializable # port.info, # would like to include, but it might go longer than 1024 characters # port.uid, # not including because it is part of the build process, not the port state ) label_text = json.dumps(label_contents) device.add_label(text = label_text, position = port.midpoint + _calculate_label_offset(port), magnification = .04 * port.width, rotation = (90 + port.orientation) % 360, layer = layer) def _calculate_label_offset(port): ''' Used to put the label in a pretty position. It is added when drawing and substracted when extracting. ''' offset_position = np.array((-np.cos(np.pi / 180 * port.orientation), -np.sin(np.pi / 180 * port.orientation))) offset_position *= port.width * .05 return offset_position def _convert_geometry_to_port(label, layer = 0): ''' Converts a label into a Port in the parent Device. The label contains name, width, orientation. Does not remove that label from the parent. Returns the new port. ''' name, width, orientation = json.loads(label.text) new_port = Port(name=name, width=width, orientation=orientation) new_port.midpoint = label.position - _calculate_label_offset(new_port) return new_port def ports_to_geometry(device, layer = 0): ''' Converts Port objects over the whole Device hierarchy to geometry and labels. layer: the special port record layer Does not change the device used as argument. Returns a new one lacking all Ports. ''' temp_device = deepcopy(device) all_cells = list(temp_device.get_dependencies(recursive=True)) all_cells.append(temp_device) for subcell in all_cells: for port in subcell.ports.values(): _convert_port_to_geometry(port, layer=layer) subcell.remove(port) return temp_device def geometry_to_ports(device, layer = 0): ''' Converts geometry representing ports over the whole Device hierarchy into Port objects. layer: the special port record layer Does not mutate the device in the argument. Returns a new one lacking all port geometry (incl. labels) ''' temp_device = deepcopy(device) all_cells = list(temp_device.get_dependencies(recursive=True)) all_cells.append(temp_device) for subcell in all_cells: # Walk through cells for lab in subcell.labels: if lab.layer == layer: the_port = _convert_geometry_to_port(lab) subcell.add_port(name=the_port.name, port=the_port) temp_device.remove_layers(layers=[layer], include_labels=True) return temp_device #============================================================================== # # Connectors # #============================================================================== def connector(midpoint = (0,0), width = 1, orientation = 0): """ Creates a Device which has back-to-back ports """ D = Device(name = 'connector') D.add_port(name = 1, midpoint = [midpoint[0], midpoint[1]], width = width, orientation = orientation) D.add_port(name = 2, midpoint = [midpoint[0], midpoint[1]], width = width, orientation = orientation-180) return D #============================================================================== # # Contact pads # #============================================================================== def compass(size = (4,2), layer = 0): """ Creates a rectangular contact pad with centered ports on edges of the rectangle (north, south, east, and west) """ D = Device(name = 'compass') r = D.add_ref( rectangle(size, layer = layer) ) r.center = (0,0) dx = size[0] dy = size[1] D.add_port(name = 'N', midpoint = [0, dy/2], width = dx, orientation = 90) D.add_port(name = 'S', midpoint = [0, -dy/2], width = dx, orientation = -90) D.add_port(name = 'E', midpoint = [dx/2, 0], width = dy, orientation = 0) D.add_port(name = 'W', midpoint = [-dx/2, 0], width = dy, orientation = 180) return D def compass_multi(size = (4,2), ports = {'N':3,'S':4}, layer = 0): """ Creates a rectangular contact pad with multiple ports along the edges rectangle (north, south, east, and west). """ D = Device(name = 'compass_multi') r = D.add_ref( rectangle(size, layer = layer) ) r.center = (0,0) dx = size[0]/2 dy = size[1]/2 if 'N' in ports: num_ports = ports['N'] m = dx-dx/num_ports p_list = np.linspace(-m, m, num_ports) [D.add_port(name = ('N%s' % (n+1)), midpoint = [p, dy], width = dx/num_ports*2, orientation = 90) for n,p in enumerate(p_list)] if 'S' in ports: num_ports = ports['S'] m = dx-dx/num_ports p_list = np.linspace(-m, m, num_ports) [D.add_port(name = ('S%s' % (n+1)), midpoint = [p, -dy], width = dx/num_ports*2, orientation = -90) for n,p in enumerate(p_list)] if 'E' in ports: num_ports = ports['E'] m = dy-dy/num_ports p_list = np.linspace(-m, m, num_ports) [D.add_port(name = ('E%s' % (n+1)), midpoint = [dx, p], width = dy/num_ports*2, orientation = 0) for n,p in enumerate(p_list)] if 'W' in ports: num_ports = ports['W'] m = dy-dy/num_ports p_list = np.linspace(-m, m, num_ports) [D.add_port(name = ('W%s' % (n+1)), midpoint = [-dx, p], width = dy/num_ports*2, orientation = 180) for n,p in enumerate(p_list)] return D # TODO: Fix the fillet here, right now only goes halfway down def flagpole(size = (4,2), stub_size = (2,1), shape = 'p', taper_type = 'straight', layer = 0): f = np.array(size) p = np.array(stub_size) shape = shape.lower() assert shape in 'pqbd', '[DEVICE] flagpole() shape must be p, q, b, or d' assert taper_type in ['straight','fillet',None], '[DEVICE] flagpole() taper_type must "straight" or "fillet" or None' if shape == 'p': orientation = -90 elif shape == 'q': f[0], p[0] = -size[0], -stub_size[0] orientation = -90 elif shape == 'b': f[1], p[1] = -size[1], -stub_size[1] orientation = 90 elif shape == 'd': f[1], p[1] = -size[1], -stub_size[1] f[0], p[0] = -size[0], -stub_size[0] orientation = 90 xpts = [0, 0, f[0], f[0], p[0], p[0], 0] ypts = [0, f[1], f[1], 0, 0, -p[1], -p[1]] D = Device(name = 'flagpole') pad_poly = D.add_polygon([xpts,ypts], layer = layer) if taper_type == 'fillet': taper_amount = min([abs(f[0]-p[0]), abs(p[1])]) pad_poly.fillet([0,0,0,0,taper_amount,0,0]) elif taper_type == 'straight': D.add_polygon([xpts[3:6],ypts[3:6]], layer = layer) D.add_port(name = 1, midpoint = [p[0]/2, -p[1]], width = abs(p[0]), orientation = orientation) D.add_port(name = 2, midpoint = [f[0]/2, f[1]], width = abs(f[0]), orientation = orientation-180) return D def tee(size = (4,2), stub_size = (2,1), taper_type = None, layer = 0): f = np.array(size) p = np.array(stub_size) xpts = np.array([f[0], f[0], p[0], p[0], -p[0], -p[0], -f[0], -f[0]])/2 ypts = [f[1], 0, 0, -p[1], -p[1], 0, 0, f[1]] D = Device(name = 'tee') pad_poly = D.add_polygon([xpts,ypts], layer = layer) if taper_type == 'fillet': taper_amount = min([abs(f[0]-p[0]), abs(p[1])]) pad_poly.fillet([0,0,taper_amount,0,0,taper_amount,0,0]) elif taper_type == 'straight': taper_poly1 = D.add_polygon([xpts[1:4],ypts[1:4]], layer = layer) taper_poly2 = D.add_polygon([xpts[4:7],ypts[4:7]], layer = layer) D.add_port(name = 1, midpoint = [f[0]/2, f[1]/2], width = f[1], orientation = 0) D.add_port(name = 2, midpoint = [-f[0]/2, f[1]/2], width = f[1], orientation = 180) D.add_port(name = 3, midpoint = [0, -p[1]], width = p[0], orientation = -90) return D #============================================================================== # Example code #============================================================================== #cp = compass(size = [4,2]) #quickplot(cp) #cpm = compass_multi(size = [40,20], ports = {'N':3,'S':4, 'E':1, 'W':8}, layer = 0) #quickplot(cpm) #cpm = compass_multi(size = [40,20], ports = {'N':3,'S':4, 'E':1, 'W':8}, layer = 0) #inset_polygon = offset(cpm, distance = -2, layer = 1) #cpm.add_polygon(inset_polygon) #quickplot(cpm) #fp = flagpole(size = [4,2], stub_size = [2,1], shape = 'p', taper_type = 'straight', layer = 0) #quickplot(fp) #tp = tee(size = [4,2], stub_size = [2,1], taper_type = 'fillet', layer = 0) #quickplot(tp) #============================================================================== # # Tapers # #============================================================================== # TODO change this so "width1" and "width2" arguments can accept Port directly def taper(length = 10, width1 = 5, width2 = None, port = None, layer = 0): if type(port) is Port and width1 is None: width1 = port.width if width2 is None: width2 = width1 xpts = [0, length, length, 0] ypts = [width1/2, width2/2, -width2/2, -width1/2] D = Device('taper') D.add_polygon([xpts,ypts], layer = layer) D.add_port(name = 1, midpoint = [0, 0], width = width1, orientation = 180) D.add_port(name = 2, midpoint = [length, 0], width = width2, orientation = 0) if type(port) is Port: D.rotate(angle = port.orientation, center = [0,0]) D.move(origin = [0,0], destination = port.midpoint) return D def ramp(length = 10, width1 = 5, width2 = 8, layer = 0): if width2 is None: width2 = width1 xpts = [0, length, length, 0] ypts = [width1, width2, 0, 0] D = Device('ramp') D.add_polygon([xpts,ypts], layer = layer) D.add_port(name = 1, midpoint = [0, width1/2], width = width1, orientation = 180) D.add_port(name = 2, midpoint = [length, width2/2], width = width2, orientation = 0) return D # Equations taken from # Hammerstad, E., & Jensen, O. (1980). Accurate Models for Microstrip # Computer-Aided Design. http://doi.org/10.1109/MWSYM.1980.1124303 def _microstrip_Z(wire_width, dielectric_thickness, eps_r): # Note these equations can be further corrected for thick films (Hammersted Eqs 6-9) # and also for frequency since microstrips are dispersive (Hammersted Eqs 10-12) u = wire_width/dielectric_thickness eta = 376.73 # Vacuum impedance a = 1 + log((u**4 + (u/52)**2)/(u**4 + 0.432))/49 + log(1 + (u/18.1)**3)/18.7; b = 0.564*((eps_r-0.9)/(eps_r+3))**0.053; F = 6 + (2*pi-6)*exp(-(30.666/u)**0.7528); eps_eff = 0.5*(eps_r+1) + 0.5*(eps_r-1)*(1 + 10/u)**(-a*b); Z = eta/(2*pi) * log(F/u + sqrt(1+(2/u)**2)) /sqrt(eps_eff); return Z,eps_eff def _microstrip_LC_per_meter(wire_width, dielectric_thickness, eps_r): # Use the fact that v = 1/sqrt(L_m*C_m) = 1/sqrt(eps*mu) and # Z = sqrt(L_m/C_m) [Where L_m is inductance per meter] Z, eps_eff = _microstrip_Z(wire_width, dielectric_thickness, eps_r) eps0 = 8.854e-12 mu0 = 4*pi*1e-7 eps = eps_eff*eps0 mu = mu0 L_m = sqrt(eps*mu)*Z C_m = sqrt(eps*mu)/Z return L_m, C_m def _microstrip_Z_with_Lk(wire_width, dielectric_thickness, eps_r, Lk_per_sq): # Add a kinetic inductance and recalculate the impedance, be careful # to input Lk as a per-meter inductance L_m, C_m = _microstrip_LC_per_meter(wire_width, dielectric_thickness, eps_r) Lk_m = Lk_per_sq*(1.0/wire_width) Z = sqrt((L_m+Lk_m)/C_m) return Z def _microstrip_v_with_Lk(wire_width, dielectric_thickness, eps_r, Lk_per_sq): L_m, C_m = _microstrip_LC_per_meter(wire_width, dielectric_thickness, eps_r) Lk_m = Lk_per_sq*(1.0/wire_width) v = 1/sqrt((L_m+Lk_m)*C_m) return v def _find_microstrip_wire_width(Z_target, dielectric_thickness, eps_r, Lk_per_sq): def error_fun(wire_width): Z_guessed = _microstrip_Z_with_Lk(wire_width, dielectric_thickness, eps_r, Lk_per_sq) return (Z_guessed-Z_target)**2 # The error x0 = dielectric_thickness try: from scipy.optimize import fmin except: raise ImportError(""" [PHIDL] To run the microsctrip functions you need scipy, please install it with `pip install scipy` """) w = fmin(error_fun, x0, args=(), disp=False) return w[0] def _G_integrand(xip, B): try: from scipy.special import iv as besseli except: """ [PHIDL] To run this function you need scipy, please install it with pip install scipy """ return besseli(0, B*sqrt(1-xip**2)) def _G(xi, B): try: import scipy.integrate except: raise ImportError(""" [PHIDL] To run the microsctrip functions you need scipy, please install it with `pip install scipy` """) return B/sinh(B)*scipy.integrate.quad(_G_integrand, 0, xi, args = (B))[0] @device_lru_cache def hecken_taper(length = 200, B = 4.0091, dielectric_thickness = 0.25, eps_r = 2, Lk_per_sq = 250e-12, Z1 = None, Z2 = None, width1 = None, width2 = None, num_pts = 100, layer = 0): if width1 is not None: Z1 = _microstrip_Z_with_Lk(width1*1e-6, dielectric_thickness*1e-6, eps_r, Lk_per_sq) if width2 is not None: Z2 = _microstrip_Z_with_Lk(width2*1e-6, dielectric_thickness*1e-6, eps_r, Lk_per_sq) xi_list = np.linspace(-1,1, num_pts) # Normalized length of the wire [-1 to +1] Z = [np.exp( 0.5*log(Z1*Z2) + 0.5*log(Z2/Z1)*_G(xi, B) ) for xi in xi_list] widths = np.array([_find_microstrip_wire_width(z, dielectric_thickness*1e-6, eps_r, Lk_per_sq)*1e6 for z in Z]) x = ((xi_list/2)*length) # Compensate for varying speed of light in the microstrip by shortening # and lengthening sections according to the speed of light in that section v = np.array([_microstrip_v_with_Lk(w*1e-6, dielectric_thickness*1e-6, eps_r, Lk_per_sq) for w in widths]) dx = np.diff(x) dx_compensated = dx*v[:-1] x_compensated = np.cumsum(dx_compensated) x = np.hstack([0,x_compensated])/max(x_compensated)*length # Create blank device and add taper polygon D = Device('hecken') xpts = np.concatenate([x, x[::-1]]) ypts = np.concatenate([widths/2, -widths[::-1]/2]) D.add_polygon((xpts,ypts), layer = layer) D.add_port(name = 1, midpoint = (0,0), width = widths[0], orientation = 180) D.add_port(name = 2, midpoint = (length,0), width = widths[-1], orientation = 0) # Add meta information about the taper D.info['num_squares'] = np.sum(np.diff(x)/widths[:-1]) D.info['width1'] = widths[0] D.info['width2'] = widths[-1] D.info['Z1'] = Z[0] D.info['Z2'] = Z[-1] # Note there are two values for v/c (and f_cutoff) because the speed of # light is different at the beginning and end of the taper D.info['w'] = widths D.info['x'] = x D.info['Z'] = Z D.info['v/c'] = v/3e8 D.info['time_length'] = np.sum(np.diff(D.info['x']*1e-6)/(D.info['v/c'][:-1]*3e8)) BetaLmin = np.sqrt(B**2 + 6.523) D.info['f_cutoff'] = 1/(2*D.info['time_length']) D.info['length'] = length return D @device_lru_cache def meander_taper(x_taper, w_taper, meander_length = 1000, spacing_factor = 3, min_spacing = 0.5, layer = 0): def taper_width(x): return np.interp(x, x_taper, w_taper) def taper_section(x_start, x_end, num_pts = 30, layer = 0): D = Device('tapersec') length = x_end - x_start x = np.linspace(0, length, num_pts) widths = np.linspace(taper_width(x_start), taper_width(x_end), num_pts) xpts = np.concatenate([x, x[::-1]]) ypts = np.concatenate([widths/2, -widths[::-1]/2]) D.add_polygon((xpts,ypts), layer = layer) D.add_port(name = 1, midpoint = (0,0), width = widths[0], orientation = 180) D.add_port(name = 2, midpoint = (length,0), width = widths[-1], orientation = 0) return D def arc_tapered(radius = 10, width1 = 1, width2 = 2, theta = 45, angle_resolution = 2.5, layer = 0): D = Device('arctaper') path1 = gdspy.Path(width = width1, initial_point = (0, 0)) path1.turn(radius = radius, angle = theta*np.pi/180, number_of_points=int(abs(2*theta/angle_resolution)), final_width = width2) [D.add_polygon(p, layer = layer) for p in path1.polygons] D.add_port(name = 1, midpoint = (0, 0), width = width1, orientation = 180) D.add_port(name = 2, midpoint = (path1.x, path1.y), width = width2, orientation = path1.direction*180/np.pi) return D D = Device('meander-taper') xpos1 = min(x_taper) xpos2 = min(x_taper) + meander_length t = D.add_ref( taper_section(x_start = xpos1, x_end = xpos2, num_pts = 50, layer = layer) ) D.add_port(t.ports[1]) dir_toggle = -1 while xpos2 < max(x_taper): arc_width1 = taper_width(xpos2) arc_radius = max(spacing_factor*arc_width1, min_spacing) arc_length = np.pi*arc_radius arc_width2 = taper_width(xpos2 + arc_length) A = arc_tapered(radius = arc_radius, width1 = arc_width1, width2 = arc_width2, theta = 180*dir_toggle, layer = layer) a = D.add_ref(A) a.connect(port = 1, destination = t.ports[2]) dir_toggle = -dir_toggle xpos1 = xpos2 + arc_length xpos2 = xpos1 + meander_length t = D.add_ref( taper_section(x_start = xpos1, x_end = xpos2, num_pts = 30, layer = layer) ) t.connect(port = 1, destination = a.ports[2]) D.add_port(t.ports[2]) return D #============================================================================== # Example code #============================================================================== #D = racetrack_gradual(width, R = 5, N=3) #quickplot(D) # D = hecken_taper(length = 200, B = 4.0091, dielectric_thickness = 0.25, eps_r = 2, # Lk_per_sq = 250e-12, Z1 = 50, width2 = 0.3, # num_pts = 100, layer = 0) # quickplot(D) #t = np.linspace(0,1) #x,y = _racetrack_gradual_parametric(t, R = 5, N = 3) #plt.plot(x,y) #plt.axis('equal') #============================================================================== # # Text # #============================================================================== def text(text = 'abcd', size = 10, justify = 'left', layer = 0): scaling = size/1000 position = (0,0) xoffset = 0 yoffset = 0 t = Device('text') for line in text.split('\n'): l = Device(name = 'textline') for c in line: ascii_val = ord(c) if c == ' ': xoffset += 500*scaling elif (33 <= ascii_val <= 126) or (ascii_val == 181): for poly in _glyph[ascii_val]: xpts = np.array(poly)[:,0]*scaling ypts = np.array(poly)[:,1]*scaling l.add_polygon([xpts + xoffset,ypts + yoffset], layer=layer) xoffset += (_width[ascii_val] + _indent[ascii_val])*scaling else: valid_chars = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~ยต' warnings.warn('[PHIDL] text(): Warning, some characters ignored, no geometry for character "%s" with ascii value %s. ' \ 'Valid characters: %s' % (chr(ascii_val), ascii_val,valid_chars)) t.add_ref(l) yoffset -= 1500*scaling xoffset = position[0] justify = justify.lower() for l in t.references: if justify == 'left': pass if justify == 'right': l.xmax = position[0] if justify == 'center': l.move(origin = l.center, destination = position, axis = 'x') t.flatten() return t #============================================================================== # Example code #============================================================================== #D = text('the quick brown\n fox jumped over\nthe lazy dog', justify = 'center', size = 800) #quickplot(D) #============================================================================== # # Wafer and die # #============================================================================== def basic_die( size = (10000, 10000), street_width = 100, street_length = 1000, die_name = 'chip99', text_size = 100, text_location = 'SW', layer = 0, draw_bbox = True, bbox_layer = 99, ): D = Device(name = 'die') sx, sy = size[0]/2, size[1]/2 xpts = np.array([sx, sx, sx-street_width, sx-street_width, sx-street_length, sx-street_length]) ypts = np.array([sy, sy-street_length, sy-street_length, sy-street_width, sy-street_width, sy]) D.add_polygon([xpts,ypts], layer = layer) D.add_polygon([-xpts,ypts], layer = layer) D.add_polygon([xpts,-ypts], layer = layer) D.add_polygon([-xpts,-ypts], layer = layer) if draw_bbox is True: D.add_polygon([[sx,sy], [sx,-sy],[-sx,-sy],[-sx,sy]], layer = bbox_layer) D.center = (0,0) t = D.add_ref(text(text = die_name, size = text_size, layer=layer)) d = street_width + 20 if type(text_location) is str: text_location = text_location.upper() if text_location == 'NW': t.xmin, t.ymax = [-sx + d, sy - d] elif text_location == 'N': t.x, t.ymax = [0, sy - d] elif text_location == 'NE': t.xmax, t.ymax = [sx - d, sy - d] if text_location == 'SW': t.xmin, t.ymin = [-sx + d, -sy + d] elif text_location == 'S': t.x, t.ymin = [0, -sy + d] elif text_location == 'SE': t.xmax, t.ymin = [sx - d, -sy + d] else: t.x, t.y = text_location return D #============================================================================== # Example code #============================================================================== # D = basic_die(size = (10000, 10000), street_width = 100, street_length = 1000, # die_name = 'chip99', text_size = 300, text_location = 'SW', layer = 0, # draw_bbox = True, bbox_layer = 99) # quickplot(D) #============================================================================== # # Waveguide curves # #============================================================================== def racetrack_gradual(width = 0.3, R = 5, N = 3, layer = 0): curve_fun = lambda t: _racetrack_gradual_parametric(t, R = 5, N = 3) route_path = gdspy.Path(width = width, initial_point = [0,0]) route_path.parametric(curve_fun, number_of_evaluations=99,\ max_points=4000, final_distance=None, layer=layer) D = Device('racetrack') D.add(route_path) return D def _racetrack_gradual_parametric(t, R, N): """ Takes in a parametric value ``t`` on (0,1), returns the x,y coordinates of a racetrack bent according to 20090810_EOS4_modulator_designs_excerptForJasonGradualBends.ppt """ x0 = R/2**(1/N) Rmin = 2**(0.5-1/N)/(N-1)*R R0 = R-(x0-Rmin/sqrt(2)) t = np.array(t) x,y = np.zeros(t.shape), np.zeros(t.shape) # Doing the math x = cos(t*pi/2)*R0 # t (0-1) while x (0 to R0) ii = (Rmin/sqrt(2) < x) & (x <= R0) jj = (0 < x) & (x <= Rmin/sqrt(2)) y[ii] = (R**N - (x[ii]+(x0-Rmin/sqrt(2)))**N)**(1/N) y[jj] = (x0-Rmin/sqrt(2))+sqrt(Rmin**2-x[jj]**2) return x,y #============================================================================== # Example code #============================================================================== # D = racetrack_gradual(width = 0.3, R = 5, N = 3) # quickplot(D) # t = np.linspace(0,1) # x,y = _racetrack_gradual_parametric(t, R = 5, N = 3) # plt.plot(x,y) # plt.axis('equal') #============================================================================== # # Packing and fill # #============================================================================== def _pack_single_bin( rect_dict, aspect_ratio, max_size, sort_by_area, density, precision, verbose, ): """ Takes a `rect_dict` argument of the form {id:(w,h)} and tries to pack it into a bin as small as possible with aspect ratio `aspect_ratio` Will iteratively grow the bin size until everything fits or the bin size reaches `max_size`. Returns a dictionary of of the packed rectangles in the form {id:(x,y,w,h)}, and a dictionary of remaining unpacked rects """ try: import rectpack except: raise ImportError('[PHIDL] The packer() function requires the module "rectpack"' + "to operate. Please retry after installing rectpack:" + "\n\n$ pip install rectpack") # Compute total area and use it for an initial estimate of the bin size total_area = 0 for r in rect_dict.values(): total_area += r[0]*r[1] aspect_ratio = np.asarray(aspect_ratio)/np.linalg.norm(aspect_ratio) # Normalize # Setup variables box_size = np.asarray(aspect_ratio*np.sqrt(total_area), dtype = np.float64) box_size = np.clip(box_size, None, max_size) if sort_by_area == True: rp_sort = rectpack.SORT_AREA else: rp_sort = rectpack.SORT_NONE # Repeatedly run the rectangle-packing algorithm with increasingly larger # areas until everything fits or we've reached the maximum size while True: # Create the packer object rect_packer = rectpack.newPacker( mode = rectpack.PackingMode.Offline, pack_algo = rectpack.MaxRectsBlsf, sort_algo = rp_sort, bin_algo= rectpack.PackingBin.BBF, rotation=False,) # Add each rectangle to the packer, create a single bin, and pack for rid, r in rect_dict.items(): rect_packer.add_rect(width = r[0], height = r[1], rid = rid) rect_packer.add_bin(width = box_size[0], height = box_size[1]) rect_packer.pack() # Adjust the box size for next time box_size *= density # Increase area to try to fit box_size = np.clip(box_size, None, max_size) if verbose == True: print('Trying to pack in bin size (%0.2f, %0.2f)' % tuple(box_size*precision)) # Quit the loop if we've packed all the rectangles or reached the max size if (len(rect_packer.rect_list()) == len(rect_dict)): if verbose == True: print('Success!') break elif all(box_size >= max_size): if verbose == True: print('Reached max_size, creating an additional bin') break # Separate packed from unpacked rectangles, make dicts of form {id:(x,y,w,h)} packed_rect_dict = {r[-1]:r[:-1] for r in rect_packer[0].rect_list()} unpacked_rect_dict = {} for k,v in rect_dict.items(): if k not in packed_rect_dict: unpacked_rect_dict[k] = v return (packed_rect_dict, unpacked_rect_dict) def packer( D_list, spacing = 10, aspect_ratio = (1,1), max_size = (None,None), sort_by_area = True, density = 1.1, precision = 1e-2, verbose = False, ): if density < 1.01: raise ValueError("[PHIDL] packer() was given a `density` argument that is" + " too small. The density argument must be >= 1.01") # Santize max_size variable max_size = [np.inf if v is None else v for v in max_size] max_size = np.asarray(max_size, dtype = np.float64) # In case it's integers max_size = max_size/precision # Convert Devices to rectangles rect_dict = {} for n, D in enumerate(D_list): w,h = (D.size + spacing)/precision w,h = int(w), int(h) if (w > max_size[0]) or (h > max_size[1]): raise ValueError("[PHIDL] packer() failed because one of the objects " + "in `D_list` is has an x or y dimension larger than `max_size` and " + "so cannot be packed") rect_dict[n] = (w,h) packed_list = [] while len(rect_dict) > 0: (packed_rect_dict, rect_dict) = _pack_single_bin(rect_dict, aspect_ratio = aspect_ratio, max_size = max_size, sort_by_area = sort_by_area, density = density, precision = precision, verbose = verbose,) packed_list.append(packed_rect_dict) D_packed_list = [] for rect_dict in packed_list: D_packed = Device() for n, rect in rect_dict.items(): x, y, w, h = rect xcenter = x + w/2 + spacing/2 ycenter = y + h/2 + spacing/2 d = D_packed.add_ref(D_list[n]) d.center = (xcenter*precision,ycenter*precision) D_packed_list.append(D_packed) return D_packed_list def _rasterize_polygons(polygons, bounds = [[-100, -100], [100, 100]], dx = 1, dy = 1): try: from skimage import draw except: raise ImportError(""" The fill function requires the module "scikit-image" to operate. Please retry after installing scikit-image: $ pip install --upgrade scikit-image """) # Prepare polygon array by shifting all points into the first quadrant and # separating points into x and y lists xpts = [] ypts = [] for p in polygons: p_array = np.asarray(p) x = p_array[:,0] y = p_array[:,1] xpts.append((x-bounds[0][0])/dx-0.5) ypts.append((y-bounds[0][1])/dy-0.5) # Initialize the raster matrix we'll be writing to xsize = int(np.ceil((bounds[1][0]-bounds[0][0]))/dx) ysize = int(np.ceil((bounds[1][1]-bounds[0][1]))/dy) raster = np.zeros((ysize, xsize), dtype=np.bool) # TODO: Replace polygon_perimeter with the supercover version for n in range(len(xpts)): rr, cc = draw.polygon(ypts[n], xpts[n], shape=raster.shape) rrp, ccp = draw.polygon_perimeter(ypts[n], xpts[n], shape=raster.shape, clip=False) raster[rr, cc] = 1 raster[rrp, ccp] = 1 return raster def _raster_index_to_coords(i, j, bounds = [[-100, -100], [100, 100]], dx = 1, dy = 1): x = (j+0.5)*dx + bounds[0][0] y = (i+0.5)*dy + bounds[0][1] return x,y def _expand_raster(raster, distance = (4,2)): try: from skimage import draw, morphology except: raise ImportError(""" The fill function requires the module "scikit-image" to operate. Please retry after installing scikit-image: $ pip install --upgrade scikit-image """) if distance[0] <= 0.5 and distance[1] <= 0.5: return raster num_pixels = np.array(np.ceil(distance), dtype = int) neighborhood = np.zeros((num_pixels[1]*2+1, num_pixels[0]*2+1), dtype=np.bool) rr, cc = draw.ellipse(num_pixels[1], num_pixels[0], distance[1]+0.5, distance[0]+0.5) neighborhood[rr, cc] = 1 return morphology.binary_dilation(image = raster, selem=neighborhood) def _fill_cell_rectangle(size = (20,20), layers = (0,1,3), densities = (0.5, 0.25, 0.7), inverted = (False, False, False)): D = Device('fillcell') for layer, density, inv in zip(layers, densities, inverted): rectangle_size = np.array(size)*np.sqrt(density) # r = D.add_ref(rectangle(size = rectangle_size, layer = layer)) R = rectangle(size = rectangle_size, layer = layer) R.center = (0,0) if inv is True: A = rectangle(size = size) A.center = (0,0) A = A.get_polygons() B = R.get_polygons() p = gdspy.boolean(A, B, operation = 'not') D.add_polygon(p, layer = layer) else: D.add_ref(R) return D def _loop_over(var): # Checks if a variable is in the form of an iterable (list/tuple) # and if not, returns it as a list. Useful for allowing argument # inputs to be either lists (e.g. [1,3,4]) or single-valued (e.g. 3) if hasattr(var,"__iter__"): return var else: return [var] def fill_rectangle(D, fill_size = (40,10), avoid_layers = 'all', include_layers = None, margin = 100, fill_layers = (0,1,3), fill_densities = (0.5, 0.25, 0.7), fill_inverted = None, bbox = None): # Create the fill cell. If fill_inverted is not specified, assume all False fill_layers = _loop_over(fill_layers) fill_densities = _loop_over(fill_densities) if fill_inverted is None: fill_inverted = [False]*len(fill_layers) fill_inverted = _loop_over(fill_inverted) if len(fill_layers) != len(fill_densities): raise ValueError("[PHIDL] phidl.geometry.fill_rectangle() `fill_layers` and" + " `fill_densities` parameters must be lists of the same length") if len(fill_layers) != len(fill_inverted): raise ValueError("[PHIDL] phidl.geometry.fill_rectangle() `fill_layers` and" + " `fill_inverted` parameters must be lists of the same length") fill_cell = _fill_cell_rectangle(size = fill_size, layers = fill_layers, densities = fill_densities, inverted = fill_inverted) F = Device(name = 'fill_pattern') if avoid_layers == 'all': exclude_polys = D.get_polygons(by_spec=False, depth=None) else: avoid_layers = [_parse_layer(l) for l in _loop_over(avoid_layers)] exclude_polys = D.get_polygons(by_spec=True, depth=None) exclude_polys = {key:exclude_polys[key] for key in exclude_polys if key in avoid_layers} exclude_polys = itertools.chain.from_iterable(exclude_polys.values()) if include_layers is None: include_polys = [] else: include_layers = [_parse_layer(l) for l in _loop_over(include_layers)] include_polys = D.get_polygons(by_spec=True, depth=None) include_polys = {key:include_polys[key] for key in include_polys if key in include_layers} include_polys = itertools.chain.from_iterable(include_polys.values()) if bbox is None: bbox = D.bbox raster = _rasterize_polygons(polygons = exclude_polys, bounds = bbox, dx = fill_size[0], dy = fill_size[1]) raster = raster & ~_rasterize_polygons(polygons = include_polys, bounds = bbox, dx = fill_size[0], dy = fill_size[1]) raster = _expand_raster(raster, distance = margin/np.array(fill_size)) for i in range(np.size(raster,0)): sub_rasters = [list(g) for k, g in itertools.groupby(raster[i])] j = 0 for s in sub_rasters: if s[0] == 0: x,y = _raster_index_to_coords(i, j, bbox, fill_size[0], fill_size[1]) # F.add(gdspy.CellArray(ref_cell = fill_cell, columns = len(s), rows = 1, spacing = fill_size, )) a = F.add_array(fill_cell, columns = len(s), rows = 1, spacing = fill_size) a.move((x, y)) j += len(s) return F #============================================================================== # # Photonics # #============================================================================== def polygon_ports(xpts=[-1,-1, 0, 0], ypts = [0, 1, 1, 0], layer = 0): # returns a polygon with ports on all edges P = Device('polygon') P.add_polygon([xpts, ypts], layer = layer) n = len(xpts) xpts.append(xpts[0]) ypts.append(ypts[0]) #determine if clockwise or counterclockwise cc = 0 for i in range(0,n): cc += ((xpts[i+1]-xpts[i])*(ypts[i+1]+ypts[i])) for i in range(0,n): midpoint_n = [(xpts[i+1]+xpts[i])/2, (ypts[i+1]+ypts[i])/2] orientation_n = np.arctan2(np.sign(cc)*(xpts[i+1]-xpts[i]),np.sign(cc)*(ypts[i]-ypts[i+1]))*180/np.pi width_n = np.abs(np.sqrt((xpts[i+1]-xpts[i])**2+(ypts[i+1]-ypts[i])**2)) P.add_port(name = str(i+1), midpoint = midpoint_n, width = width_n, orientation = orientation_n) return P #============================================================================== # Example code #============================================================================== # P = polygon(xpts=[-1,-3, 0, 0], ypts = [0, 1, 2, 0], layer = 3) # quickplot(P) @device_lru_cache def grating(num_periods = 20, period = 0.75, fill_factor = 0.5, width_grating = 5, length_taper = 10, width = 0.4, partial_etch = False): #returns a fiber grating G = Device('grating') # make the deep etched grating if partial_etch is False: # make the grating teeth for i in range(num_periods): cgrating = G.add_ref(compass(size=[period*fill_factor,width_grating], layer = 0)) cgrating.x+=i*period # make the taper tgrating = G.add_ref(taper(length = length_taper, width1 = width_grating, width2 = width, port = None, layer = 0)) tgrating.xmin = cgrating.xmax # define the port of the grating p = G.add_port(port = tgrating.ports[2], name = 1) # make a partially etched grating if partial_etch is True: # hard coded overlap partetch_overhang = 5 # make the etched areas (opposite to teeth) for i in range(num_periods): cgrating = G.add_ref(compass(size=[period*(1-fill_factor),width_grating+partetch_overhang*2]), layer = 1) cgrating.x+=i*period # define the port of the grating p = G.add_port(port = cgrating.ports['E'], name = 1) p.midpoint=p.midpoint+np.array([(1-fill_factor)*period,0]) #draw the deep etched square around the grating deepbox = G.add_ref(compass(size=[num_periods*period, width_grating]), layer=0) return G #============================================================================== # Example code #============================================================================== # G = grating(num_periods = 20, period = 0.75, fill_factor = 0.5, width_grating = 20, length_taper = 10, width = 0.4, partial_etch = False) # quickplot(G) #============================================================================== # # Test Structures # #============================================================================== # Via Route ---------------------------------------- def _via_iterable(via_spacing, wire_width, wiring1_layer, wiring2_layer, via_layer, via_width): VI = Device('test_via_iter') wire1 = VI.add_ref(compass(size=(via_spacing, wire_width), layer=wiring1_layer)) wire2 = VI.add_ref(compass(size=(via_spacing, wire_width), layer=wiring2_layer)) via1 = VI.add_ref(compass(size=(via_width, via_width), layer=via_layer)) via2 = VI.add_ref(compass(size=(via_width, via_width), layer=via_layer)) wire1.connect(port='E', destination = wire2.ports['W'], overlap=wire_width) via1.connect(port='W', destination = wire1.ports['E'], overlap = (wire_width + via_width)/2) via2.connect(port='W', destination = wire2.ports['E'], overlap = (wire_width + via_width)/2) VI.add_port(name='W', port = wire1.ports['W']) VI.add_port(name='E', port = wire2.ports['E']) VI.add_port(name='S', midpoint = [(1*wire_width)+ wire_width/2,-wire_width/2], width = wire_width, orientation = -90) VI.add_port(name='N', midpoint = [(1*wire_width)+ wire_width/2,wire_width/2], width = wire_width, orientation = 90) return VI def test_via(num_vias = 100, wire_width = 10, via_width = 15, via_spacing = 40, pad_size = (300,300), min_pad_spacing = 0, pad_layer = 0, wiring1_layer = 1, wiring2_layer = 2, via_layer = 3): """ Usage: Call via_route_test_structure() by indicating the number of vias you want drawn. You can also change the other parameters however if you do not specifiy a value for a parameter it will just use the default value Ex:: via_route_test_structure(num_vias=54) - or -:: via_route_test_structure(num_vias=12, pad_size=(100,100),wire_width=8) total requested vias (num_vias) -> this needs to be even pad size (pad_size) -> given in a pair (width, height) wire_width -> how wide each wire should be pad_layer -> GDS layer number of the pads wiring1_layer -> GDS layer number of the top wiring wiring2_layer -> GDS layer number of the bottom wiring via_layer -> GDS layer number of the vias ex: via_route(54, min_pad_spacing=300) """ VR = Device('test_via') pad1 = VR.add_ref(rectangle(size=pad_size, layer=pad_layer)) pad1_overlay = VR.add_ref(rectangle(size=pad_size, layer=wiring1_layer)) pad2 = VR.add_ref(rectangle(size=pad_size, layer=pad_layer)) pad2_overlay = VR.add_ref(rectangle(size=pad_size, layer=wiring1_layer)) nub = VR.add_ref(compass(size=(3*wire_width,wire_width),layer=pad_layer)) nub_overlay = VR.add_ref(compass(size=(3*wire_width,wire_width),layer=wiring1_layer)) head = VR.add_ref(compass(size=(wire_width,wire_width),layer=pad_layer)) head_overlay = VR.add_ref(compass(size=(wire_width,wire_width),layer=wiring1_layer)) nub.ymax = pad1.ymax-5 nub.xmin = pad1.xmax nub_overlay.ymax = pad1.ymax-5 nub_overlay.xmin = pad1.xmax head.connect(port = "W", destination = nub.ports["E"]) head_overlay.connect(port = "W", destination = nub_overlay.ports["E"]) pad1_overlay.xmin = pad1.xmin pad1_overlay.ymin = pad1.ymin old_port = head.ports['S'] count = 0 width_via_iter = 2*via_spacing - 2*wire_width pad2.xmin = pad1.xmax + min_pad_spacing up = False down = True edge = True current_width = 3*wire_width + wire_width #width of nub and 1 overlap obj_old = head obj = head via_iterable = _via_iterable(via_spacing, wire_width, wiring1_layer, wiring2_layer, via_layer, via_width) while( (count+2) <= num_vias): obj = VR.add_ref(via_iterable) obj.connect(port = 'W', destination = old_port, overlap = wire_width) old_port = obj.ports['E'] edge = False if(obj.ymax > pad1.ymax): obj.connect(port = 'W', destination = obj_old.ports['S'], overlap = wire_width) old_port = obj.ports['S'] current_width += width_via_iter down = True up = False edge = True elif(obj.ymin < pad1.ymin): obj.connect(port = 'W', destination = obj_old.ports['N'], overlap = wire_width) old_port = obj.ports['N'] current_width += width_via_iter up = True down = False edge = True count = count + 2 obj_old = obj if(current_width < min_pad_spacing and (min_pad_spacing - current_width) > 3*wire_width): tail = VR.add_ref(compass(size=(min_pad_spacing-current_width+wire_width,wire_width),layer=wiring1_layer)) tail_overlay = VR.add_ref(compass(size=(min_pad_spacing-current_width+wire_width,wire_width),layer=pad_layer)) else: tail = VR.add_ref(compass(size=(3*wire_width,wire_width),layer=wiring1_layer)) tail_overlay = VR.add_ref(compass(size=(3*wire_width,wire_width),layer=wiring1_layer)) if(up == True and edge != True): tail.connect(port = 'W', destination = obj.ports['S'], overlap = wire_width) tail_overlay.connect(port = 'W', destination = obj.ports['S'], overlap = wire_width) elif(down == True and edge != True): tail.connect(port = 'W', destination = obj.ports['N'], overlap = wire_width) tail_overlay.connect(port = 'W', destination = obj.ports['N'], overlap = wire_width) else: tail.connect(port = 'W', destination = obj.ports['E'], overlap = wire_width) tail_overlay.connect(port = 'W', destination = obj.ports['E'], overlap = wire_width) pad2.xmin = tail.xmax pad2_overlay.xmin = pad2.xmin pad2_overlay.ymin = pad2.ymin return VR def test_comb(pad_size = (200,200), wire_width = 1, wire_gap = 3, comb_layer = 0, overlap_zigzag_layer = 1, comb_pad_layer = None, comb_gnd_layer = None, overlap_pad_layer = None): """ Usage: Call comb_insulation_test_structure() with any of the parameters shown below which you'd like to change. You only need to supply the parameters which you intend on changing You can alternatively call it with no parameters and it will take all the default alues shown below. Ex:: comb_insulation_test_structure(pad_size=(175,175), wire_width=2, wire_gap=5) - or -:: comb_insulation_test_structure() """ CI = Device("test_comb") if comb_pad_layer is None: comb_pad_layer = comb_layer if comb_gnd_layer is None: comb_gnd_layer = comb_layer if overlap_pad_layer is None: overlap_pad_layer = overlap_zigzag_layer wire_spacing = wire_width + wire_gap*2 #%% pad overlays overlay_padb = CI.add_ref(rectangle(size=(pad_size[0]*9/10,pad_size[1]*9/10), layer=overlap_pad_layer)) overlay_padl = CI.add_ref(rectangle(size=(pad_size[0]*9/10,pad_size[1]*9/10), layer=comb_pad_layer ) ) overlay_padt = CI.add_ref(rectangle(size=(pad_size[0]*9/10,pad_size[1]*9/10), layer=comb_pad_layer ) ) overlay_padr = CI.add_ref(rectangle(size=(pad_size[0]*9/10,pad_size[1]*9/10), layer=comb_gnd_layer)) overlay_padl.xmin = 0 overlay_padl.ymin = 0 overlay_padb.ymax = 0 overlay_padb.xmin = overlay_padl.xmax + pad_size[1]/5 overlay_padr.ymin = overlay_padl.ymin overlay_padr.xmin = overlay_padb.xmax + pad_size[1]/5 overlay_padt.xmin = overlay_padl.xmax + pad_size[1]/5 overlay_padt.ymin = overlay_padl.ymax #%% pads padl = CI.add_ref(rectangle(size=pad_size, layer=comb_layer)) padt = CI.add_ref(rectangle(size=pad_size, layer=comb_layer)) padr = CI.add_ref(rectangle(size=pad_size, layer=comb_layer)) padb = CI.add_ref(rectangle(size=pad_size, layer=overlap_zigzag_layer)) padl_nub = CI.add_ref(rectangle(size=(pad_size[0]/4,pad_size[1]/2), layer=comb_layer)) padr_nub = CI.add_ref(rectangle(size=(pad_size[0]/4,pad_size[1]/2), layer=comb_layer)) padl.xmin = overlay_padl.xmin padl.center = [padl.center[0],overlay_padl.center[1]] padt.ymax = overlay_padt.ymax padt.center = [overlay_padt.center[0],padt.center[1]] padr.xmax = overlay_padr.xmax padr.center = [padr.center[0],overlay_padr.center[1]] padb.ymin = overlay_padb.ymin padb.center = [overlay_padb.center[0],padb.center[1]] padl_nub.xmin = padl.xmax padl_nub.center = [padl_nub.center[0],padl.center[1]] padr_nub.xmax = padr.xmin padr_nub.center = [padr_nub.center[0],padr.center[1]] #%% connected zig head = CI.add_ref(compass(size=(pad_size[0]/12, wire_width), layer=comb_layer)) head.xmin = padl_nub.xmax head.ymax = padl_nub.ymax connector = CI.add_ref(compass(size=(wire_width, wire_width), layer=comb_layer)) connector.connect(port = 'W', destination=head.ports['E']) old_port = connector.ports['S'] top = True obj = connector while(obj.xmax + pad_size[0]/12 < padr_nub.xmin): #long zig zag rectangle obj = CI.add_ref(compass(size=(pad_size[1]/2 - 2*wire_width, wire_width), layer=comb_layer)) obj.connect(port = 'W', destination=old_port) old_port = obj.ports['E'] if(top): #zig zag edge rectangle obj = CI.add_ref(compass(size=(wire_width, wire_width), layer=comb_layer)) obj.connect(port = 'N', destination=old_port) top = False else: #zig zag edge rectangle obj = CI.add_ref(compass(size=(wire_width, wire_width), layer=comb_layer)) obj.connect(port = 'S', destination=old_port) top = True # comb rectange comb = CI.add_ref(rectangle(size=((padt.ymin-head.ymax)+pad_size[1]/2 - (wire_spacing + wire_width)/2, wire_width), layer=comb_layer)) comb.rotate(90) comb.ymax = padt.ymin comb.xmax = obj.xmax - (wire_spacing+wire_width)/2 old_port = obj.ports['E'] obj = CI.add_ref(compass(size=(wire_spacing, wire_width), layer=comb_layer)) obj.connect(port = 'W', destination=old_port) old_port = obj.ports['E'] obj = CI.add_ref(compass(size=(wire_width, wire_width), layer=comb_layer)) obj.connect(port = 'W', destination=old_port) if(top): old_port = obj.ports['S'] else: old_port = obj.ports['N'] old_port = obj.ports['E'] if(padr_nub.xmin-obj.xmax > 0): tail = CI.add_ref(compass(size=(padr_nub.xmin-obj.xmax, wire_width), layer=comb_layer)) else: tail = CI.add_ref(compass(size=(wire_width, wire_width), layer=comb_layer)) tail.connect(port = 'W', destination=old_port) #%% disconnected zig dhead = CI.add_ref(compass(size=(padr_nub.ymin -padb.ymax - wire_width, wire_width), layer=overlap_zigzag_layer)) dhead.rotate(90) dhead.ymin = padb.ymax dhead.xmax = tail.xmin-(wire_spacing+wire_width)/2 connector = CI.add_ref(compass(size=(wire_width, wire_width), layer=overlap_zigzag_layer)) connector.connect(port = 'S', destination=dhead.ports['E']) old_port = connector.ports['N'] right = True obj = connector while(obj.ymax + wire_spacing + wire_width < head.ymax): obj = CI.add_ref(compass(size=(wire_spacing, wire_width), layer=overlap_zigzag_layer)) obj.connect(port = 'W', destination=old_port) old_port = obj.ports['E'] if(right): obj = CI.add_ref(compass(size=(wire_width, wire_width), layer=overlap_zigzag_layer)) obj.connect(port = 'W', destination=old_port) right = False else: obj = CI.add_ref(compass(size=(wire_width, wire_width), layer=overlap_zigzag_layer)) obj.connect(port = 'E', destination=old_port) right = True old_port = obj.ports['N'] obj = CI.add_ref(compass(size=(dhead.xmin-(head.xmax+head.xmin+wire_width)/2, wire_width), layer=overlap_zigzag_layer)) obj.connect(port = 'E', destination=old_port) old_port = obj.ports['W'] obj = CI.add_ref(compass(size=(wire_width, wire_width), layer=overlap_zigzag_layer)) obj.connect(port = 'S', destination=old_port) if(right): old_port = obj.ports['W'] else: old_port = obj.ports['E'] return CI #This is a helper function to make the Ic step wire structure def _test_ic_wire_step(thick_width = 10, thin_width = 1, wire_layer = 2): WS4 = Device('test_ic_step') wire_stepa = WS4.add_ref(optimal_step(thick_width/2, thin_width/2, layer=wire_layer)) wire_stepb = WS4.add_ref(optimal_step(thin_width/2, thick_width/2, layer=wire_layer)) wire_stepc = WS4.add_ref(optimal_step(thick_width/2, thin_width/2, layer=wire_layer)) wire_stepd = WS4.add_ref(optimal_step(thin_width/2, thick_width/2, layer=wire_layer)) wire_stepb.rotate(180) wire_stepb.xmin = wire_stepa.xmin wire_stepc.rotate(180) wire_stepc.xmin = wire_stepa.xmax wire_stepd.xmin = wire_stepc.xmin return WS4 def test_ic(wire_widths = [0.25, 0.5,1,2,4], wire_widths_wide = [0.75, 1.5, 3, 4, 6], pad_size = (200,200), pad_gap=75, wire_layer = 0, pad_layer = 1, gnd_layer = None): """ Usage: Call ic_test_structure() with either a list of widths for the thickest part of each wire to test and a list for the thinnest parts of each wire. Alternatively, specify a list of widths for the thinnest part of each wire and ignore the wire_widths parameter. Instead you should specify the width_growth_factor which indicates by what factor the thick part of the wire will be larger than the thin part. Ex:: ic_test_structure(wire_widths = [5,10,10,10,10], thin_width=[0.5,1,2,3,4]) - or -:: ic_test_structure(width_growth_factor = 5, thin_width=[0.5,1,2,3,4]) """ ICS = Device('test_ic') if gnd_layer is None: gnd_layer = pad_layer translation = 0 padb = ICS.add_ref(rectangle(size=(np.size(wire_widths) * (pad_size[0]*6/5), pad_size[1]), layer=wire_layer)) padb_overlay = ICS.add_ref(rectangle(size=((np.size(wire_widths) * (pad_size[0]*6/5))*9/10, pad_size[1]*9/10), layer=gnd_layer)) padb_overlay.center = padb.center padb_overlay.ymin = padb.ymin for i, x in enumerate(wire_widths_wide): padt = ICS.add_ref(rectangle(pad_size, wire_layer)) padt.xmin = padb.xmin + translation padt.ymin = padb.ymax + pad_gap padt_overlay = ICS.add_ref(rectangle(size=(pad_size[0]*9/10, pad_size[1]*9/10), layer=pad_layer)) padt_overlay.center = padt.center padt_overlay.ymax = padt.ymax difference = padt.ymin-padb.ymax wire_step = ICS.add_ref(_test_ic_wire_step(wire_widths_wide[i], wire_widths[i], wire_layer=wire_layer)) wire_step.rotate(90) wire_step.center = (padt.center[0], padb.ymax + difference/2) translation = translation + pad_size[0]*12/10 conn_wire_top = ICS.add_ref(rectangle(size=(wire_widths_wide[i], padt.ymin-wire_step.ymax), layer=wire_layer)) conn_wire_bottom = ICS.add_ref(rectangle(size=(wire_widths_wide[i], wire_step.ymin-padb.ymax), layer=wire_layer)) conn_wire_top.ymax = padt.ymin conn_wire_top.xmin = wire_step.xmin conn_wire_bottom.ymin = padb.ymax conn_wire_bottom.xmin = wire_step.xmin return ICS def test_res(pad_size = [50,50], num_squares = 1000, width = 1, res_layer = 0, pad_layer = None, gnd_layer = None): """ Creates an efficient resonator structure for a wafer layout. Keyword arguments: pad_size -- Size of the two matched impedance pads (microns) num_squares -- Number of squares comprising the resonator wire width -- The width of the squares (microns) """ x = pad_size[0] z = pad_size[1] # Checking validity of input if x <= 0 or z <= 0: raise ValueError('Pad must have positive, real dimensions') elif width > z: raise ValueError('Width of cell cannot be greater than height of pad') elif num_squares <= 0: raise ValueError('Number of squares must be a positive real number') elif width <= 0: raise ValueError('Width of cell must be a positive real number') # Performing preliminary calculations num_rows = int(np.floor(z / (2 * width))) if num_rows % 2 == 0: num_rows -= 1 num_columns = num_rows - 1 squares_in_row = (num_squares - num_columns - 2) / num_rows # Compensating for weird edge cases if squares_in_row < 1: num_rows = int(round(num_rows / 2) - 2) squares_in_row = 1 if width * 2 > z: num_rows = 1 squares_in_row = num_squares - 2 length_row = squares_in_row * width # Creating row/column corner combination structure T = Device() Row = rectangle(size = (length_row, width), layer = res_layer) Col = rectangle(size = (width, width), layer = res_layer) row = T.add_ref(Row) col = T.add_ref(Col) col.move([length_row - width, -width]) # Creating entire waveguide net N = Device('net') n = 1 for i in range(num_rows): if i != num_rows - 1: d = N.add_ref(T) else: d = N.add_ref(Row) if n % 2 == 0: d.mirror(p1 = (d.x, d.ymax), p2 = (d.x, d.ymin)) d.movey(-(n - 1) * T.ysize) n += 1 d = N.add_ref(Col).movex(-width) d = N.add_ref(Col).move([length_row, -(n - 2) * T.ysize]) # Creating pads P = Device('pads') Pad1 = rectangle(size = (x,z), layer = pad_layer) Pad2 = rectangle(size = (x + 5, z), layer = pad_layer) Gnd1 = offset(Pad1, distance = -5, layer = gnd_layer) Gnd2 = offset(Pad2, distance = -5, layer = gnd_layer) pad1 = P.add_ref(Pad1).movex(-x - width) pad2 = P.add_ref(Pad1).movex(length_row + width) gnd1 = P.add_ref(Gnd1).center = pad1.center gnd2 = P.add_ref(Gnd2) nets = P.add_ref(N).y = pad1.y gnd2.center = pad2.center gnd2.movex(2.5) return P #============================================================================== # # Optimal current-crowding superconducting structures # #============================================================================== @device_lru_cache def optimal_hairpin(width = 0.2, pitch = 0.6, length = 10, turn_ratio = 4, num_pts = 50, layer = 0): # Optimal structure from https://doi.org/10.1103/PhysRevB.84.174510 # Clem, J., & Berggren, K. (2011). Geometry-dependent critical currents in # superconducting nanocircuits. Physical Review B, 84(17), 1โ27. #========================================================================== # Create the basic geometry #========================================================================== a = (pitch + width)/2 y = -(pitch - width)/2 x = -pitch dl = width/(num_pts*2) n = 0 # Get points of ideal curve from conformal mapping # TODO This is an inefficient way of finding points that you need xpts = [x]; ypts = [y] while (y < 0) & (n<1e6): s = x + 1j*y w = np.sqrt(1 - np.exp(pi*s/a)) wx = np.real(w); wy = np.imag(w) wx = wx/np.sqrt(wx**2+wy**2); wy = wy/np.sqrt(wx**2+wy**2) x = x + wx*dl; y = y + wy*dl xpts.append(x); ypts.append(y) n = n+1 ypts[-1] = 0 # Set last point be on the x=0 axis for sake of cleanliness ds_factor = int(len(xpts)/num_pts) # Downsample the total number of points xpts = xpts[::-ds_factor]; xpts = xpts[::-1] # This looks confusing, but it's just flipping the arrays around ypts = ypts[::-ds_factor]; ypts = ypts[::-1] # so the last point is guaranteed to be included when downsampled # Add points for the rest of meander xpts.append(xpts[-1] + turn_ratio*width); ypts.append(0) xpts.append(xpts[-1]); ypts.append(-a) xpts.append(xpts[0]); ypts.append(-a) xpts.append(max(xpts)-length); ypts.append(-a) xpts.append(xpts[-1]); ypts.append(-a + width) xpts.append(xpts[0]); ypts.append(ypts[0]) xpts = np.array(xpts) ypts = np.array(ypts) #========================================================================== # Create a blank device, add the geometry, and define the ports #========================================================================== D = Device(name = 'hairpin') D.add_polygon([xpts,ypts], layer = layer) D.add_polygon([xpts,-ypts], layer = layer) xports = min(xpts) yports = -a + width/2 D.add_port(name = 1, midpoint = [xports,-yports], width = width, orientation = 180) D.add_port(name = 2, midpoint = [xports,yports], width = width, orientation = 180) return D # TODO Include parameter which specifies "half" (one edge flat) vs "full" (both edges curved) @device_lru_cache def optimal_step(start_width = 10, end_width = 22, num_pts = 50, width_tol = 1e-3, anticrowding_factor = 1.2, symmetric = False, layer = 0): # Optimal structure from https://doi.org/10.1103/PhysRevB.84.174510 # Clem, J., & Berggren, K. (2011). Geometry-dependent critical currents in # superconducting nanocircuits. Physical Review B, 84(17), 1โ27. #========================================================================== # Create the basic geometry #========================================================================== def step_points(eta, W, a): # Returns points from a unit semicircle in the w (= u + iv) plane to # the optimal curve in the zeta (= x + iy) plane which transitions # a wire from a width of 'W' to a width of 'a' # eta takes value 0 to pi W = np.complex(W) a = np.complex(a) gamma = (a*a + W*W)/(a*a - W*W) w = np.exp(1j*eta) zeta = 4*1j/pi*(W*np.arctan(np.sqrt((w-gamma)/(gamma+1))) \ + a*np.arctan(np.sqrt((gamma-1)/(w-gamma)))) x = np.real(zeta) y = np.imag(zeta) return x,y def invert_step_point(x_desired = -10, y_desired = None, W = 1, a = 2): # Finds the eta associated with the value x_desired along the optimal curve def fh(eta): guessed_x, guessed_y = step_points(eta, W = W, a = a) if y_desired is None: return (guessed_x-x_desired)**2 # The error else: return (guessed_y-y_desired)**2 try: from scipy.optimize import fminbound except: raise ImportError(""" [PHIDL] To run the optimal-curve geometry functions you need scipy, please install it with `pip install scipy` """) found_eta = fminbound(fh, x1 = 0, x2 = pi, args=()) return step_points(found_eta, W = W, a = a) if start_width > end_width: reverse = True start_width, end_width = end_width, start_width else: reverse = False if start_width == end_width: # Just return a square ypts = [0, start_width, start_width, 0] xpts = [0, 0, start_width, start_width] else: xmin,ymin = invert_step_point(y_desired = start_width*(1+width_tol), W = start_width, a = end_width) xmax,ymax = invert_step_point(y_desired = end_width*(1-width_tol), W = start_width, a = end_width) xpts = np.linspace(xmin, xmax, num_pts).tolist() ypts = [] for x in xpts: x,y = invert_step_point(x_desired = x, W = start_width, a = end_width) ypts.append(y) ypts[-1] = end_width ypts[0] = start_width if symmetric == False: xpts.append(xpts[-1]) ypts.append(0) xpts.append(xpts[0]) ypts.append(0) else: xpts += [x for x in xpts[::-1]] ypts += [-y for y in ypts[::-1]] xpts = [x/2 for x in xpts] ypts = [y/2 for y in ypts] # anticrowding_factor stretches the wire out; a stretched wire is a gentler # transition, so there's less chance of current crowding if the fabrication # isn't perfect but as a result, the wire isn't as short as it could be xpts = (np.array(xpts)*anticrowding_factor).tolist() if reverse is True: xpts = (-np.array(xpts)).tolist() start_width, end_width = end_width, start_width #========================================================================== # Create a blank device, add the geometry, and define the ports #========================================================================== D = Device(name = 'step') D.add_polygon([xpts,ypts], layer = layer) if symmetric == False: D.add_port(name = 1, midpoint = [min(xpts),start_width/2], width = start_width, orientation = 180) D.add_port(name = 2, midpoint = [max(xpts),end_width/2], width = end_width, orientation = 0) if symmetric == True: D.add_port(name = 1, midpoint = [min(xpts),0], width = start_width, orientation = 180) D.add_port(name = 2, midpoint = [max(xpts),0], width = end_width, orientation = 0) return D def optimal_90deg(width = 100.0, num_pts = 15, length_adjust = 1, layer = 0): # Optimal structure from https://doi.org/10.1103/PhysRevB.84.174510 # Clem, J., & Berggren, K. (2011). Geometry-dependent critical currents in # superconducting nanocircuits. Physical Review B, 84(17), 1โ27. D = Device() # Get points of ideal curve a = 2*width v = np.logspace(-length_adjust,length_adjust,num_pts) xi = a/2.0*((1+2/np.pi*np.arcsinh(1/v)) + 1j*(1+2/np.pi*np.arcsinh(v))) xpts = list(np.real(xi)); ypts = list(np.imag(xi)) # Add points for the rest of curve d = 2*xpts[0] # Farthest point out * 2, rounded to nearest 100 xpts.append(width); ypts.append(d) xpts.append(0); ypts.append(d) xpts.append(0); ypts.append(0) xpts.append(d); ypts.append(0) xpts.append(d); ypts.append(width) xpts.append(xpts[0]); ypts.append(ypts[0]) D.add_polygon([xpts, ypts], layer = layer) D.add_port(name = 1, midpoint = [a/4,d], width = a/2, orientation = 90) D.add_port(name = 2, midpoint = [d,a/4], width = a/2, orientation = 0) return D #============================================================================== # Example code #============================================================================== #hairpin = optimal_hairpin(width = 1, pitch = 3, length = 30, num_pts = 20) #quickplot(hairpin) #step = optimal_step(start_width = 5, end_width = 1, num_pts = 80, width_tol = 1e-3) #quickplot(step) #turn = optimal_90deg(width = 90, length_adjust = 1) #quickplot(turn) #============================================================================== # # Superconducting devices # #============================================================================== @device_lru_cache def snspd(wire_width = 0.2, wire_pitch = 0.6, size = (10,8), num_squares = None, turn_ratio = 4, terminals_same_side = False, layer = 0): # Convenience tests to auto-shape the size based # on the number of squares if num_squares is not None and ((size is None) or ((size[0] is None) and (size[1]) is None)): xy = np.sqrt(num_squares*wire_pitch*wire_width) size = [xy,xy] num_squares = None if ([size[0], size[1], num_squares].count(None) != 1): raise ValueError('[PHIDL] snspd() requires that exactly ONE value of' + ' the arguments ``num_squares`` and ``size`` be None'+ ' to prevent overconstraining, for example:\n' + '>>> snspd(size = (3, None), num_squares = 2000)') if size[0] is None: ysize = size[1] xsize = num_squares*wire_pitch*wire_width/ysize elif size[1] is None: xsize = size[0] ysize = num_squares*wire_pitch*wire_width/xsize else: xsize = size[0] ysize = size[1] num_meanders = int(np.ceil(ysize/wire_pitch)) D = Device(name = 'snspd') hairpin = optimal_hairpin(width = wire_width, pitch = wire_pitch, turn_ratio = turn_ratio, length = xsize/2, num_pts = 20, layer = layer) if (terminals_same_side is False) and ((num_meanders % 2) == 0): num_meanders += 1 elif (terminals_same_side is True) and ((num_meanders % 2) == 1): num_meanders += 1 start_nw = D.add_ref(compass(size = [xsize/2 ,wire_width], layer = layer)) hp_prev = D.add_ref(hairpin) hp_prev.connect(1, start_nw.ports['E']) alternate = True for n in range(2,num_meanders): hp = D.add_ref(hairpin) if alternate: hp.connect(2, hp_prev.ports[2]) last_port = hp.ports[1] else: hp.connect(1, hp_prev.ports[1]) last_port = hp.ports[2] hp_prev = hp alternate = not alternate finish_se = D.add_ref(compass(size = [xsize/2 ,wire_width], layer = layer)) finish_se.connect('E', last_port) D.add_port(port = start_nw.ports['W'], name = 1) D.add_port(port = finish_se.ports['W'], name = 2) D.info['num_squares'] = num_meanders*(xsize/wire_width) D.info['area'] = xsize*ysize D.info['size'] = (xsize, ysize) return D def snspd_expanded(wire_width = 0.2, wire_pitch = 0.6, size = (10,8), num_squares = None, connector_width = 1, connector_symmetric = False, turn_ratio = 4, terminals_same_side = False, layer = 0): """ Creates an optimally-rounded SNSPD with wires coming out of it that expand""" D = Device('snspd_expanded') S = snspd(wire_width = wire_width, wire_pitch = wire_pitch, size = size, num_squares = num_squares, turn_ratio = turn_ratio, terminals_same_side = terminals_same_side, layer = layer) s = D.add_ref(S) step_device = optimal_step(start_width = wire_width, end_width = connector_width, num_pts = 100, anticrowding_factor = 2, width_tol = 1e-3, symmetric = connector_symmetric, layer = layer) step1 = D.add_ref(step_device) step2 = D.add_ref(step_device) step1.connect(port = 1, destination = s.ports[1]) step2.connect(port = 1, destination = s.ports[2]) D.add_port(name = 1, port = step1.ports[2]) D.add_port(name = 2, port = step2.ports[2]) D.info = S.info S.info = {} return D #============================================================================== # Example code #============================================================================== #s = snspd(wire_width = 0.2, wire_pitch = 0.6, size = [10,3], terminals_same_side = True) #quickplot(s) #s = snspd(wire_width = 0.2, wire_pitch = 0.6, size = [10, None], # num_squares = 1000, terminals_same_side = True) #quickplot(s) def ytron_round(rho = 1, arm_lengths = (500,300), source_length = 500, arm_widths = (200, 200), theta = 2.5, theta_resolution = 10, layer = 0): #========================================================================== # Create the basic geometry #========================================================================== theta = theta*pi/180 theta_resolution = theta_resolution*pi/180 thetalist = np.linspace(-(pi-theta),-theta, int((pi-2*theta)/theta_resolution) + 2) semicircle_x = rho*cos(thetalist) semicircle_y = rho*sin(thetalist)+rho # Rest of yTron xc = rho*cos(theta) yc = rho*sin(theta) arm_x_left = arm_lengths[0]*sin(theta) arm_y_left = arm_lengths[0]*cos(theta) arm_x_right = arm_lengths[1]*sin(theta) arm_y_right = arm_lengths[1]*cos(theta) # Write out x and y coords for yTron polygon xpts = semicircle_x.tolist() + [xc+arm_x_right, xc+arm_x_right+arm_widths[1], xc+arm_widths[1], \ xc+arm_widths[1], 0, -(xc+arm_widths[0]), -(xc+arm_widths[0]), -(xc+arm_x_left+arm_widths[0]), -(xc+arm_x_left)] ypts = semicircle_y.tolist() + [yc+arm_y_right, yc+arm_y_right, yc, yc-source_length, yc-source_length, \ yc-source_length, yc, yc+arm_y_left, yc+arm_y_left] #========================================================================== # Create a blank device, add the geometry, and define the ports #========================================================================== D = Device(name = 'ytron') D.add_polygon([xpts,ypts], layer = layer) D.add_port(name = 'left', midpoint = [-(xc+arm_x_left+arm_widths[0]/2), yc+arm_y_left], width = arm_widths[0], orientation = 90) D.add_port(name = 'right', midpoint = [xc+arm_x_right+arm_widths[1]/2, yc+arm_y_right], width = arm_widths[1], orientation = 90) D.add_port(name = 'source', midpoint = [0+(arm_widths[1]-arm_widths[0])/2, -source_length+yc], width = arm_widths[0] + arm_widths[1] + 2*xc, orientation = -90) #========================================================================== # Record any parameters you may want to access later #========================================================================== D.info['rho'] = rho D.info['left_width'] = arm_widths[0] D.info['right_width'] = arm_widths[1] D.info['source_width'] = arm_widths[0] + arm_widths[1] + 2*xc return D #============================================================================== # Example code #============================================================================== #y = ytron_round(rho = 1, arm_lengths = (500,300), source_length = 500, # arm_widths = (200, 200), theta = 2.5, theta_resolution = 10, # layer = 0) #quickplot(y)