Source code for gdsCAD.core

# -*- coding: utf-8 -*-
"""
The primary geometry elements, layout and organization classes.

The objects found here are intended to correspond directly to elements found in
the GDSII specification.

The fundamental gdsCAD object is the Layout, which contains all the information
to be sent to the mask shop. A Layout can contain multiple Cells which in turn
contain references to other Cells, or contain drawing geometry.

:class:`Layout`
    The container holding all design data (GDSII: LIBRARY)
:class:`Cell`
    A collection of drawing elements, and/or references to other Cells
    (GDSII: STRUCTURE)
**Primitive Elements:**
    These can all be added to a cell. Only :class:`Boundary` and :class:`Path`
    are drawing elements.
    
    * :class:`Boundary`: A filled polygon (GDSII: BOUNDARY)    
    * :class:`Path`: An unfilled polygonal line (GDSII: PATH)
    * :class:`Text`: Non-printing labelling text (GDSII: TEXT)
    * :class:`CellReference`: A simple reference to another Cell (GDSII: SREF)
    * :class:`CellArray`: A reference to another cell to be copied multiple
      times (GDSII: AREF)    
    * :class:`Elements`: A listlike collection of Boundary or Path drawing elements
      (no GDSII equivalent)


.. note::
    Copyright 2009-2012 Lucas Heitzmann Gabrielli
    
    Copyright 2013 Andrew G. Mark

    gdsCAD (based on gdspy) is released under the terms of the GNU GPL
    
"""


import struct
import numbers
import datetime
import warnings
import numpy as np
import copy
import pdb
import string
import os.path

try:
    import matplotlib.pyplot as plt
    import matplotlib.patches
    import matplotlib.text
    import matplotlib.lines
    import matplotlib.transforms as transforms
    import matplotlib.cm
    import shapely.geometry
    import descartes
except ImportError, err:
    warnings.warn(str(err) + '. It will not be possible to display designs.')

try:
    import dxfgrabber
except ImportError, err:
    warnings.warn(str(err) + '. It will not be possible to import DXF artwork.')

default_layer = 1
default_datatype = 0

def _show(self):
    """
    Display the object
    
    :returns: the display Axes.
    
    """
    ax = plt.gca()
    ax.set_aspect('equal')
    ax.margins(0.1)    
    
    artists=self.artist()
    for a in artists:
        a.set_transform(a.get_transform() + ax.transData)
        if isinstance(a, matplotlib.patches.Patch):
            ax.add_patch(a)
        elif isinstance(a, matplotlib.lines.Line2D):
            ax.add_line(a)
        else:
            ax.add_artist(a)
    
    ax.autoscale(True)
    plt.show()
    
    return ax

[docs]class ElementBase(object): """ Base class for geometric elements. Other drawing elements derive from this. """ @staticmethod def _layer_properties(layer): # Default colors from previous versions colors = ['k', 'r', 'g', 'b', 'c', 'm', 'y'] colors += matplotlib.cm.gist_ncar(np.linspace(0.98, 0, 15)) color = colors[layer % len(colors)] return {'color': color} def __init__(self, points, dtype=np.float32): self._points = np.array(points, dtype=dtype) self._bbox = None @property
[docs] def points(self): return self._points
[docs] def copy(self, suffix=None): """ Make a copy of this element. :param suffix: Ignored """ return copy.deepcopy(self)
[docs] def translate(self, displacement): """ Translate this object. :param displacment: The vector by which to displace this object. :returns: self The transformation acts in place. """ self._points += np.array(displacement) self._bbox = None return self
[docs] def rotate(self, angle, center=(0, 0)): """ Rotate this object. :param angle: The angle of rotation (in deg). :param center: Center point for the rotation. :returns: self The optional center point can be specified by a 2D vector or the string 'com' for center of mass. The transformation acts in place. """ ang = angle * np.pi/180 m=np.array([[np.cos(ang), -np.sin(ang)], [np.sin(ang), np.cos(ang)]]) if isinstance(center, str) and center.lower()=='com': center=self.points.mean(0) else: center=np.array(center) self._points = m.dot((self.points-center).T).T+center self._bbox = None return self
[docs] def reflect(self, axis, origin=(0,0)): """ Reflect this object in the x or y axis :param axis: 'x' or 'y' indicating which axis in which to make the refln :param origin: A point which will remain invariant on reflection :returns: self Optional origin can be a 2D vector or 'COM' indicating that scaling should be made about the pts centre of mass. The transformation acts in place. """ if axis=='x': self.scale([1,-1], origin) elif axis=='y': self.scale([-1,1], origin) else: raise ValueError('Unknown axis %s'%str(axis)) return self
[docs] def scale(self, k, origin=(0,0)): """ Scale this object by the factor k :param k: the value by which to scale the object :param origin: the point about which to make the scaling :returns: self The factor k can be a scalar or 2D vector allowing non-uniform scaling. Optional origin can be a 2D vector or 'COM' indicating that scaling should be made about the pts centre of mass. The transformation acts in place. """ if isinstance(origin, str) and origin.lower()=='com': origin=self.points.mean(0) else: origin=np.array(origin) k=np.array(k) self._points=(self.points-origin)*k+origin self._bbox = None return self
@property
[docs] def bounding_box(self): """ Return the bounding box containing the polygon """ if self._bbox is not None: return self._bbox.copy() bb = np.zeros([2,2]) bb[0,0] = self._points[:,0].min() bb[0,1] = self._points[:,1].min() bb[1,0] = self._points[:,0].max() bb[1,1] = self._points[:,1].max() self._bbox = bb return bb
[docs]class Boundary(ElementBase): """ A filled, closed polygonal geometric object. :param points: Coordinates of the vertices of the polygon. :param layer: The GDSII layer number for this element Defaults to core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). :param verbose: If False, warnings about the number of vertices of the polygon will be suppressed. .. note:: This is a direct equivalent to the Boundary element found in the GDSII specification. The last point should not be equal to the first (polygons are automatically closed). The official GDSII specification supports only a maximum of 199 vertices per polygon. Examples:: triangle_pts = [(0, 40), (15, 40), (10, 50)] triangle = gdsCAD.core.Boundary(triangle_pts) myCell.add(triangle) """ show=_show def __init__(self, points, layer=None, datatype=None, verbose=False, dtype=np.float32) : points = np.asarray(points, dtype=dtype) if (points[0] != points[-1]).any(): points = np.concatenate((points, [points[0]])) ElementBase.__init__(self, points) if verbose and 8191 >= self.points.shape[0] > 199: warnings.warn("[GDSPY] A polygon with more than 199 points was created " "(not officially supported by the GDSII format).", stacklevel=2) if verbose and self.points.shape[0] > 8191: warnings.warn("[GDSPY] A polygon with more than 8191 points was created." "Multiple XY required which is an unofficial GDSII extension.", stacklevel=2) if layer is None: self.layer = default_layer else: self.layer = layer if datatype is None: self.datatype = default_datatype else: self.datatype = datatype def __str__(self): return "Boundary ({} vertices, layer {}, datatype {})".format(len(self.points), self.layer, self.datatype)
[docs] def area(self): """ Calculates the area of the element. Assumes that the Boundary respects the GDSII requirement that the path be simple and closed. """ # shoestring method for area of an irregular polygon first, second = self._points[:-1], self._points[1:] area = first[:,0]*second[:,1] - second[:,0]*first[:,1] return abs(area.sum())/2.0
[docs] def to_gds(self, multiplier): """ Convert this object to a GDSII element. :param multiplier: A number that multiplies all dimensions written in the GDSII element. :returns: The GDSII binary string that represents this object. """ gds_coordinates = np.array(np.round(self._points * multiplier), dtype='>i4') nr_points = gds_coordinates.shape[0] export_pos = 0 data = struct.pack('>' + 4 *'HH', 4, 0x0800, 6, 0x0D02, self.layer, 6, 0x0E02, self.datatype) # Export coordinates, if there are more than 8191 points split it into several XY entries # This is an unofficial but very common extension of the GDSII protocol. while export_pos < gds_coordinates.shape[0]: entry_points = min(8191, nr_points - export_pos) data_size = 4 + 8 * entry_points data += struct.pack('>HH', data_size, 0x1003) data += gds_coordinates[export_pos:export_pos+entry_points].tostring() export_pos += entry_points data += struct.pack('>HH', 4, 0x1100) return data
[docs] def to_path(self, width=1.0, pathtype=0): """ Convert this Boundary to a Path :param width: The width of the line :param pathtype: The endpoint style """ return Path(self.points, width=width, layer=self.layer, datatype=self.datatype, pathtype=pathtype, verbose=False, dtype=self.points.dtype)
[docs] def artist(self): """ Return a list of matplotlib artists to draw this object """ return [matplotlib.patches.Polygon(self.points, closed=True, lw=0, **self._layer_properties(self.layer))]
[docs]class Path(ElementBase): """ An unfilled, unclosed polygonal line of fixed width. :param points: Coordinates of the vertices of the polygon. :param width: The width of the line :param layer: The GDSII layer number for this element. Defaults to core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). :param pathtype: The endpoint style .. note:: This is a direct equivalent to the Path element found in the GDSII specification. Paths are not automatically closed. The official GDSII specification supports only a maximum of 199 vertices per path. The style of endcaps is specificed by pathtype: ==== ================================= ==== ================================= 0 Square ended paths 1 Round ended 2 Square ended, extended 1/2 width 4 Variable length extensions ==== ================================= Examples:: arrow_pts = [(0, 40), (15, 40), (10, 50)] arrow = gdsCAD.core.Path(arrow_pts) myCell.add(arrow) """ show=_show def __init__(self, points, width=1.0, layer=None, datatype=None, pathtype=0, verbose=False, dtype=np.float32): ElementBase.__init__(self, points, dtype=dtype) if verbose and self.points.shape[0] > 199: warnings.warn("[GDSPY] A Path with more than 199 points was created " "(not officially supported by the GDSII format).", stacklevel=2) if self.points.shape[0] > 8191: raise ValueError('Paths with more than 8191 not supported by GDSII') self.width=width self.pathtype=pathtype if layer is None: self.layer = default_layer else: self.layer = layer if datatype is None: self.datatype = default_datatype else: self.datatype = datatype def __str__(self): return "Path ({} vertices, layer {}, datatype {})".format(len(self.points), self.layer, self.datatype)
[docs] def area(self): """ Calculates the approximate area of the element. This is only an estimate. It does not correctly deal with overlaps at corners. """ dr = np.sqrt(((self._points[1:] - self._points[:-1])**2).sum(1)) return dr.sum()*self.width
[docs] def to_gds(self, multiplier): """ Convert this object to a GDSII element. :param multiplier : A number that multiplies all dimensions written in the GDSII element. :returns: The GDSII binary string that represents this object. """ gds_coordinates = np.array(np.round(self._points * multiplier), dtype='>i4') data = struct.pack('>12H', 4, 0x0900, 6, 0x0D02, self.layer, 6, 0x0E02, self.datatype, 6, 0x2102, self.pathtype, 8) data += struct.pack('>HL2H', 0x0F03, int(round(self.width * multiplier)), 4 + 8 * gds_coordinates.shape[0], 0x1003) data += gds_coordinates.tostring() return data + struct.pack('>2H', 4, 0x1100)
[docs] def to_boundary(self): """ Convert this Path to a Boundary. Open paths will be closed as boundaries. """ return Boundary(self.points, layer=self.layer, datatype=self.datatype, verbose=False, dtype=self.points.dtype)
[docs] def artist(self, color=None): """ Return a list of matplotlib artists to draw this object .. Warning:: Path endpoints are not rendered correctly. They always display as half-circles. Paths are rendered by first converting them to a shapely polygon and then converting this to a descartes polgyonpatch. This generates a path whose line width scales with the drawing size. Aside from being convoluted it means that path ends always render as half-circles. """ points=[tuple(p) for p in self.points] lines = shapely.geometry.LineString(points) poly = lines.buffer(self.width/2.) return [descartes.PolygonPatch(poly, lw=0, **self._layer_properties(self.layer))]
[docs]class Text(ElementBase): """ A non-printing text label :param text: The text of this label. :param position: Text anchor position. :param anchor: Position of the anchor relative to the text. :param rotation: Angle of rotation of the label (in *degrees*). :param magnification: Magnification factor for the label. :param layer: The GDSII layer number for this element. Defaults to core.default_layer. :param datatype: The GDSII text type for the label (between 0 and 63). .. note:: This is a direct equivalent to the Text element found in the GDSII specification. Text that can be used to label parts of the geometry or display messages. The text does not create additional geometry, it's meant for display and labeling purposes only. Examples:: txt = gdspy.Text('Sample label', (10, 0), 'sw') myCell.add(txt) """ _anchor = {'nw':0, 'top left':0, 'upper left':0, 'n':1, 'top center':1, 'upper center':1, 'ne':2, 'top right':2, 'upper right':2, 'w':4, 'middle left':4, 'o':5, 'middle center':5, 'e':6, 'middle right':6, 'sw':8, 'bottom left':8, 'lower left':8, 's':9, 'bottom center':9, 'lower center':9, 'se':10, 'bottom right':10, 'lower right':10} show = _show def __init__(self, text, position, anchor='o', rotation=None, magnification=None, layer=None, datatype=None, x_reflection=None, dtype=np.float32): ElementBase.__init__(self, position, dtype=dtype) self.text = text self.anchor = Text._anchor[anchor.lower()] self.rotation = rotation self.x_reflection = x_reflection self.magnification = magnification if layer is None: self.layer = default_layer else: self.layer = layer if datatype is None: self.datatype = default_datatype else: self.datatype = datatype def __str__(self): return "Text (\"{0}\", at ({1[0]}, {1[1]}), rotation {2}, magnification {3}, layer {4}, texttype {5})".format(self.text, self.points, self.rotation, self.magnification, self.layer, self.texttype)
[docs] def area(self): """ The area of the element. For text this is always 0, since it is non-printing. """ return 0
[docs] def to_gds(self, multiplier): """ Convert this text to a GDSII element. :param multiplier: A number that multiplies all dimensions written in the GDSII structure. :returns: The GDSII binary string that represents this label. """ text = self.text if len(text)%2 != 0: text = text + '\0' data = struct.pack('>11h', 4, 0x0C00, 6, 0x0D02, self.layer, 6, 0x1602, self.datatype, 6, 0x1701, self.anchor) if not (self.rotation is None and self.magnification is None): word = 0 values = b'' if not (self.magnification is None): word += 0x0004 values += struct.pack('>2h', 12, 0x1B05) + _eight_byte_real(self.magnification) if not (self.rotation is None): word += 0x0002 values += struct.pack('>2h', 12, 0x1C05) + _eight_byte_real(self.rotation) data += struct.pack('>2hH', 6, 0x1A01, word) + values return data + struct.pack('>2h2l2h', 12, 0x1003, int(round(self.points[0] * multiplier)), int(round(self.points[1] * multiplier)), 4 + len(text), 0x1906) + text.encode('ascii') + struct.pack('>2h', 4, 0x1100)
[docs] def rotate(self, angle, center=(0, 0)): """ Rotate this object. :param angle: The angle of rotation (in deg). :param center: Center point for the rotation. :returns: self The transformation acts in place. """ if self.rotation is None: self.rotation = angle else: self.rotation += angle ElementBase.rotate(self, angle, center) return self
[docs] def reflect(self, axis, origin=(0,0)): """ Reflect this object in the x or y axis :param axis: 'x' or 'y' indcating which axis in which to make the refln :param origin: A point which will remain invariant on reflection :returns: self Optional origin can be a 2D vector or 'COM' indicating that scaling should be made about the pts centre of mass. The transformation acts in place. """ if self.x_reflection is None: self.x_reflection = True else: self.x_reflection ^= True ElementBase.reflect(self, axis, origin) if axis=='y': self.rotate(180, origin) return self
@property
[docs] def bounding_box(self): """ Return the bounding box containing the Text It's not really clear how this should work, but for the moment it only returns the point of insertion """ bb = np.array((self.points, self.points)) return bb
[docs] def artist(self): """ Return a list of matplotlib artists for drawing this object .. warning:: Does not properly handle rotations or scaling """ return [matplotlib.text.Text(self.points[0], self.points[1], self.text, **self._layer_properties(self.layer))]
[docs]class Elements(object): """ A list-like collection of Boundary and/or Path objects. :param obj : List containing the coordinates of the vertices of each polygon. Or a list of already defined elements. :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). :param obj_type: Specify whether to interpret the list of point arrays as boundaries, or paths The class :class:`Elements` is intended to simplify geometric transformations on several drawing elements at once. There is no GDSII equivalent. Elements is not a substitute for :class:`Cell`. In particular, multiple Elements added to a design cannot be added by reference. Each instance will be seperately written to the file. There are many different ways of initializing an Elements list. The simplest is to call it with no parameters i.e. Elements() and then add elements. One list of elements can be added to another. The individual objects in the first Elements list will be added to the second so that the list is flat. All elements in the list share the same layer and datatype. Changing the layer or datatype for the Elements list changes it for all contained elements Elements can be indexed using simple indexing:: print elist[1] Elements can be used as an iterator:: for el in elist: print el Examples:: square_pts=[[0,0, [1,0], [1,1], [0,1]]] triangle_pts=[[1,0], [2,0], [2,2]] square=Polygon(square_pts) triangle=Path(triangle_pts, width=0.5) # Create an empty list and fill it later elist=Elements() elist.add(square) elist.add(triangle) # Create a filled square and an unfilled triangle elist=Elements([square, triangle]) # Create two filled boundaries from a list of points elist=Elements([square_pts, triangle_pts]) # Create two unfilled paths from a list of points elist=Elements([square_pts, triangle_pts], obj_type='path', width=0.5) # Create a filled square and an unfilled triangle elist=Elements([square_pts, triangle_pts], obj_type=['boundary', 'path']) """ show = _show def __init__(self, obj=None, layer=None, datatype=None, obj_type=None, **kwargs): self.obj = [] # No parameters => Create an empty Elements list if (layer is None) and (obj is None): return #Empty list # A list of elements => Create an identical list if isinstance(obj[0], ElementBase): self._check_obj_list(obj) self.obj=list(obj) layer = obj[0].layer datatype = obj[0].datatype # Expecting list of point sequences else: # Use the pts to define Boundaries if obj_type is None: obj_type=['boundary']*len(obj) # Use the points to pts to create the obj_type defined elif isinstance(obj_type, str): obj_type=[obj_type]*len(obj) elif len(obj_type) != len(obj): raise ValueError('Length of obj_type list must match that of obj list') for p, t in zip(obj, obj_type): if t.lower() == 'boundary': self.obj.append(Boundary(p, layer, datatype, **kwargs)) elif t.lower() == 'path': self.obj.append(Path(p, layer=layer, datatype=datatype, **kwargs)) if layer is None: self.layer = default_layer else: self.layer = layer if datatype is None: self.datatype = default_datatype else: self.datatype = datatype def _check_obj_list(self, obj_list): for o in obj_list: if not isinstance(o, (ElementBase)): raise TypeError('Object list must contain only Boundaries or Paths') @property def layer(self): """ Get the layer """ return self._layer @layer.setter
[docs] def layer(self, val): """ Set the layer """ self._layer=val for p in self: p.layer=val
@property def datatype(self): """ Get the datatype """ return self._datatype @datatype.setter
[docs] def datatype(self, val): """ Set the datatype """ self._datatype=val for p in self: p.datatype=val
[docs] def copy(self, suffix=None): """ Make a copy of the object and all contained elements """ return copy.deepcopy(self)
def __str__(self): return "Elements layer={}, datatype={} ({} polygons, {} vertices)".format(self.layer, self.datatype, len(self.polygons), sum([len(p.points) for p in self.polygons]))
[docs] def add(self, obj): """ Add a new element or list of elements to this list. :param element: The element to be inserted in this list. """ if isinstance(obj, Elements): self._check_obj_list(obj) self.obj.extend(obj) return if not isinstance(obj, ElementBase): raise ValueError('Can only add a drawing element to Elements') self.obj.append(obj)
def __len__(self): """ Return the number of elements in the list """ return len(self.obj) def __getitem__(self, index): """ Get the element at index """ return self.obj[index] def __setitem__(self, index, value): """ Set a new element at index """ self.obj[index]=value def __iter__(self): """ Iterate over elements in list """ return iter(self.obj)
[docs] def translate(self, displacement): """ Translate this object. :param displacement: The vector by which to displace all the elements. :returns: self The transformation acts in place. """ displacement=np.array(displacement) for p in self: p.translate(displacement) return self
[docs] def rotate(self, angle, center=(0, 0)): """ Rotate this object. :param angle: The angle of rotation (in deg). :param center: Center point for the rotation. :returns: self The transformation acts in place. """ for p in self: p.rotate(angle, center) return self
[docs] def reflect(self, axis, origin=(0,0)): """ Reflect this object in the x or y axis :param axis: 'x' or 'y' indcating which axis in which to make the refln :param origin: A point which will remain invariant on reflection :returns: self Optional origin can be a 2D vector or 'COM' indicating that scaling should be made about the pts centre of mass. The transformation acts in place. """ for p in self: p.reflect(axis, origin) return self
[docs] def scale(self, k, origin=(0,0)): """ Scale this object by the factor k :param k: the value by which to scale the object :param origin: the point about which to make the scaling :returns: self The factor k can be a scalar or 2D vector allowing non-uniform scaling. Optional origin can be a 2D vector or 'COM' indicating that scaling should be made about the pts centre of mass. The transformation acts in place. """ for p in self: p.scale(k, origin) return self
[docs] def area(self): """ Calculate the area of the elements. """ area = 0 for e in self: area += e.area() return area
[docs] def to_gds(self, multiplier): """ Convert this object to a series of GDSII elements. :param multiplier: A number that multiplies all dimensions written in the GDSII elements. :returns: The GDSII binary string that represents this object. """ data = b'' for p in self: data += p.to_gds(multiplier) return data
@property
[docs] def bounding_box(self): """ Return the bounding box containing all Elements """ subboxes=[] for p in self: subboxes.append(p.bounding_box) subboxes=np.array(subboxes) bb = np.array([[min(subboxes[:, 0,0]), min(subboxes[:, 0,1])], [max(subboxes[:, 1,0]), max(subboxes[:, 1,1])]]) return bb
[docs] def artist(self, color=None): """ Return a list of matplotlib artists for drawing this object """ art=[] for p in self: art+=p.artist() return art
[docs]class Layout(dict): """ A layout object :param name: Name of the GDSII library. :param unit: Unit size for the objects in the library (in *meters*). :param precision: Precision for the dimensions of the objects in the library (in *meters*). A layout is a dict based collection of Cells. Cells can be accessed by their name:: l=gdsCAD.core.Layout('layout') l.add(top_cell) print l[top_cell.name] The dimensions actually written on the GDSII file will be the dimensions of the objects created times the ratio ``unit/precision``. For example, if a circle with radius 1.5 is created and we set ``unit=1.0e-6`` (1 um) and ``precision=1.0e-9`` (1 nm), the radius of the circle will be 1.5 um and the GDSII file will contain the dimension 1500 nm. .. note:: This is a direct equivalent to the Library element found in the GDSII specification. """ show=_show def __init__(self, name='library', unit=1e-6, precision=1.e-9): dict.__init__(self) self.name=name self.unit=unit self.precision=precision
[docs] def add(self, cell): """ Add a new cell to this layout. :param element: The Cell to be inserted in this Layout. """ names=[c.name for c in self.get_dependencies()] if cell.name in names: warnings.warn("A cell named {0} is already in this library.".format(cell.name)) self[cell.name]=cell
[docs] def get_dependencies(self): """ Returns a list of all cells included in this layout. Subcells are checked recursively :param out: List of the cells referenced by this cell. """ dependencies = set(self.values()) for cell in self.values(): dependencies |= set(cell.get_dependencies()) return list(dependencies)
[docs] def copy(self): """ Creates a deep copy of this Layout. :returns: The new copy of this layout. This makes a deep copy, all elements are recursively duplicated """ return copy.deepcopy(self)
[docs] def save(self, outfile): """ Output a list of cells as a GDSII stream library. Cell names are checked for uniqueness. If there are duplicate cell names then a unique ID is appended to the cell name to force uniqueness. :param outfile: The file (or path) where the GDSII stream will be written. It must be opened for writing operations in binary format. """ close_source = False if not hasattr(outfile, "write"): outfile = os.path.expanduser(outfile) outfile = open(outfile, "wb") close_source = True cells=self.get_dependencies() cell_names = [x.name for x in cells] duplicates = set([x for x in cell_names if cell_names.count(x) > 1]) if duplicates: print 'Duplicate cell names that will be made unique:', ', '.join(duplicates) print 'Writing the following cells' for cell in cells: if cell.name not in duplicates: print cell.name+':',cell else: print cell.unique_name+':',cell longlist=[name for name in sorted(cell_names) if len(name)>32] if longlist: print '%d of the cells have names which are longer than the official GDSII limit of 32 character' % len(longlist) print '---------------' for n in longlist: print n, ' : %d chars'%len(n) now = datetime.datetime.today() if len(self.name)%2 != 0: name = self.name + '\0' else: name = self.name outfile.write(struct.pack('>19h', 6, 0x0002, 0x0258, 28, 0x0102, now.year, now.month, now.day, now.hour, now.minute, now.second, now.year, now.month, now.day, now.hour, now.minute, now.second, 4+len(name), 0x0206) + name.encode('ascii') + struct.pack('>2h', 20, 0x0305) + _eight_byte_real(self.precision / self.unit) + _eight_byte_real(self.precision)) for cell in cells: outfile.write(cell.to_gds(self.unit / self.precision, duplicates)) outfile.write(struct.pack('>2h', 4, 0x0400)) if close_source: outfile.close()
[docs] def top_level(self): """ Output the top level cells from the GDSII layout. Top level cells are those that are not referenced by any other cells. :returns: List of top level cells. """ top = self.values() for cell in self.values(): for dependency in cell.get_dependencies(): if dependency in top: top.remove(dependency) return top
@property
[docs] def bounding_box(self): """ Returns the bounding box for this layout. :returns: Bounding box of this cell [[x_min, y_min], [x_max, y_max]], or ``None`` if the cell is empty. """ top=self.top_level() boxes=[e.bounding_box for e in top] boxes=np.array([b for b in boxes if b is not None]) #discard empty cells return np.array([[min(boxes[:,0,0]), min(boxes[:,0,1])], [max(boxes[:,1,0]), max(boxes[:,1,1])]])
[docs] def artist(self): """ Return a list of matplotlib artists for drawing this object Returns artists for every top level cell in this layout """ top=self.top_level() artists=[] for c in top: artists += c.artist() return artists
[docs]class Cell(object): """ Collection of elements, both geometric objects and references to other cells. :param name: The name of the cell. """ show=_show def __init__(self, name): self.name = str(name) self._objects = [] self._references = [] @property
[docs] def elements(self): return self.objects + self.references
@property
[docs] def objects(self): """ Get all elements excluding any references. """ return tuple(self._objects)
@property
[docs] def references(self): """ Get all references in this cell. """ return tuple(self._references)
def __str__(self): return "Cell (\"{}\", {} elements, {} references)".format(self.name, len(self.objects), len(self.references)) def __getitem__(self, index): """ Get the element at index """ return self.elements[index] def __setitem__(self, index, value): """ Set a new element at index """ self.elements[index]=value def __iter__(self): """ Iterate over elements in list """ return iter(self.elements) def __len__(self): return len(self.elements) @property
[docs] def unique_name(self): return self.name + '_' + _compact_id(self)
[docs] def to_gds(self, multiplier, duplicates=[]): """ Convert this cell to a GDSII structure. :param multiplier: A number that multiplies all dimensions written in the GDSII structure. :param uniquify: If True saves the cell reference according to its uniquified name. :returns: The GDSII binary string that represents this cell. """ now = datetime.datetime.today() name = self.unique_name if self.name in duplicates else self.name if len(name)%2 != 0: name = name + '\0' data = struct.pack('>16h', 28, 0x0502, now.year, now.month, now.day, now.hour, now.minute, now.second, now.year, now.month, now.day, now.hour, now.minute, now.second, 4 + len(name), 0x0606) + name.encode('ascii') for element in self: if isinstance(element, ReferenceBase): data += element.to_gds(multiplier, duplicates) else: data += element.to_gds(multiplier) return data + struct.pack('>2h', 4, 0x0700)
[docs] def copy(self, name=None, suffix=None): """ Creates a deepcopy of this cell. This makes a deep copy, all elements are recursively duplicated :param name: The name of the new cell. :param suffix: A suffix to add to the end of the name of every subcell :returns: The new copy of this cell. """ new_cell=copy.deepcopy(self) if name is None: if suffix is not None: new_cell.name+=suffix else: new_cell.name = name if suffix is not None: deps=new_cell.get_dependencies(include_elements=True) for cell in [e for e in deps if isinstance(e, Cell)]: cell.name += suffix return new_cell
[docs] def add(self, element, *args, **kwargs): """ Add a new element or list of elements to this cell. :param element: The element or list of elements to be inserted in this cell. A :class:`Cell` are added by implicitly creating a :class:`CellReference`, they can be accompanied by all the arguments available when explicity using :class:`CellReference`. To add a Cell as an array it is necessary to first create the :class:`CellArray` and then add that. """ if isinstance(element, Cell): self._references.append(CellReference(element, *args, **kwargs)) elif isinstance(element, (ElementBase, Elements, ReferenceBase)): if len(args)!=0 or len(kwargs)!=0: raise TypeError('Cannot have extra arguments when adding elements') if isinstance(element, ReferenceBase): self._references.append(element) else: self._objects.append(element) elif isinstance(element, (tuple, list)): for e in element: self.add(e, **kwargs) else: raise TypeError('Cannot add type %s to cell.' % type(element)) self.bb_is_valid = False
[docs] def area(self, by_layer=False): """ Calculate the total area of the elements on this cell, including cell references and arrays. :param by_layer: If ``True``, the return value is a dictionary with the areas of each individual layer. :returns: Area of this cell. """ if by_layer: cell_area = {} for element in self.elements: element_area = element.area(True) for ll in element_area.iterkeys(): if cell_area.has_key(ll): cell_area[ll] += element_area[ll] else: cell_area[ll] = element_area[ll] else: cell_area = 0 for element in self.elements: cell_area += element.area() return cell_area
[docs] def prune(self): """ Remove any subcells that contain no elements. :returns: True if the cell and all of its subcells contain no elements """ blacklist=[] for c in self.references: val=c.ref_cell.prune() if val: blacklist += [c] self._references=[e for e in self.references if e not in blacklist] return False if len(self) else True
[docs] def get_layers(self): """ Returns a list of layers in this cell. :returns: List of the layers used in this cell. """ layers = set() for element in self.elements: if isinstance(element, (ElementBase, Elements)): layers.add(element.layer) elif isinstance(element, ReferenceBase): layers |= set(element.ref_cell.get_layers()) return list(layers)
@property
[docs] def bounding_box(self): """ Returns the bounding box for this cell. :returns: Bounding box of this cell [[x_min, y_min], [x_max, y_max]], or ``None`` if the cell is empty. """ if len(self) == 0: return None boxes=[e.bounding_box for e in self] boxes=np.array([b for b in boxes if b is not None]) return np.array([[min(boxes[:,0,0]), min(boxes[:,0,1])], [max(boxes[:,1,0]), max(boxes[:,1,1])]])
[docs] def get_dependencies(self, include_elements=False): """ Returns a list of all cells included as references by this cell. Subcells are checked recursively. :param include_elements: If true returns a complete list of all elements in the heirarchy :returns: List of the cells referenced by this cell. """ dependencies = [] for reference in self.references: dependencies += reference.get_dependencies(include_elements) if include_elements: dependencies += self.elements return dependencies
[docs] def artist(self): """ Return a list of matplotlib artists for drawing this object """ art=[] for e in self: art+=e.artist() return art
[docs] def flatten(self): """ Returns a list of copies of the elements of this cell with References converted to Paths and Boundaries. :returns: A flattened list of copies of this cell's contents. A flattened version of this cell can be reconstructed with:: flat_cell = Cell('FLAT') flat_cell.add(deep_cell.flatten()) """ obj_list = [] # Add all drawing elements for obj in self.objects: if isinstance(obj, Elements): obj_list.extend(obj.copy().obj) else: obj_list.append(obj.copy()) # Add references for ref in self.references: obj_list.extend(ref.flatten()) return obj_list
[docs]class ReferenceBase: """ Base class for cell references """ def __init__(self): pass def __len__(self): return len(self.ref_cell)
[docs] def copy(self, suffix=None): return copy.copy(self)
[docs] def translate(self, displacement): """ Translate this object by displacement :param displacement: the vector by which to move the cell :returns: self """ self.origin+=np.array(displacement) return self
[docs] def rotate(self, angle): """ Rotate this object by angle :param angle: the angle by which to rotate the cell :returns: self """ if self.rotation is None: self.rotation = 0 self.rotation += angle if self.rotation == 0: self.rotation=None return self
[docs] def scale(self, k): """ Scale this object by factor k :param k: the factor by which to scale the cell :returns: self """ if self.magnification is None: self.magnification = 0 self.magnification *= k if self.magnification == 1.0: self.magnificiation=None return self
[docs] def get_dependencies(self, include_elements=False): return [self.ref_cell]+self.ref_cell.get_dependencies(include_elements)
[docs]class CellReference(ReferenceBase): """ Simple reference to an existing cell. :param ref_cell: The referenced cell. :param origin: Position where the reference is inserted. :param rotation: Angle of rotation of the reference (in *degrees*). :param magnification: Magnification factor for the reference. :param x_reflection: If ``True``, the reference is reflected parallel to the x direction before being rotated. .. note:: This is a direct equivalent to the SREF element found in the GDSII specification. """ def __init__(self, ref_cell, origin=(0, 0), rotation=None, magnification=None, x_reflection=False): ReferenceBase.__init__(self) self.origin = np.array(origin) self.ref_cell = ref_cell self.rotation = rotation self.magnification = magnification self.x_reflection = x_reflection #return CellReference(v, origin=self.origin, rotation=self.rotation, magnification=self.magnification, x_reflection=self.x_reflection) def __str__(self): if isinstance(self.ref_cell, Cell): name = self.ref_cell.name else: name = self.ref_cell return "CellReference (\"{0}\", at ({1[0]}, {1[1]}), rotation {2}, magnification {3}, reflection {4})".format(name, self.origin, self.rotation, self.magnification, self.x_reflection) def __repr__(self): if isinstance(self.ref_cell, Cell): name = self.ref_cell.name else: name = self.ref_cell return "CellReference(\"{0}\", ({1[0]}, {1[1]}), {2}, {3}, {4})".format(name, self.origin, self.rotation, self.magnification, self.x_reflection)
[docs] def to_gds(self, multiplier, duplicates=[]): """ Convert this object to a GDSII element. :param multiplier: A number that multiplies all dimensions written in the GDSII element. :param uniquify: If True saves the cell reference according to its uniquified name. :returns: The GDSII binary string that represents this object. """ ref_cell = self.ref_cell name = ref_cell.unique_name if ref_cell.name in duplicates else ref_cell.name if len(name)%2 != 0: name = name + '\0' data = struct.pack('>4h', 4, 0x0A00, 4 + len(name), 0x1206) + name.encode('ascii') if not (self.rotation is None) or not (self.magnification is None) or self.x_reflection: word = 0 values = b'' if self.x_reflection: word += 0x8000 if not (self.magnification is None): word += 0x0004 values += struct.pack('>2h', 12, 0x1B05) + _eight_byte_real(self.magnification) if not (self.rotation is None): word += 0x0002 values += struct.pack('>2h', 12, 0x1C05) + _eight_byte_real(self.rotation) data += struct.pack('>2hH', 6, 0x1A01, word) + values return data + struct.pack('>2h2l2h', 12, 0x1003, int(round(self.origin[0] * multiplier)), int(round(self.origin[1] * multiplier)), 4, 0x1100)
[docs] def area(self, by_layer=False): """ Calculate the total area of the referenced cell with the magnification factor included. :param by_layer: If ``True``, the return value is a dictionary with the areas of each individual layer. :returns: Area of this cell. """ if self.magnification is None: return self.ref_cell.area(by_layer) else: if by_layer: factor = self.magnification * self.magnification cell_area = self.ref_cell.area(True) for kk in cell_area.iterkeys(): cell_area[kk] *= factor return cell_area else: return self.ref_cell.area() * self.magnification * self.magnification
@property
[docs] def bounding_box(self): """ Returns the bounding box for this reference. Currently does not handle rotated references :returns: Bounding box of this cell [[x_min, y_min], [x_max, y_max]], or ``None`` if the cell is empty. """ import utils if len(self.ref_cell)==0: return None mag=self.magnification if (self.magnification is not None) else 1.0 bbox=self.ref_cell.bounding_box bbox *= mag if self.rotation: x0,y0=bbox[0] x1,y1=bbox[1] box=np.array([[x0,y0], [x0,y1], [x1, y1], [x1, y0]]) box = utils.rotate(box, self.rotation) bbox[0]=[min(box[:,0]), min(box[:,1])] bbox[1]=[max(box[:,0]), max(box[:,1])] bbox[0] += self.origin bbox[1] += self.origin return bbox
[docs] def artist(self): """ Return a list of matplotlib artists for drawing this object .. warning:: Does not yet handle x_reflections correctly """ xform=matplotlib.transforms.Affine2D() if self.magnification is not None: xform.scale(self.magnification) if self.rotation is not None: xform.rotate_deg(self.rotation) xform.translate(self.origin[0], self.origin[1]) artists=self.ref_cell.artist() for a in artists: a.set_transform(a.get_transform() + xform) return artists
[docs] def flatten(self): """ Return reference as a flattened list of elements. """ mag = 1 if self.magnification is None else self.magnification rot = 0 if self.rotation is None else self.rotation elements = self.ref_cell.flatten() for e in elements: e.scale(mag).rotate(rot).translate(self.origin) return elements
[docs]class CellArray(ReferenceBase): """ Multiple references to an existing cell in a grid arrangement. :param ref_cell: The referenced cell. :param cols: Number of columns in the array. :param rows: Number of rows in the array. :param spacing: The distance between copies within the array. This can be either a 2-tuple or a pair of 2-tuples. The former (n,m) is interpreted as ((n,0), (0,m)) :param origin: Position where the cell is inserted. :param rotation: Angle of rotation of the reference (in *degrees*). :param magnification: Magnification factor for the reference. :param x_reflection: If ``True``, the reference is reflected parallel to the x direction before being rotated. .. note:: This is a direct equivalent to the AREF element found in the GDSII specification. """ def __init__(self, ref_cell, cols, rows, spacing, origin=(0, 0), rotation=None, magnification=None, x_reflection=False): ReferenceBase.__init__(self) self.rows = int(rows) self.cols = int(cols) self.spacing = np.array(spacing) try: self.spacing[0][0] except IndexError: self.spacing = np.array([[self.spacing[0], 0], [0, self.spacing[1]]]) self.origin = np.array(origin) self.ref_cell = ref_cell self.rotation = rotation self.magnification = magnification self.x_reflection = x_reflection def __str__(self): if isinstance(self.ref_cell, Cell): name = self.ref_cell.name else: name = self.ref_cell return "CellArray (\"{0}\", {1} x {2}, at ({3[0]}, {3[1]}), spacing {4[0]} x {4[1]}, rotation {5}, magnification {6}, reflection {7})".format(name, self.cols, self.rows, self.origin, self.spacing, self.rotation, self.magnification, self.x_reflection) def __repr__(self): if isinstance(self.ref_cell, Cell): name = self.ref_cell.name else: name = self.ref_cell return "CellArray(\"{0}\", {1}, {2}, ({4[0]}, {4[1]}), ({3[0]}, {3[1]}), {5}, {6}, {7})".format(name, self.cols, self.rows, self.origin, self.spacing, self.rotation, self.magnification, self.x_reflection)
[docs] def copy(self, suffix=None): return copy.copy(self)
[docs] def to_gds(self, multiplier, duplicates=[]): """ Convert this object to a GDSII element. :param multiplier: A number that multiplies all dimensions written in the GDSII element. :param uniquify: If True saves the cell reference according to its uniquified name. :returns: The GDSII binary string that represents this object. """ ref_cell = self.ref_cell name = ref_cell.unique_name if ref_cell.name in duplicates else ref_cell.name if len(name)%2 != 0: name = name + '\0' data = struct.pack('>4h', 4, 0x0B00, 4 + len(name), 0x1206) + name.encode('ascii') x2 = self.origin[0] + self.cols * self.spacing[0][0] y2 = self.origin[1] + self.cols * self.spacing[0][1] x3 = self.origin[0] + self.rows * self.spacing[1][0] y3 = self.origin[1] + self.rows * self.spacing[1][1] if not (self.rotation is None) or not (self.magnification is None) or self.x_reflection: word = 0 values = b'' if self.x_reflection: word += 0x8000 y3 = 2 * self.origin[1] - y3 if not (self.magnification is None): word += 0x0004 values += struct.pack('>2h', 12, 0x1B05) + _eight_byte_real(self.magnification) if not (self.rotation is None): word += 0x0002 sa = np.sin(self.rotation * np.pi / 180.0) ca = np.cos(self.rotation * np.pi / 180.0) tmp = (x2 - self.origin[0]) * ca - (y2 - self.origin[1]) * sa + self.origin[0] y2 = (x2 - self.origin[0]) * sa + (y2 - self.origin[1]) * ca + self.origin[1] x2 = tmp tmp = (x3 - self.origin[0]) * ca - (y3 - self.origin[1]) * sa + self.origin[0] y3 = (x3 - self.origin[0]) * sa + (y3 - self.origin[1]) * ca + self.origin[1] x3 = tmp values += struct.pack('>2h', 12, 0x1C05) + _eight_byte_real(self.rotation) data += struct.pack('>2hH', 6, 0x1A01, word) + values return data + struct.pack('>6h6l2h', 8, 0x1302, self.cols, self.rows, 28, 0x1003, int(round(self.origin[0] * multiplier)), int(round(self.origin[1] * multiplier)), int(round(x2 * multiplier)), int(round(y2 * multiplier)), int(round(x3 * multiplier)), int(round(y3 * multiplier)), 4, 0x1100)
[docs] def area(self, by_layer=False): """ Calculate the total area of the referenced cell with the magnification factor included. :param by_layer: If ``True``, the return value is a dictionary with the areas of each individual layer. :returns: Area of this cell. """ if self.magnification is None: factor = self.cols * self.rows else: factor = self.cols * self.rows * self.magnification * self.magnification if by_layer: cell_area = self.ref_cell.area(True) for kk in cell_area.iterkeys(): cell_area[kk] *= factor return cell_area else: return self.ref_cell.area() * factor
@property
[docs] def bounding_box(self): """ Returns the bounding box for this reference. Currently does not handle rotated references :returns: Bounding box of this cell [[x_min, y_min], [x_max, y_max]], or ``None`` if the cell is empty. """ import utils if len(self.ref_cell)==0: return None mag=self.magnification if (self.magnification is not None) else 1.0 size=np.array((self.cols-1, self.rows-1)).dot(self.spacing) bbox=self.ref_cell.bounding_box bbox *= mag bbox[1] += size if self.rotation: x0,y0=bbox[0] x1,y1=bbox[1] box=np.array([[x0,y0], [x0,y1], [x1, y1], [x1, y0]]) box = utils.rotate(box, self.rotation) bbox[0]=[min(box[:,0]), min(box[:,1])] bbox[1]=[max(box[:,0]), max(box[:,1])] bbox[0] += self.origin bbox[1] += self.origin return bbox
[docs] def artist(self): """ Return a list of matplotlib artists for drawing this object .. warning:: Does not yet handle x_reflections correctly """ mag=1.0 if self.magnification is not None: mag=self.magnification artists=[] #Magnify the cell and then pattern for i in range(self.cols): for j in range(self.rows): p=np.array([i,j]).dot(self.spacing) art=self.ref_cell.artist() trans=matplotlib.transforms.Affine2D() trans.scale(mag) trans.translate(p[0], p[1]) for a in art: a.set_transform(a.get_transform() + trans) artists += art #Rotate and translate the patterned array trans=matplotlib.transforms.Affine2D() if self.rotation is not None: trans.rotate_deg(self.rotation) if any(self.origin): trans.translate(self.origin[0], self.origin[1]) for a in artists: a.set_transform(a.get_transform() + trans) return artists
[docs] def flatten(self): """ Return reference as a flattened list of elements. """ mag = 1 if self.magnification is None else self.magnification rot = 0 if self.rotation is None else self.rotation elements = [] sub_el = self.ref_cell.flatten() for i in range(self.cols): for j in range(self.rows): p=np.array([i,j]).dot(self.spacing) for e in sub_el: elements.append(e.copy().scale(mag).translate(p)) for e in elements: e.rotate(rot).translate(self.origin) return elements #def GdsImport(infile, unit=None, rename={}, layers={}, datatypes={}, texttypes={}, verbose=True): # imp=_GdsImport(infile, unit=unit, rename=rename, layers=layers, datatypes=datatypes, texttypes=texttypes, verbose=verbose) # out=Layout('IMPORT') # for v in imp.cell_dict.values(): # out.add(v) # # return out
[docs]def GdsImport(infile, rename={}, layers={}, datatypes={}, verbose=True): """ Import a new Layout from a GDSII stream file. :param infile: GDSII stream file (or path) to be imported. It must be opened for reading in binary format. :param rename: Dictionary used to rename the imported cells. Keys and values must be strings. :param layers: Dictionary used to convert the layers in the imported cells. Keys and values must be integers. :param datatypes: Dictionary used to convert the datatypes in the imported cells. Keys and values must be integers. :param verbose: If False, suppresses warnings about unsupported elements in the imported file. :returns: A :class:``Layout`` containing the imported gds file. Notes:: Not all features from the GDSII specification are currently supported. A warning will be produced if any unsuported features are found in the imported file. Examples:: layout = core.GdsImport('sample.gds') """ _record_name = ('HEADER', 'BGNLIB', 'LIBNAME', 'UNITS', 'ENDLIB', 'BGNSTR', 'STRNAME', 'ENDSTR', 'BOUNDARY', 'PATH', 'SREF', 'AREF', 'TEXT', 'LAYER', 'DATATYPE', 'WIDTH', 'XY', 'ENDEL', 'SNAME', 'COLROW', 'TEXTNODE', 'NODE', 'TEXTTYPE', 'PRESENTATION', 'SPACING', 'STRING', 'STRANS', 'MAG', 'ANGLE', 'UINTEGER', 'USTRING', 'REFLIBS', 'FONTS', 'PATHTYPE', 'GENERATIONS', 'ATTRTABLE', 'STYPTABLE', 'STRTYPE', 'ELFLAGS', 'ELKEY', 'LINKTYPE', 'LINKKEYS', 'NODETYPE', 'PROPATTR', 'PROPVALUE', 'BOX', 'BOXTYPE', 'PLEX', 'BGNEXTN', 'ENDTEXTN', 'TAPENUM', 'TAPECODE', 'STRCLASS', 'RESERVED', 'FORMAT', 'MASK', 'ENDMASKS', 'LIBDIRSIZE', 'SRFNAME', 'LIBSECUR') _unused_records = (0x05, 0x00, 0x01, 0x02, 0x034, 0x38) layout = Layout('IMPORT') _incomplete = [] if infile.__class__ == ''.__class__: infile = open(infile, 'rb') close = True else: close = False emitted_warnings = [] record = _read_record(infile) kwargs = {} create_element = None while record is not None: ## LAYER if record[0] == 0x0d: kwargs['layer'] = layers.get(record[1][0], record[1][0]) ## DATATYPE elif record[0] == 0x0e: kwargs['datatype'] = datatypes.get(record[1][0], record[1][0]) ## TEXTTYPE elif record[0] == 0x16: kwargs['texttype'] = record[1][0] ## XY elif record[0] == 0x10: if 'xy' not in kwargs: kwargs['xy'] = factor * record[1] else: kwargs['xy'] = np.hstack((kwargs['xy'], factor * record[1])) ## WIDTH elif record[0] == 0x0f: kwargs['width'] = factor * abs(record[1][0]) if record[1][0] < 0 and record[0] not in emitted_warnings: warnings.warn("[GDSPY] Paths with absolute width value are not supported. Scaling these paths will also scale their width.", stacklevel=2) emitted_warnings.append(record[0]) ## ENDEL elif record[0] == 0x11: if create_element is not None: cell.add(create_element(**kwargs)) create_element = None kwargs = {} ## BOUNDARY elif record[0] == 0x08: create_element = _create_polygon ## PATH elif record[0] == 0x09: create_element = _create_path ## TEXT elif record[0] == 0x0c: create_element = _create_text ## SNAME elif record[0] == 0x12: if not str is bytes: if record[1][-1] == 0: record[1] = record[1][:-1].decode('ascii') else: record[1] = record[1].decode('ascii') kwargs['ref_cell'] = rename.get(record[1], record[1]) ## COLROW elif record[0] == 0x13: kwargs['columns'] = record[1][0] kwargs['rows'] = record[1][1] ## STRANS elif record[0] == 0x1a: kwargs['x_reflection'] = ((long(record[1][0]) & 0x8000) > 0) ## MAG elif record[0] == 0x1b: kwargs['magnification'] = record[1][0] ## ANGLE elif record[0] == 0x1c: kwargs['rotation'] = record[1][0] ## SREF elif record[0] == 0x0a: create_element = _create_reference ## AREF elif record[0] == 0x0b: create_element = _create_array ## STRNAME elif record[0] == 0x06: if not str is bytes: if record[1][-1] == 0: record[1] = record[1][:-1].decode('ascii') else: record[1] = record[1].decode('ascii') name = rename.get(record[1], record[1]) cell = Cell(name) layout[name] = cell ## STRING elif record[0] == 0x19: if not str is bytes: if record[1][-1] == 0: kwargs['text'] = record[1][:-1].decode('ascii') else: kwargs['text'] = record[1].decode('ascii') else: kwargs['text'] = record[1] ## ENDSTR elif record[0] == 0x07: cell = None ## UNITS elif record[0] == 0x03: factor = record[1][0] ## PRESENTATION elif record[0] == 0x17: kwargs['anchor'] = ['nw', 'n', 'ne', None, 'w', 'o', 'e', None, 'sw', 's', 'se'][record[1][0]] ## ENDLIB elif record[0] == 0x04: for ref in _incomplete: if ref.ref_cell in cell_dict: ref.ref_cell = cell_dict[ref.ref_cell] else: ref.ref_cell = Cell.cell_dict.get(ref.ref_cell, ref.ref_cell) ## Not supported elif verbose and record[0] not in emitted_warnings and record[0] not in _unused_records: warnings.warn("[GDSPY] Record type {0} not supported by gds_import.".format(_record_name[record[0]]), stacklevel=2) emitted_warnings.append(record[0]) record = _read_record(infile) if close: infile.close() return layout
[docs]def DxfImport(fname, scale=1.0): """ Import artwork from a DXF File. :param fname: the DXF file :param scale: the scale factor for the drawing dimensions Currently only supports POLYLINE and LINE entities which are returned as a list. Closed POLYLINES are interpreted as Boundaries, LINES and open POLYLINES are interpreted as Paths. DxfImport will attempt to cast layer name strings to integers. If it fails the default layer will be used. """ dxf = dxfgrabber.readfile(fname) art = [] for e in dxf.entities: if isinstance(e, dxfgrabber.entities.LWPolyline): art.append(_parse_POLYLINE(e, scale)) elif isinstance(e, dxfgrabber.entities.Line): art.append(_parse_LINE(e, scale)) else: print 'Ignoring unknown entity type %s in DxfImport.' % type(e) return art
def _parse_POLYLINE(pline, scale): """ Convert a DXF Polyline to a GDS Path or Boundary """ try: layer = int(pline.layer) except ValueError: layer = None if layer == 0: layer = None; if pline.const_width <> 0: width = pline.const_width * scale else: width = np.array(pline.width).mean() * scale if width == 0: width = 1.0 points = np.array(pline.points) * scale d = np.sqrt(((points[0]-points[-1])**2).sum()) if d < 1e-10: return Boundary(points, layer=layer) else: return Path(points, width=width, layer=layer) def _parse_LINE(line, scale): """ Convert a DXF Line to a GDS Path """ try: layer = int(line.layer) except ValueError: layer = None if layer == 0: layer = None; width = line.thickness if width == 0: width = 1.0 points = np.array((line.start, line.end)) * scale points = points[:,:2] return Path(points, width=width, layer=layer) def _read_record(stream): """ Read a complete record from a GDSII stream file. Parameters ---------- stream : file GDSII stream file to be imported. Returns ------- out : 2-tuple Record type and data (as a np.array) """ header = stream.read(4) if len(header) < 4: return None size, rec_type = struct.unpack('>HH', header) data_type = (rec_type & 0x00ff) rec_type = rec_type // 256 data = None if size > 4: if data_type == 0x01: data = np.array(struct.unpack('>{0}H'.format((size - 4) // 2), stream.read(size - 4)), dtype='uint') elif data_type == 0x02: data = np.array(struct.unpack('>{0}h'.format((size - 4) // 2), stream.read(size - 4)), dtype=int) elif data_type == 0x03: data = np.array(struct.unpack('>{0}l'.format((size - 4) // 4), stream.read(size - 4)), dtype=int) elif data_type == 0x05: data = np.array([_eight_byte_real_to_float(stream.read(8)) for _ in range((size - 4) // 8)]) else: data = stream.read(size - 4) if data[-1] == '\0': data = data[:-1] return [rec_type, data] def _create_polygon(layer, datatype, xy): return Boundary(xy.reshape((xy.size // 2, 2)), layer, datatype) def _create_path(**kwargs): xy = kwargs.pop('xy') kwargs['points'] = xy.reshape((xy.size // 2, 2)) return Path(**kwargs) def _create_text(xy, width=None, **kwargs): kwargs['position'] = xy return Text(**kwargs) def _create_reference(**kwargs): kwargs['origin'] = kwargs.pop('xy') ref = CellReference(**kwargs) if not isinstance(ref.ref_cell, Cell): _incomplete.append(ref) return ref def _create_array(**kwargs): xy = kwargs.pop('xy') kwargs['origin'] = xy[0:2] if 'x_reflection' in kwargs: if 'rotation' in kwargs: sa = -np.sin(kwargs['rotation'] * np.pi / 180.0) ca = np.cos(kwargs['rotation'] * np.pi / 180.0) x2 = (xy[2] - xy[0]) * ca - (xy[3] - xy[1]) * sa + xy[0] y3 = (xy[4] - xy[0]) * sa + (xy[5] - xy[1]) * ca + xy[1] else: x2 = xy[2] y3 = xy[5] if kwargs['x_reflection']: y3 = 2 * xy[1] - y3 kwargs['spacing'] = ((x2 - xy[0]) / kwargs['columns'], (y3 - xy[1]) / kwargs['rows']) else: kwargs['spacing'] = ((xy[2] - xy[0]) / kwargs['columns'], (xy[5] - xy[1]) / kwargs['rows']) ref = CellArray(**kwargs) if not isinstance(ref.ref_cell, Cell): _incomplete.append(ref) return ref def _compact_id(obj): """ Return the id of the object as an ascii string. This is guaranteed to be unique, and uses only characters that are permitted in valid GDSII names. """ i=bin(id(obj))[2:] chars=string.ascii_uppercase+string.ascii_lowercase+string.digits+'?$' out='' while len(i): s=int(i[-6:], base=2) out+=chars[s] i=i[:-6] return out[::-1] def _eight_byte_real(value): """ Convert a number into the GDSII 8 byte real format. Parameters ---------- value : number The number to be converted. Returns ------- out : string The GDSII binary string that represents ``value``. """ byte1 = 0 byte2 = 0 short3 = 0 long4 = 0 if value != 0: if value < 0: byte1 = 0x80 value = -value exponent = int(np.floor(np.log2(value) * 0.25)) mantissa = long(value * 16L**(14 - exponent)) while mantissa >= 72057594037927936L: exponent += 1 mantissa = long(value * 16L**(14 - exponent)) byte1 += exponent + 64 byte2 = (mantissa // 281474976710656L) short3 = (mantissa % 281474976710656L) // 4294967296L long4 = mantissa % 4294967296L return struct.pack(">HHL", byte1 * 256 + byte2, short3, long4) def _eight_byte_real_to_float(value): """ Convert a number from GDSII 8 byte real format to float. Parameters ---------- value : string The GDSII binary string representation of the number. Returns ------- out : float The number represented by ``value``. """ short1, short2, long3 = struct.unpack('>HHL', value) exponent = (short1 & 0x7f00) // 256 mantissa = (((short1 & 0x00ff) * 65536L + short2) * 4294967296L + long3) / 72057594037927936.0 return (-1 if (short1 & 0x8000) else 1) * mantissa * 16L ** (exponent - 64)