import 0.2.1

This commit is contained in:
Jan Petykiewicz 2019-05-15 21:06:29 -07:00
commit 5a4c7db6b4
35 changed files with 3888 additions and 0 deletions

0
gdsii/__init__.py Normal file
View file

230
gdsii/_records.py Normal file
View file

@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2010 Eugeniy Meshcheryakov <eugen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from . import record, tags
class AbstractRecord(object):
def __init__(self, variable):
self.variable = variable
def read(self, instance, gen):
raise NotImplementedError
def save(self, instance, stream):
raise NotImplementedError
def __repr__(self):
return '<property: %s>'%self.variable
class SecondVar(object):
"""Class that simplifies second property support."""
def __init__(self, variable2):
self.variable2 = variable2
class SimpleRecord(AbstractRecord):
def __init__(self, variable, gds_record):
AbstractRecord.__init__(self, variable)
self.gds_record = gds_record
def read(self, instance, gen):
rec = gen.current
rec.check_tag(self.gds_record)
rec.check_size(1)
setattr(instance, self.variable, rec.data[0])
gen.read_next()
def save(self, instance, stream):
record.Record(self.gds_record, (getattr(instance, self.variable),)).save(stream)
class SimpleOptionalRecord(SimpleRecord):
def optional_read(self, instance, unused_gen, rec):
"""
Called when optional tag is found. `rec` contains that tag.
`gen` is advanced to next record befor calling this function.
"""
rec.check_size(1)
setattr(instance, self.variable, rec.data[0])
def read(self, instance, gen):
rec = gen.current
if rec.tag == self.gds_record:
gen.read_next()
self.optional_read(instance, gen, rec)
def save(self, instance, stream):
data = getattr(instance, self.variable, None)
if data is not None:
record.Record(self.gds_record, (data,)).save(stream)
class OptionalWholeRecord(SimpleOptionalRecord):
"""Class for records that need to store all data (not data[0])."""
def optional_read(self, instance, unused_gen, rec):
setattr(instance, self.variable, rec.data)
def save(self, instance, stream):
data = getattr(instance, self.variable, None)
if data is not None:
record.Record(self.gds_record, data).save(stream)
class PropertiesRecord(AbstractRecord):
def read(self, instance, gen):
rec = gen.current
props = []
while rec.tag == tags.PROPATTR:
rec.check_size(1)
propattr = rec.data[0]
rec = gen.read_next()
rec.check_tag(tags.PROPVALUE)
props.append((propattr, rec.data))
rec = gen.read_next()
setattr(instance, self.variable, props)
def save(self, instance, stream):
props = getattr(instance, self.variable)
if props:
for (propattr, propvalue) in props:
record.Record(tags.PROPATTR, (propattr,)).save(stream)
record.Record(tags.PROPVALUE, propvalue).save(stream)
class XYRecord(SimpleRecord):
def read(self, instance, gen):
rec = gen.current
rec.check_tag(self.gds_record)
setattr(instance, self.variable, rec.points)
gen.read_next()
def save(self, instance, stream):
pts = getattr(instance, self.variable)
record.Record(self.gds_record, points=pts).save(stream)
class StringRecord(SimpleRecord):
def read(self, instance, gen):
rec = gen.current
rec.check_tag(self.gds_record)
setattr(instance, self.variable, rec.data)
gen.read_next()
def save(self, instance, stream):
record.Record(self.gds_record, getattr(instance, self.variable)).save(stream)
class ColRowRecord(AbstractRecord, SecondVar):
def __init__(self, variable1, variable2):
AbstractRecord.__init__(self, variable1)
SecondVar.__init__(self, variable2)
def read(self, instance, gen):
rec = gen.current
rec.check_tag(tags.COLROW)
rec.check_size(2)
cols, rows = rec.data
setattr(instance, self.variable, cols)
setattr(instance, self.variable2, rows)
gen.read_next()
def save(self, instance, stream):
col = getattr(instance, self.variable)
row = getattr(instance, self.variable2)
record.Record(tags.COLROW, (col, row)).save(stream)
class TimestampsRecord(SimpleRecord, SecondVar):
def __init__(self, variable1, variable2, gds_record):
SimpleRecord.__init__(self, variable1, gds_record)
SecondVar.__init__(self, variable2)
def read(self, instance, gen):
rec = gen.current
rec.check_tag(self.gds_record)
mod_time, acc_time = rec.times
setattr(instance, self.variable, mod_time)
setattr(instance, self.variable2, acc_time)
gen.read_next()
def save(self, instance, stream):
mod_time = getattr(instance, self.variable)
acc_time = getattr(instance, self.variable2)
record.Record(self.gds_record, times=(mod_time, acc_time)).save(stream)
class STransRecord(OptionalWholeRecord):
mag = SimpleOptionalRecord('mag', tags.MAG)
angle = SimpleOptionalRecord('angle', tags.ANGLE)
def optional_read(self, instance, gen, rec):
setattr(instance, self.variable, rec.data)
self.mag.read(instance, gen)
self.angle.read(instance, gen)
def save(self, instance, stream):
data = getattr(instance, self.variable, None)
if data is not None:
OptionalWholeRecord.save(self, instance, stream)
self.mag.save(instance, stream)
self.angle.save(instance, stream)
class ACLRecord(SimpleOptionalRecord):
def optional_read(self, instance, unused_gen, rec):
setattr(instance, self.variable, rec.acls)
def save(self, instance, stream):
data = getattr(instance, self.variable, None)
if data:
record.Record(self.gds_record, acls=data).save(stream)
class FormatRecord(SimpleOptionalRecord, SecondVar):
def __init__(self, variable1, variable2, gds_record):
SimpleOptionalRecord.__init__(self, variable1, gds_record)
SecondVar.__init__(self, variable2)
def optional_read(self, instance, gen, rec):
SimpleOptionalRecord.optional_read(self, instance, gen, rec)
cur_rec = gen.curent
if cur_rec.tag == tags.MASK:
masks = []
while cur_rec.tag == tags.MASK:
masks.append(cur_rec.data)
cur_rec = gen.read_next()
cur_rec.check_tag(tags.ENDMASKS)
setattr(instance, self.variable2, masks)
gen.read_next()
def save(self, instance, stream):
fmt = getattr(instance, self.variable, None)
if fmt is not None:
SimpleOptionalRecord.save(self, instance, stream)
masks = getattr(instance, self.variable2, None)
if masks:
for mask in masks:
record.Record(tags.MASK, mask).save(stream)
record.Record(tags.ENDMASKS).save(stream)
class UnitsRecord(SimpleRecord, SecondVar):
def __init__(self, variable1, variable2, gds_record):
SimpleRecord.__init__(self, variable1, gds_record)
SecondVar.__init__(self, variable2)
def read(self, instance, gen):
rec = gen.current
rec.check_tag(self.gds_record)
rec.check_size(2)
unit1, unit2 = rec.data
setattr(instance, self.variable, unit1)
setattr(instance, self.variable2, unit2)
gen.read_next()
def save(self, instance, stream):
unit1 = getattr(instance, self.variable)
unit2 = getattr(instance, self.variable2)
record.Record(self.gds_record, (unit1, unit2)).save(stream)

390
gdsii/elements.py Normal file
View file

@ -0,0 +1,390 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2010 Eugeniy Meshcheryakov <eugen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`gdsii.elements` -- interface to GDSII elements
====================================================
This module contains definitions for classes representing
various GDSII elements. Mapping between GDSII elements and
classes is given in the following table:
+-------------------+-------------------+
| GDSII Record | Class |
+===================+===================+
| :const:`AREF` | :class:`ARef` |
+-------------------+-------------------+
| :const:`BOUNDARY` | :class:`Boundary` |
+-------------------+-------------------+
| :const:`BOX` | :class:`Box` |
+-------------------+-------------------+
| :const:`NODE` | :class:`Node` |
+-------------------+-------------------+
| :const:`PATH` | :class:`Path` |
+-------------------+-------------------+
| :const:`SREF` | :class:`SRef` |
+-------------------+-------------------+
| :const:`TEXT` | :class:`Text` |
+-------------------+-------------------+
This module implements the following GDS syntax:
.. productionlist::
element: `aref` |
: `boundary` |
: `box` |
: `node` |
: `path` |
: `sref` |
: `text`
Additional definitions:
.. productionlist::
properties: `property`*
property: PROPATTR
: PROPVALUE
strans: STRANS
: [MAG]
: [ANGLE]
.. moduleauthor:: Eugeniy Meshcheryakov <eugen@debian.org>
"""
from __future__ import absolute_import
from . import exceptions, record, tags, _records
__all__ = (
'Boundary',
'Path',
'SRef',
'ARef',
'Text',
'Node',
'Box'
)
_ELFLAGS = _records.OptionalWholeRecord('elflags', tags.ELFLAGS)
_PLEX = _records.SimpleOptionalRecord('plex', tags.PLEX)
_LAYER = _records.SimpleRecord('layer', tags.LAYER)
_DATATYPE = _records.SimpleRecord('data_type', tags.DATATYPE)
_PATHTYPE = _records.SimpleOptionalRecord('path_type', tags.PATHTYPE)
_WIDTH = _records.SimpleOptionalRecord('width', tags.WIDTH)
_BGNEXTN = _records.SimpleOptionalRecord('bgn_extn', tags.BGNEXTN)
_ENDEXTN = _records.SimpleOptionalRecord('end_extn', tags.ENDEXTN)
_XY = _records.XYRecord('xy', tags.XY)
_SNAME = _records.StringRecord('struct_name', tags.SNAME)
_STRANS = _records.STransRecord('strans', tags.STRANS)
_COLROW = _records.ColRowRecord('cols', 'rows')
_TEXTTYPE = _records.SimpleRecord('text_type', tags.TEXTTYPE)
_PRESENTATION = _records.OptionalWholeRecord('presentation', tags.PRESENTATION)
_STRING = _records.StringRecord('string', tags.STRING)
_NODETYPE = _records.SimpleRecord('node_type', tags.NODETYPE)
_BOXTYPE = _records.SimpleRecord('box_type', tags.BOXTYPE)
_PROPERTIES = _records.PropertiesRecord('properties')
class _Base(object):
"""Base class for all GDSII elements."""
# dummy descriptors to silence pyckecker, should be set in derived classes
_gds_tag = None
_gds_objs = None
__slots__ = ()
def __init__(self):
"""Initialize the element."""
self._init_optional()
def _init_optional(self):
"""Initialize optional attributes to None."""
raise NotImplementedError
@classmethod
def _load(cls, gen):
"""
Load an element from file using given generator `gen`.
:param gen: :class:`pygdsii.record.Record` generator
:returns: new element of class defined by `gen`
"""
element_class = cls._tag_to_class_map[gen.current.tag]
if not element_class:
raise exceptions.FormatError('unexpected element tag')
# do not call __init__() during reading from file
# __init__() should require some arguments
new_element = element_class._read_element(gen)
return new_element
@classmethod
def _read_element(cls, gen):
"""Read element using `gen` generator."""
self = cls.__new__(cls)
self._init_optional()
gen.read_next()
for obj in self._gds_objs:
obj.read(self, gen)
gen.current.check_tag(tags.ENDEL)
gen.read_next()
return self
def _save(self, stream):
record.Record(self._gds_tag).save(stream)
for obj in self._gds_objs:
obj.save(self, stream)
record.Record(tags.ENDEL).save(stream)
class Boundary(_Base):
"""
Class for :const:`BOUNDARY` GDSII element.
GDS syntax:
.. productionlist::
boundary: BOUNDARY
: [ELFLAGS]
: [PLEX]
: LAYER
: DATATYPE
: XY
: [`properties`]
: ENDEL
"""
_gds_tag = tags.BOUNDARY
_gds_objs = (_ELFLAGS, _PLEX, _LAYER, _DATATYPE, _XY, _PROPERTIES)
__slots__ = ('layer', 'data_type', 'xy', 'elflags', 'plex', 'properties')
def __init__(self, layer, data_type, xy):
_Base.__init__(self)
self.layer = layer
self.data_type = data_type
self.xy = xy
def _init_optional(self):
self.elflags = None
self.plex = None
self.properties = None
class Path(_Base):
"""
Class for :const:`PATH` GDSII element.
GDS syntax:
.. productionlist::
path: PATH
: [ELFLAGS]
: [PLEX]
: LAYER
: DATATYPE
: [PATHTYPE]
: [WIDTH]
: [BGNEXTN]
: [ENDEXTN]
: XY
: [`properties`]
: ENDEL
"""
_gds_tag = tags.PATH
_gds_objs = (_ELFLAGS, _PLEX, _LAYER, _DATATYPE, _PATHTYPE, _WIDTH,
_BGNEXTN, _ENDEXTN, _XY, _PROPERTIES)
__slots__ = ('layer', 'data_type', 'xy', 'elflags', 'plex', 'path_type',
'width', 'bgn_extn', 'end_extn', 'properties')
def __init__(self, layer, data_type, xy):
_Base.__init__(self)
self.layer = layer
self.data_type = data_type
self.xy = xy
def _init_optional(self):
self.elflags = None
self.plex = None
self.path_type = None
self.width = None
self.bgn_extn = None
self.end_extn = None
self.properties = None
class SRef(_Base):
"""
Class for :const:`SREF` GDSII element.
GDS syntax:
.. productionlist::
sref: SREF
: [ELFLAGS]
: [PLEX]
: SNAME
: [`strans`]
: XY
: [`properties`]
: ENDEL
"""
_gds_tag = tags.SREF
_gds_objs = (_ELFLAGS, _PLEX, _SNAME, _STRANS, _XY, _PROPERTIES)
__slots__ = ('struct_name', 'xy', 'elflags', 'strans', 'mag', 'angle',
'properties')
def __init__(self, struct_name, xy):
_Base.__init__(self)
self.struct_name = struct_name
self.xy = xy
def _init_optional(self):
self.elflags = None
self.strans = None
self.mag = None
self.angle = None
self.properties = None
class ARef(_Base):
"""
Class for :const:`AREF` GDSII element.
GDS syntax:
.. productionlist::
aref: AREF
: [ELFLAGS]
: [PLEX]
: SNAME
: [`strans`]
: COLROW
: XY
: [`properties`]
: ENDEL
"""
_gds_tag = tags.AREF
_gds_objs = (_ELFLAGS, _PLEX, _SNAME, _STRANS, _COLROW, _XY, _PROPERTIES)
__slots__ = ('struct_name', 'cols', 'rows', 'xy', 'elflags', 'plex',
'strans', 'mag', 'angle', 'properties')
def __init__(self, struct_name, cols, rows, xy):
_Base.__init__(self)
self.struct_name = struct_name
self.cols = cols
self.rows = rows
self.xy = xy
def _init_optional(self):
self.elflags = None
self.plex = None
self.strans = None
self.mag = None
self.angle = None
self.properties = None
class Text(_Base):
"""
Class for :const:`TEXT` GDSII element.
GDS syntax:
.. productionlist::
text: TEXT
: [ELFLAGS]
: [PLEX]
: LAYER
: TEXTTYPE
: [PRESENTATION]
: [PATHTYPE]
: [WIDTH]
: [`strans`]
: XY
: STRING
: [`properties`]
: ENDEL
"""
_gds_tag = tags.TEXT
_gds_objs = (_ELFLAGS, _PLEX, _LAYER, _TEXTTYPE, _PRESENTATION, _PATHTYPE,
_WIDTH, _STRANS, _XY, _STRING, _PROPERTIES)
__slots__ = ('layer', 'text_type', 'xy', 'string', 'elflags', 'plex',
'presentation', 'path_type', 'width', 'strans', 'mag', 'angle',
'properties')
def __init__(self, layer, text_type, xy, string):
_Base.__init__(self)
self.layer = layer
self.text_type = text_type
self.xy = xy
self.string = string
def _init_optional(self):
self.elflags = None
self.plex = None
self.presentation = None
self.path_type = None
self.width = None
self.strans = None
self.mag = None
self.angle = None
self.properties = None
class Node(_Base):
"""
Class for :const:`NODE` GDSII element.
GDS syntax:
.. productionlist::
node: NODE
: [ELFLAGS]
: [PLEX]
: LAYER
: NODETYPE
: XY
: [`properties`]
: ENDEL
"""
_gds_tag = tags.NODE
_gds_objs = (_ELFLAGS, _PLEX, _LAYER, _NODETYPE, _XY, _PROPERTIES)
__slots__ = ('layer', 'node_type', 'xy', 'elflags', 'plex', 'properties')
def __init__(self, layer, node_type, xy):
_Base.__init__(self)
self.layer = layer
self.node_type = node_type
self.xy = xy
def _init_optional(self):
self.elflags = None
self.plex = None
self.properties = None
class Box(_Base):
"""
Class for :const:`BOX` GDSII element.
GDS syntax:
.. productionlist::
box: BOX
: [ELFLAGS]
: [PLEX]
: LAYER
: BOXTYPE
: XY
: [`properties`]
: ENDEL
"""
_gds_tag = tags.BOX
_gds_objs = (_ELFLAGS, _PLEX, _LAYER, _BOXTYPE, _XY, _PROPERTIES)
__slots__ = ('layer', 'box_type', 'xy', 'elflags', 'plex', 'properties')
def __init__(self, layer, box_type, xy):
_Base.__init__(self)
self.layer = layer
self.box_type = box_type
self.xy = xy
def _init_optional(self):
self.elflags = None
self.plex = None
self.properties = None
_all_elements = (Boundary, Path, SRef, ARef, Text, Node, Box)
_Base._tag_to_class_map = (lambda: dict(((cls._gds_tag, cls) for cls in _all_elements)))()

42
gdsii/exceptions.py Normal file
View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2010 Eugeniy Meshcheryakov <eugen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`gdsii.exceptions` --- GDSII exceptions
============================================
This module contains exception classes used in `python-gdsii`.
"""
__all__ = ('FormatError', 'EndOfFileError', 'IncorrectDataSize',
'UnsupportedTagType', 'MissingRecord', 'DataSizeError')
class FormatError(Exception):
"""Base class for all GDSII exceptions."""
class EndOfFileError(FormatError):
"""Raised on unexpected end of file."""
class IncorrectDataSize(FormatError):
"""Raised if data size is incorrect."""
class UnsupportedTagType(FormatError):
"""Raised on unsupported tag type."""
class MissingRecord(FormatError):
"""Raised when required record is not found."""
class DataSizeError(FormatError):
"""Raised when data size is incorrect for a given record."""

139
gdsii/library.py Normal file
View file

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2010 Eugeniy Meshcheryakov <eugen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`gdsii.library` --- interface to a GDSII library
=====================================================
This module contains class that represents a GDSII library.
.. moduleauthor:: Eugeniy Meshcheryakov <eugen@debian.org>
"""
from __future__ import absolute_import
from . import exceptions, record, structure, tags, _records
from datetime import datetime
_HEADER = _records.SimpleRecord('version', tags.HEADER)
_BGNLIB = _records.TimestampsRecord('mod_time', 'acc_time', tags.BGNLIB)
_LIBDIRSIZE = _records.SimpleOptionalRecord('libdirsize', tags.LIBDIRSIZE)
_SRFNAME = _records.OptionalWholeRecord('srfname', tags.SRFNAME)
_LIBSECUR = _records.ACLRecord('acls', tags.LIBSECUR)
_LIBNAME = _records.StringRecord('name', tags.LIBNAME)
_REFLIBS = _records.OptionalWholeRecord('reflibs', tags.REFLIBS)
_FONTS = _records.OptionalWholeRecord('fonts', tags.FONTS)
_ATTRTABLE = _records.OptionalWholeRecord('attrtable', tags.ATTRTABLE)
_GENERATIONS = _records.SimpleOptionalRecord('generations', tags.GENERATIONS)
_FORMAT = _records.FormatRecord('format', 'masks', tags.FORMAT)
_UNITS = _records.UnitsRecord('logical_unit', 'physical_unit', tags.UNITS)
class Library(list):
"""
GDSII library class. This class is derived from :class:`list` and can contain
one one more instances of :class:`gdsii.structure.Structure`.
GDS syntax for the library:
.. productionlist::
library: HEADER
: BGNLIB
: [LIBDIRSIZE]
: [SRFNAME]
: [LIBSECUR]
: LIBNAME
: [REFLIBS]
: [FONTS]
: [ATTRTABLE]
: [GENERATIONS]
: [`format`]
: UNITS
: {`structure`}*
: ENDLIB
format: FORMAT
: [MASK+ ENDMASKS]
"""
_gds_objs = (_HEADER, _BGNLIB, _LIBDIRSIZE, _SRFNAME, _LIBSECUR, _LIBNAME, _REFLIBS,
_FONTS, _ATTRTABLE, _GENERATIONS, _FORMAT, _UNITS)
def __init__(self, version, name, physical_unit, logical_unit, mod_time=None,
acc_time=None):
"""
Initialize the library.
`mod_time` and `acc_time` are set to current UTC time by default.
"""
list.__init__(self)
self.version = version
self.name = name
self.physical_unit = physical_unit
self.logical_unit = logical_unit
self.mod_time = mod_time if mod_time is not None else datetime.utcnow()
self.acc_time = acc_time if acc_time is not None else datetime.utcnow()
self._init_optional()
def _init_optional(self):
"""Initialize optional attributes to None."""
self.libdirsize = None
self.srfname = None
self.acls = None
self.reflibs = None
self.fonts = None
self.attrtable = None
self.generations = None
self.format = None
self.masks = None
@classmethod
def load(cls, stream):
"""
Load a GDS library from a file.
:param stream: a :class:`file` or file-like object opened for reading in binary mode.
:returns: a new library.
"""
self = cls.__new__(cls)
list.__init__(self)
self._init_optional()
gen = record.Reader(stream)
gen.read_next()
for obj in self._gds_objs:
obj.read(self, gen)
# read structures starting with BGNSTR or ENDLIB
rec = gen.current
while True:
if rec.tag == tags.BGNSTR:
self.append(structure.Structure._load(gen))
rec = gen.read_next()
elif rec.tag == tags.ENDLIB:
break
else:
raise exceptions.FormatError('unexpected tag where BGNSTR or ENDLIB are expected: %d', rec.tag)
return self
def save(self, stream):
"""
Save the library into a file.
:param stream: a :class:`file` or file-like object opened for writing in binary mode.
"""
for obj in self._gds_objs:
obj.save(self, stream)
for struc in self:
struc._save(stream)
record.Record(tags.ENDLIB).save(stream)
def __repr__(self):
return '<Library: %s>' % self.name.decode()

578
gdsii/record.py Normal file
View file

@ -0,0 +1,578 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2010 Eugeniy Meshcheryakov <eugen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`gdsii.record` --- GDSII record I/O
========================================
This module contains classes for low-level GDSII I/O.
.. moduleauthor:: Eugeniy Meshcheryakov <eugen@debian.org>
"""
from __future__ import absolute_import
from . import exceptions, tags, types
from datetime import datetime
import math
import struct
__all__ = [
'Record',
'Reader'
]
_RECORD_HEADER_FMT = struct.Struct('>HH')
def _parse_nodata(data):
"""Parse :const:`NODATA` data type. Does nothing."""
def _parse_bitarray(data):
"""
Parse :const:`BITARRAY` data type.
>>> _parse_bitarray(b'ab') # ok, 2 bytes
24930
>>> _parse_bitarray(b'abcd') # too long
Traceback (most recent call last):
...
IncorrectDataSize: BITARRAY
>>> _parse_bitarray('') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: BITARRAY
"""
if len(data) != 2:
raise exceptions.IncorrectDataSize('BITARRAY')
(val,) = struct.unpack('>H', data)
return val
def _parse_int2(data):
"""
Parse INT2 data type.
>>> _parse_int2(b'abcd') # ok, even number of bytes
(24930, 25444)
>>> _parse_int2(b'abcde') # odd number of bytes
Traceback (most recent call last):
...
IncorrectDataSize: INT2
>>> _parse_int2(b'') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: INT2
"""
data_len = len(data)
if not data_len or (data_len % 2):
raise exceptions.IncorrectDataSize('INT2')
return struct.unpack('>%dh' % (data_len//2), data)
def _parse_int4(data):
"""
Parse INT4 data type.
>>> _parse_int4(b'abcd')
(1633837924,)
>>> _parse_int4(b'abcdef') # not divisible by 4
Traceback (most recent call last):
...
IncorrectDataSize: INT4
>>> _parse_int4(b'') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: INT4
"""
data_len = len(data)
if not data_len or (data_len % 4):
raise exceptions.IncorrectDataSize('INT4')
return struct.unpack('>%dl' % (data_len//4), data)
def _int_to_real(num):
"""
Convert REAL8 from internal integer representation to Python reals.
Zeroes:
>>> print(_int_to_real(0x0))
0.0
>>> print(_int_to_real(0x8000000000000000)) # negative
0.0
>>> print(_int_to_real(0xff00000000000000)) # denormalized
0.0
Others:
>>> print(_int_to_real(0x4110000000000000))
1.0
>>> print(_int_to_real(0xC120000000000000))
-2.0
"""
sgn = -1 if 0x8000000000000000 & num else 1
mant = num & 0x00ffffffffffffff
exp = (num >> 56) & 0x7f
return math.ldexp(sgn * mant, 4 * (exp - 64) - 56)
def _parse_real8(data):
"""
Parse REAL8 data type.
>>> _parse_real8(struct.pack('>3Q', 0x0, 0x4110000000000000, 0xC120000000000000))
(0.0, 1.0, -2.0)
>>> _parse_real8(b'') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: REAL8
>>> _parse_real8(b'abcd') # not divisible by 8
Traceback (most recent call last):
...
IncorrectDataSize: REAL8
"""
data_len = len(data)
if not data_len or (data_len % 8):
raise exceptions.IncorrectDataSize('REAL8')
ints = struct.unpack('>%dQ' % (data_len//8), data)
return tuple(_int_to_real(n) for n in ints)
def _parse_ascii(data):
r"""
Parse ASCII data type.
>>> _parse_ascii(b'') # zero bytes
Traceback (most recent call last):
...
IncorrectDataSize: ASCII
>>> _parse_ascii(b'abcde') == b'abcde'
True
>>> _parse_ascii(b'abcde\0') == b'abcde' # strips trailing NUL
True
"""
if not len(data):
raise exceptions.IncorrectDataSize('ASCII')
# XXX cross-version compatibility
if data[-1:] == b'\0':
return data[:-1]
return data
_PARSE_FUNCS = {
types.NODATA: _parse_nodata,
types.BITARRAY: _parse_bitarray,
types.INT2: _parse_int2,
types.INT4: _parse_int4,
types.REAL8: _parse_real8,
types.ASCII: _parse_ascii
}
def _pack_nodata(data):
"""
Pack NODATA tag data. Should always return empty string::
>>> packed = _pack_nodata([])
>>> packed == b''
True
>>> len(packed)
0
"""
return b''
def _pack_bitarray(data):
"""
Pack BITARRAY tag data.
>>> packed = _pack_bitarray(123)
>>> packed == struct.pack('>H', 123)
True
>>> len(packed)
2
"""
return struct.pack('>H', data)
def _pack_int2(data):
"""
Pack INT2 tag data.
>>> _pack_int2([1, 2, -3]) == struct.pack('>3h', 1, 2, -3)
True
>>> packed = _pack_int2((1, 2, 3))
>>> packed == struct.pack('>3h', 1, 2, 3)
True
>>> len(packed)
6
"""
size = len(data)
return struct.pack('>{0}h'.format(size), *data)
def _pack_int4(data):
"""
Pack INT4 tag data.
>>> _pack_int4([1, 2, -3]) == struct.pack('>3l', 1, 2, -3)
True
>>> packed = _pack_int4((1, 2, 3))
>>> packed == struct.pack('>3l', 1, 2, 3)
True
>>> len(packed)
12
"""
size = len(data)
return struct.pack('>{0}l'.format(size), *data)
def _real_to_int(fnum):
"""
Convert REAL8 from Python real to internal integer representation.
>>> '0x%016x' % _real_to_int(0.0)
'0x0000000000000000'
>>> print(_int_to_real(_real_to_int(1.0)))
1.0
>>> print(_int_to_real(_real_to_int(-2.0)))
-2.0
>>> print(_int_to_real(_real_to_int(1e-9)))
1e-09
"""
# first convert number to IEEE double and split it in parts
(ieee,) = struct.unpack('=Q', struct.pack('=d', fnum))
sign = ieee & 0x8000000000000000
ieee_exp = (ieee >> 52) & 0x7ff
ieee_mant = ieee & 0xfffffffffffff
if ieee_exp == 0:
# zero or denormals
# TODO maybe handle denormals
return 0
# substract exponent bias
unb_ieee_exp = ieee_exp - 1023
# add leading one and move to GDSII position
ieee_mant_full = (ieee_mant + 0x10000000000000) << 3
# convert exponent to 16-based, +1 for differences in presentation
# of mantissa (1.xxxx in EEEE and 0.1xxxxx in GDSII
exp16, rest = divmod(unb_ieee_exp + 1, 4)
# compensate exponent converion
if rest:
rest = 4 - rest
exp16 += 1
ieee_mant_comp = ieee_mant_full >> rest
# add GDSII exponent bias
exp16_biased = exp16 + 64
# try to fit everything
if exp16_biased < -14:
return 0 # number is too small. FIXME is it possible?
elif exp16_biased < 0:
ieee_mant_comp = ieee_mant_comp >> (exp16_biased * 4)
exp16_biased = 0
elif exp16_biased > 0x7f:
raise exceptions.FormatError('number is to big for REAL8')
return sign | (exp16_biased << 56) | ieee_mant_comp
def _pack_real8(data):
"""
Pack REAL8 tag data.
>>> packed = _pack_real8([0, 1, -1, 0.5, 1e-9])
>>> len(packed)
40
>>> list(map(str, _parse_real8(packed)))
['0.0', '1.0', '-1.0', '0.5', '1e-09']
"""
size = len(data)
return struct.pack('>{0}Q'.format(size), *[_real_to_int(num) for num in data])
def _pack_ascii(data):
r"""
Pack ASCII tag data.
>>> _pack_ascii(b'abcd') == b'abcd'
True
>>> _pack_ascii(b'abc') == b'abc\0'
True
"""
size = len(data)
if size % 2:
return data + b'\0'
return data
_PACK_FUNCS = {
types.NODATA: _pack_nodata,
types.BITARRAY: _pack_bitarray,
types.INT2: _pack_int2,
types.INT4: _pack_int4,
types.REAL8: _pack_real8,
types.ASCII: _pack_ascii
}
class Record(object):
"""
Class for representing a GDSII record with attached data.
Example::
>>> r = Record(tags.STRNAME, 'my_structure')
>>> '%04x' % r.tag
'0606'
>>> r.tag_name
'STRNAME'
>>> r.tag_type
6
>>> r.tag_type_name
'ASCII'
>>> r.data
'my_structure'
>>> r = Record(0xffff, 'xxx') # Unknown tag type
>>> r.tag_name
'0xffff'
>>> r.tag_type_name
'0xff'
"""
__slots__ = ['tag', 'data']
def __init__(self, tag, data=None, points=None, times=None, acls=None):
"""Initialize with tag and parsed data."""
self.tag = tag
if data is not None:
self.data = data
elif points is not None:
new_data = []
# TODO make it faster
for point in points:
new_data.append(point[0])
new_data.append(point[1])
self.data = new_data
elif times is not None:
mod_time = times[0]
acc_time = times[1]
self.data = (
mod_time.year - 1900,
mod_time.month,
mod_time.day,
mod_time.hour,
mod_time.minute,
mod_time.second,
acc_time.year - 1900,
acc_time.month,
acc_time.day,
acc_time.hour,
acc_time.minute,
acc_time.second
)
elif acls is not None:
new_data = []
for acl in acls:
new_data.extend(acl)
self.data = new_data
else:
self.data = None
def check_tag(self, tag):
"""
Checks if current record has the same tag as the given one.
Raises :exc:`MissingRecord` exception otherwise. For example::
>>> rec = Record(tags.STRNAME, b'struct')
>>> rec.check_tag(tags.STRNAME)
>>> rec.check_tag(tags.DATATYPE)
Traceback (most recent call last):
...
MissingRecord: Wanted: 3586, got: STRNAME
"""
if self.tag != tag:
raise exceptions.MissingRecord('Wanted: %s, got: %s'%(tag, self.tag_name))
def check_size(self, size):
"""
Checks if data size equals to the given size.
Raises :exc:`DataSizeError` otherwise. For example::
>>> rec = Record(tags.DATATYPE, (0,))
>>> rec.check_size(1)
>>> rec.check_size(5)
Traceback (most recent call last):
...
DataSizeError: 3586
"""
if len(self.data) != size:
raise exceptions.DataSizeError(self.tag)
@classmethod
def read(cls, stream):
"""
Read a GDSII record from file.
:param stream: GDS file opened for reading in binary mode
:returns: a new :class:`Record` instance
:raises: :exc:`UnsupportedTagType` if data cannot be parsed
:raises: :exc:`EndOfFileError` if end of file is reached
"""
header = stream.read(4)
if not header or len(header) != 4:
raise exceptions.EndOfFileError
data_size, tag = _RECORD_HEADER_FMT.unpack(header)
if data_size < 4:
raise exceptions.IncorrectDataSize('data size is too small')
if data_size % 2:
raise exceptions.IncorrectDataSize('data size is odd')
data_size -= 4 # substract header size
data = stream.read(data_size)
if len(data) != data_size:
raise exceptions.EndOfFileError
tag_type = tags.type_of_tag(tag)
try:
parse_func = _PARSE_FUNCS[tag_type]
except KeyError:
raise exceptions.UnsupportedTagType(tag_type)
return cls(tag, parse_func(data))
def save(self, stream):
"""
Save record to a GDS file.
:param stream: file opened for writing in binary mode
:raises: :exc:`UnsupportedTagType` if tag type is not supported
:raises: :exc:`FormatError` on incorrect data sizes, etc
:raises: whatever :func:`struct.pack` can raise
"""
tag_type = self.tag_type
try:
pack_func = _PACK_FUNCS[tag_type]
except KeyError:
raise exceptions.UnsupportedTagType(tag_type)
packed_data = pack_func(self.data)
record_size = len(packed_data) + 4
if record_size > 0xFFFF:
raise exceptions.FormatError('data size is too big')
header = _RECORD_HEADER_FMT.pack(record_size, self.tag)
stream.write(header)
stream.write(packed_data)
@property
def tag_name(self):
"""Tag name, if known, otherwise tag ID formatted as hex number."""
if self.tag in tags.REV_DICT:
return tags.REV_DICT[self.tag]
return '0x%04x' % self.tag
@property
def tag_type(self):
"""Tag data type ID."""
return tags.type_of_tag(self.tag)
@property
def tag_type_name(self):
"""Tag data type name, if known, and formatted number otherwise."""
tag_type = tags.type_of_tag(self.tag)
if tag_type in types.REV_DICT:
return types.REV_DICT[tag_type]
return '0x%02x' % tag_type
@property
def points(self):
"""
Convert data to list of points. Useful for :const:`XY` record.
Raises :exc:`DataSizeError` if data size is incorrect.
For example::
>>> r = Record(tags.XY, [0, 1, 2, 3])
>>> r.points
[(0, 1), (2, 3)]
>>> r = Record(tags.XY, []) # not allowed
>>> r.points
Traceback (most recent call last):
...
DataSizeError: 4099
>>> r = Record(tags.XY, [1, 2, 3]) # odd number of coordinates
>>> r.points
Traceback (most recent call last):
...
DataSizeError: 4099
"""
data_size = len(self.data)
if not data_size or (data_size % 2):
raise exceptions.DataSizeError(self.tag)
return [(self.data[i], self.data[i+1]) for i in range(0, data_size, 2)]
@property
def times(self):
"""
Convert data to tuple ``(modification time, access time)``.
Useful for :const:`BGNLIB` and :const:`BGNSTR`.
>>> r = Record(tags.BGNLIB, [100, 1, 1, 1, 2, 3, 110, 8, 14, 21, 10, 35])
>>> print(r.times[0].isoformat())
2000-01-01T01:02:03
>>> print(r.times[1].isoformat())
2010-08-14T21:10:35
>>> r = Record(tags.BGNLIB, [100, 1, 1, 1, 2, 3]) # wrong data length
>>> r.times
Traceback (most recent call last):
...
DataSizeError: 258
"""
if len(self.data) != 12:
raise exceptions.DataSizeError(self.tag)
return (datetime(self.data[0]+1900, *self.data[1:6]),
datetime(self.data[6]+1900, *self.data[7:12]))
@property
def acls(self):
"""
Convert data to list of acls ``(GID, UID, ACCESS)``.
Useful for :const:`LIBSECUR`.
>>> r = Record(tags.LIBSECUR, [1, 2, 3, 4, 5, 6])
>>> r.acls
[(1, 2, 3), (4, 5, 6)]
>>> r = Record(tags.LIBSECUR, [1, 2, 3, 4]) # wrong data size
>>> r.acls
Traceback (most recent call last):
...
DataSizeError: 15106
"""
if len(self.data) % 3:
raise exceptions.DataSizeError(self.tag)
return list(zip(self.data[::3], self.data[1::3], self.data[2::3]))
@classmethod
def iterate(cls, stream):
"""
Generator function for iterating over all records in a GDSII file.
Yields :class:`Record` objects.
:param stream: GDS file opened for reading in binary mode
"""
last = False
while not last:
rec = cls.read(stream)
if rec.tag == tags.ENDLIB:
last = True
yield rec
class Reader(object):
"""Class for buffered reading of Records"""
__slots__ = ('current', 'stream')
def __init__(self, stream):
self.stream = stream
def read_next(self):
"""Read and return next record from stream."""
self.current = Record.read(self.stream)
return self.current
if __name__ == '__main__':
import doctest
doctest.testmod(optionflags=doctest.IGNORE_EXCEPTION_DETAIL)

84
gdsii/structure.py Normal file
View file

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2010 Eugeniy Meshcheryakov <eugen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`gdsii.structure` --- interface to a GDSII structure
=========================================================
This module contains class that represents a GDSII structure.
.. moduleauthor:: Eugeniy Meshcheryakov <eugen@debian.org>
"""
from __future__ import absolute_import
from . import elements, record, tags, _records
from datetime import datetime
_STRNAME = _records.StringRecord('name', tags.STRNAME)
_BGNSTR = _records.TimestampsRecord('mod_time', 'acc_time', tags.BGNSTR)
_STRCLASS = _records.SimpleOptionalRecord('strclass', tags.STRCLASS)
class Structure(list):
"""
GDSII structure class. This class is derived for :class:`list` and can
contain one or more elements from :mod:`gdsii.elements`.
GDS syntax for the structure:
.. productionlist::
structure: BGNSTR
: STRNAME
: [STRCLASS]
: {`element`}*
: ENDSTR
"""
_gds_objs = (_BGNSTR, _STRNAME, _STRCLASS)
def __init__(self, name, mod_time=None, acc_time=None):
"""
Initialize the structure.
`mod_time` and `acc_time` are set to current UTC time by default.
"""
list.__init__(self)
self.name = name
self.mod_time = mod_time if mod_time is not None else datetime.utcnow()
self.acc_time = acc_time if acc_time is not None else datetime.utcnow()
def _init_optional(self):
"""Initialize optional attributes to None."""
self.strclass = None
@classmethod
def _load(cls, gen):
self = cls.__new__(cls)
list.__init__(self)
self._init_optional()
for obj in self._gds_objs:
obj.read(self, gen)
# read elements till ENDSTR
while gen.current.tag != tags.ENDSTR:
self.append(elements._Base._load(gen))
return self
def _save(self, stream):
for obj in self._gds_objs:
obj.save(self, stream)
for elem in self:
elem._save(stream)
record.Record(tags.ENDSTR).save(stream)
def __repr__(self):
return '<Structure: %s>' % self.name.decode()

128
gdsii/tags.py Normal file
View file

@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2010 Eugeniy Meshcheryakov <eugen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`gdsii.tags` --- definitions of GDSII tags
===============================================
This module containing definitions of GDSII record tags.
.. moduleauthor:: Eugeniy Meshcheryakov <eugen@debian.org>
"""
DICT = {
'HEADER': 0x0002,
'BGNLIB': 0x0102,
'LIBNAME': 0x0206,
'UNITS': 0x0305,
'ENDLIB': 0x0400,
'BGNSTR': 0x0502,
'STRNAME': 0x0606,
'ENDSTR': 0x0700,
'BOUNDARY': 0x0800,
'PATH': 0x0900,
'SREF': 0x0A00,
'AREF': 0x0B00,
'TEXT': 0x0C00,
'LAYER': 0x0D02,
'DATATYPE': 0x0E02,
'WIDTH': 0x0F03,
'XY': 0x1003,
'ENDEL': 0x1100,
'SNAME': 0x1206,
'COLROW': 0x1302,
'TEXTNODE': 0x1400,
'NODE': 0x1500,
'TEXTTYPE': 0x1602,
'PRESENTATION': 0x1701,
# SPACING: 0x18??
'STRING': 0x1906,
'STRANS': 0x1A01,
'MAG': 0x1B05,
'ANGLE': 0x1C05,
# UINTEGER: 0x1D??
# USTRING: 0x1E??
'REFLIBS': 0x1F06,
'FONTS': 0x2006,
'PATHTYPE': 0x2102,
'GENERATIONS': 0x2202,
'ATTRTABLE': 0x2306,
'STYPTABLE': 0x2406,
'STRTYPE': 0x2502,
'ELFLAGS': 0x2601,
'ELKEY': 0x2703,
# LINKTYPE: 0x28??
# LINKKEYS: 0x29??
'NODETYPE': 0x2A02,
'PROPATTR': 0x2B02,
'PROPVALUE': 0x2C06,
'BOX': 0x2D00,
'BOXTYPE': 0x2E02,
'PLEX': 0x2F03,
'BGNEXTN': 0x3003,
'ENDEXTN': 0x3103,
'TAPENUM': 0x3202,
'TAPECODE': 0x3302,
'STRCLASS': 0x3401,
# RESERVED: 0x3503
'FORMAT': 0x3602,
'MASK': 0x3706,
'ENDMASKS': 0x3800,
'LIBDIRSIZE': 0x3902,
'SRFNAME': 0x3A06,
'LIBSECUR': 0x3B02,
# Types used only with Custom Plus
'BORDER': 0x3C00,
'SOFTFENCE': 0x3D00,
'HARDFENCE': 0x3E00,
'SOFTWIRE': 0x3F00,
'HARDWIRE': 0x4000,
'PATHPORT': 0x4100,
'NODEPORT': 0x4200,
'USERCONSTRAINT': 0x4300,
'SPACERERROR': 0x4400,
'CONTACT': 0x4500
}
REV_DICT = {}
for (key, value) in DICT.items():
globals()[key] = value
REV_DICT[value] = key
del key, value
def type_of_tag(tag):
"""
Returns type of a tag.
:param tag: tag ID
:type tag: int
:rtype: int
Examples:
>>> type_of_tag(HEADER)
2
>>> type_of_tag(MASK)
6
"""
return tag & 0xff
if __name__ == '__main__':
import doctest
doctest.testmod()

40
gdsii/types.py Normal file
View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2010 Eugeniy Meshcheryakov <eugen@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`gdsii.types` --- definitions of GDSII data types
======================================================
This module contains definitions of GDSII data types.
"""
DICT = {
'NODATA': 0,
'BITARRAY': 1,
'INT2': 2,
'INT4': 3,
'REAL4': 4, # not used
'REAL8': 5,
'ASCII': 6
}
REV_DICT = {}
for (key, value) in DICT.items():
globals()[key] = value
REV_DICT[value] = key
del key, value