initial commit
commit
034d5fbcb7
@ -0,0 +1,200 @@
|
||||
# klamath README
|
||||
|
||||
`klamath` is a Python module for reading and writing to the GDSII file format.
|
||||
|
||||
The goal is to keep this library simple:
|
||||
- Map data types directly wherever possible.
|
||||
* Presents an accurate representation of what is saved to the file.
|
||||
* Avoids excess copies / allocations for speed.
|
||||
* No "automatic" error checking, except when casting datatypes.
|
||||
If data integrity checks are provided at all, they must be
|
||||
explicitly run by the caller.
|
||||
- Low-level functionality is first-class.
|
||||
* Meant for use-cases where the caller wants to read or write
|
||||
individual GDS records.
|
||||
* Offers complete control over the written file.
|
||||
- Opinionated and limited high-level functionality.
|
||||
* Discards or ignores rarely-encountered data types.
|
||||
* Keeps functions simple and reusable.
|
||||
* Only de/encodes the file format, doesn't provide tools to modify
|
||||
the data itself.
|
||||
* Still requires explicit values for most fields.
|
||||
- No compilation
|
||||
* Uses `numpy` for speed, since it's commonly available / pre-built.
|
||||
* Building this library should not require a compiler.
|
||||
|
||||
`klamath` was built to provide a fast and versatile GDS interface for
|
||||
[masque](https://mpxd.net/code/jan/masque), which provides higher-level
|
||||
tools for working with hierarchical design data and supports multiple
|
||||
file formats.
|
||||
|
||||
|
||||
### Alternatives
|
||||
- [gdspy](https://github.com/heitzmann/gdspy)
|
||||
* Provides abstractions and methods for working with design data
|
||||
outside of the I/O process (e.g. polygon clipping).
|
||||
* Requires compilation (C++) to build from source.
|
||||
* Focused on high-level API
|
||||
- [python-gdsii](https://pypi.org/project/python-gdsii)
|
||||
* Pure-python implementation. Can easily be altered to use `numpy`
|
||||
for speed, but is limited by object allocation overhead.
|
||||
* Focused on high-level API
|
||||
|
||||
|
||||
### Links
|
||||
- [Source repository](https://mpxd.net/code/jan/klamath)
|
||||
- [PyPI](https://pypi.org/project/klamath)
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Requirements:
|
||||
* python >= 3.7 (written and tested with 3.8)
|
||||
* numpy
|
||||
|
||||
|
||||
Install with pip:
|
||||
```bash
|
||||
pip3 install klamath
|
||||
```
|
||||
|
||||
Alternatively, install from git
|
||||
```bash
|
||||
pip3 install git+https://mpxd.net/code/jan/klamath.git@release
|
||||
```
|
||||
|
||||
## Examples
|
||||
### Low-level
|
||||
|
||||
Filter which polygons are read based on layer:
|
||||
|
||||
```python3
|
||||
import io
|
||||
import klamath
|
||||
from klamath import records
|
||||
from klamath.record import Record
|
||||
|
||||
def read_polygons(stream, filter_layer_tuple=(4, 5)):
|
||||
"""
|
||||
Given a stream positioned at the start of a record,
|
||||
return the vertices of all BOUNDARY records which match
|
||||
the provided `filter_layer_tuple`, up to the next
|
||||
ENDSTR record.
|
||||
"""
|
||||
polys = []
|
||||
while True:
|
||||
size, tag = Record.read_header(stream)
|
||||
stream.seek(size, io.SEEK_CUR) # skip to next header
|
||||
|
||||
if tag == records.ENDEL.tag:
|
||||
break # If ENDEL, we are done
|
||||
|
||||
if tag != records.BOUNDARY.tag:
|
||||
continue # Skip until we find a BOUNDARY
|
||||
|
||||
layer = records.LAYER.skip_and_read(stream)[0] # skip to LAYER
|
||||
dtype = records.DATATYPE.read(stream)[0]
|
||||
|
||||
if (layer, dtype) != filter_layer_tuple:
|
||||
continue # Skip reading XY unless layer matches
|
||||
|
||||
xy = XY.read(stream).reshape(-1, 2)
|
||||
polys.append(xy)
|
||||
return polys
|
||||
```
|
||||
|
||||
### High-level
|
||||
|
||||
Write an example GDS file:
|
||||
|
||||
```python3
|
||||
import klamath
|
||||
from klamath.elements import Boundary, Text, Path, Reference
|
||||
|
||||
stream = file.open('example.gds', 'wb')
|
||||
|
||||
header = klamath.library.FileHeader(
|
||||
name=b'example',
|
||||
meters_per_db_unit=1e-9, # 1 nm DB unit
|
||||
user_units_per_db_unit=1e-3) # 1 um (1000nm) display unit
|
||||
header.write(stream)
|
||||
|
||||
elements_A = [
|
||||
Boundary(layer=(4, 18),
|
||||
xy=[[0, 0], [10, 0], [10, 20], [0, 20], [0, 0]],
|
||||
properties={1: b'prop1string', 2: b'some other string'}),
|
||||
Text(layer=(5, 5),
|
||||
xy=[[5, 10]],
|
||||
string=b'center position',
|
||||
properties={}, # Remaining args are set to default values
|
||||
presentation=0, # and will be omitted when writing
|
||||
angle_deg=0,
|
||||
invert_y=False,
|
||||
width=0,
|
||||
path_type=0,
|
||||
mag=1),
|
||||
Path(layer=(4, 20),
|
||||
xy=[[0, 0], [10, 10], [0, 20]],
|
||||
path_type=0,
|
||||
width=0,
|
||||
extension=(0, 0), # ignored since path_type=0
|
||||
properties={}),
|
||||
]
|
||||
klamath.library.write(stream, name=b'my_struct', elements=elements_A)
|
||||
|
||||
elements_top = [
|
||||
Reference(struct_name=b'my_struct',
|
||||
xy=[[30, 30]],
|
||||
colrow=None, # not an array
|
||||
angle_deg=0,
|
||||
invert_y=True,
|
||||
mag=1.5,
|
||||
properties={}),
|
||||
Reference(struct_name=b'my_struct',
|
||||
colrow=(3, 2), # 3x2 array at (0, 50)
|
||||
xy=[[0, 50], [60, 50], [30, 50]] # with basis vectors
|
||||
angle_deg=30, # [20, 0] and [0, 30]
|
||||
invert_y=False,
|
||||
mag=1,
|
||||
properties={}),
|
||||
]
|
||||
klamath.library.write(stream, name=b'top', elements=elements_top)
|
||||
|
||||
klamath.records.ENDLIB.write(stream)
|
||||
stream.close()
|
||||
```
|
||||
|
||||
Read back the file:
|
||||
|
||||
```python3
|
||||
import klamath
|
||||
|
||||
stream = file.open('example.gds', 'rb')
|
||||
header = klamath.library.FileHeader.read(stream)
|
||||
|
||||
structs = {}
|
||||
|
||||
struct = klamath.library.try_read_struct(stream)
|
||||
while struct is not None:
|
||||
name, elements = struct
|
||||
structs[name] = elements
|
||||
struct = klamath.library.try_read_struct(stream)
|
||||
|
||||
stream.close()
|
||||
```
|
||||
|
||||
Read back a single struct by name:
|
||||
|
||||
```python3
|
||||
import klamath
|
||||
|
||||
stream = file.open('example.gds', 'rb')
|
||||
|
||||
header = klamath.library.FileHeader.read(stream)
|
||||
struct_positions = klamath.library.scan_structs(stream)
|
||||
|
||||
stream.seek(struct_positions[b'my_struct'])
|
||||
elements_A = klamath.library.try_read_struct(stream)
|
||||
|
||||
stream.close()
|
||||
```
|
@ -0,0 +1 @@
|
||||
0.1
|
@ -0,0 +1,42 @@
|
||||
"""
|
||||
`klamath` is a Python module for reading and writing to the GDSII file format.
|
||||
|
||||
The goal is to keep this library simple:
|
||||
- Map data types directly wherever possible.
|
||||
* Presents an accurate representation of what is saved to the file.
|
||||
* Avoids excess copies / allocations for speed.
|
||||
* No "automatic" error checking, except when casting datatypes.
|
||||
If data integrity checks are provided at all, they must be
|
||||
explicitly run by the caller.
|
||||
- Low-level functionality is first-class.
|
||||
* Meant for use-cases where the caller wants to read or write
|
||||
individual GDS records.
|
||||
* Offers complete control over the written file.
|
||||
- Opinionated and limited high-level functionality.
|
||||
* Discards or ignores rarely-encountered data types.
|
||||
* Keeps functions simple and reusable.
|
||||
* Only de/encodes the file format, doesn't provide tools to modify
|
||||
the data itself.
|
||||
* Still requires explicit values for most fields.
|
||||
- No compilation
|
||||
* Uses `numpy` for speed, since it's commonly available / pre-built.
|
||||
* Building this library should not require a compiler.
|
||||
|
||||
`klamath` was built to provide a fast and versatile GDS interface for
|
||||
[masque](https://mpxd.net/code/jan/masque), which provides higher-level
|
||||
tools for working with hierarchical design data and supports multiple
|
||||
file formats.
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
from . import basic
|
||||
from . import record
|
||||
from . import records
|
||||
from . import elements
|
||||
from . import library
|
||||
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f:
|
||||
__version__ = f.read().strip()
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,174 @@
|
||||
"""
|
||||
Functionality for encoding/decoding basic datatypes
|
||||
"""
|
||||
from typing import Sequence, BinaryIO, List
|
||||
import struct
|
||||
from datetime import datetime
|
||||
|
||||
import numpy # type: ignore
|
||||
|
||||
|
||||
class KlamathError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
Parse functions
|
||||
"""
|
||||
def parse_bitarray(data: bytes) -> int:
|
||||
if len(data) != 2:
|
||||
raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.')
|
||||
(val,) = struct.unpack('>H', data)
|
||||
return val
|
||||
|
||||
|
||||
def parse_int2(data: bytes) -> numpy.ndarray:
|
||||
data_len = len(data)
|
||||
if data_len == 0 or (data_len % 2) != 0:
|
||||
raise KlamathError(f'Incorrect int2 size ({len(data)}). Data is {data!r}.')
|
||||
return numpy.frombuffer(data, dtype='>i2', count=data_len // 2)
|
||||
|
||||
|
||||
def parse_int4(data: bytes) -> numpy.ndarray:
|
||||
data_len = len(data)
|
||||
if data_len == 0 or (data_len % 4) != 0:
|
||||
raise KlamathError(f'Incorrect int4 size ({len(data)}). Data is {data!r}.')
|
||||
return numpy.frombuffer(data, dtype='>i4', count=data_len // 4)
|
||||
|
||||
|
||||
def decode_real8(nums: numpy.ndarray) -> numpy.ndarray:
|
||||
""" Convert GDS REAL8 data to IEEE float64. """
|
||||
nums = nums.astype(numpy.uint64)
|
||||
neg = nums & 0x8000_0000_0000_0000
|
||||
exp = (nums >> 56) & 0x7f
|
||||
mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64)
|
||||
mant[neg != 0] *= -1
|
||||
return numpy.ldexp(mant, 4 * (exp - 64) - 56, dtype=numpy.float64)
|
||||
|
||||
|
||||
def parse_real8(data: bytes) -> numpy.ndarray:
|
||||
data_len = len(data)
|
||||
if data_len == 0 or (data_len % 8) != 0:
|
||||
raise KlamathError(f'Incorrect real8 size ({len(data)}). Data is {data!r}.')
|
||||
ints = numpy.frombuffer(data, dtype='>u8', count=data_len // 8)
|
||||
return decode_real8(ints)
|
||||
|
||||
|
||||
def parse_ascii(data: bytes) -> bytes:
|
||||
if len(data) == 0:
|
||||
raise KlamathError(f'Received empty ascii data.')
|
||||
if data[-1:] == b'\0':
|
||||
return data[:-1]
|
||||
return data
|
||||
|
||||
|
||||
def parse_datetime(data: bytes) -> List[datetime]:
|
||||
""" Parse date/time data (12 byte blocks) """
|
||||
if len(data) == 0 or len(data) % 12 != 0:
|
||||
raise KlamathError(f'Incorrect datetime size ({len(data)}). Data is {data!r}.')
|
||||
dts = []
|
||||
for ii in range(0, len(data), 12):
|
||||
year, *date_parts = parse_int2(data[ii:ii+12])
|
||||
dts.append(datetime(year + 1900, *date_parts))
|
||||
return dts
|
||||
|
||||
|
||||
"""
|
||||
Pack functions
|
||||
"""
|
||||
def pack_bitarray(data: int) -> bytes:
|
||||
if data > 65535 or data < 0:
|
||||
raise KlamathError(f'bitarray data out of range: {data}')
|
||||
return struct.pack('>H', data)
|
||||
|
||||
|
||||
def pack_int2(data: Sequence[int]) -> bytes:
|
||||
arr = numpy.array(data)
|
||||
if (arr > 32767).any() or (arr < -32768).any():
|
||||
raise KlamathError(f'int2 data out of range: {arr}')
|
||||
return arr.astype('>i2').tobytes()
|
||||
|
||||
|
||||
def pack_int4(data: Sequence[int]) -> bytes:
|
||||
arr = numpy.array(data)
|
||||
if (arr > 2147483647).any() or (arr < -2147483648).any():
|
||||
raise KlamathError(f'int4 data out of range: {arr}')
|
||||
return arr.astype('>i4').tobytes()
|
||||
|
||||
|
||||
def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray:
|
||||
""" Convert from float64 to GDS REAL8 representation. """
|
||||
# Split the bitfields
|
||||
ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64))
|
||||
sign = ieee & numpy.uint64(0x8000_0000_0000_0000)
|
||||
ieee_exp = (ieee >> numpy.uint64(52)).astype(numpy.int32) & numpy.int32(0x7ff)
|
||||
ieee_mant = ieee & numpy.uint64(0xf_ffff_ffff_ffff)
|
||||
|
||||
subnorm = (ieee_exp == 0) & (ieee_mant != 0)
|
||||
zero = (ieee_exp == 0) & (ieee_mant == 0)
|
||||
|
||||
# Convert exponent.
|
||||
# * 16-based
|
||||
# * +1 due to mantissa differences (1.xxxx in IEEE vs 0.1xxxxx in GDSII)
|
||||
exp16, rest = numpy.divmod(ieee_exp + 1 - 1023, 4)
|
||||
# Compensate exponent conversion
|
||||
comp = (rest != 0)
|
||||
exp16[comp] += 1
|
||||
|
||||
shift = rest.copy().astype(numpy.int8)
|
||||
shift[comp] = 4 - rest[comp]
|
||||
shift -= 3 # account for gds bit position
|
||||
|
||||
# add leading one
|
||||
gds_mant_unshifted = ieee_mant + 0x10_0000_0000_0000
|
||||
|
||||
rshift = (shift > 0)
|
||||
gds_mant = numpy.empty_like(ieee_mant)
|
||||
gds_mant[~rshift] = gds_mant_unshifted[~rshift] << (-shift[~rshift]).astype(numpy.uint8)
|
||||
gds_mant[ rshift] = gds_mant_unshifted[ rshift] >> ( shift[ rshift]).astype(numpy.uint8)
|
||||
|
||||
# add gds exponent bias
|
||||
exp16_biased = exp16 + 64
|
||||
|
||||
neg_biased = (exp16_biased < 0)
|
||||
gds_mant[neg_biased] >>= (exp16_biased[neg_biased] * 4).astype(numpy.uint8)
|
||||
exp16_biased[neg_biased] = 0
|
||||
|
||||
too_big = (exp16_biased > 0x7f) & ~(zero | subnorm)
|
||||
if too_big.any():
|
||||
raise KlamathError(f'Number(s) too big for real8 format: {fnums[too_big]}')
|
||||
|
||||
gds_exp = exp16_biased.astype(numpy.uint64) << 56
|
||||
|
||||
real8 = sign | gds_exp | gds_mant
|
||||
real8[zero] = 0
|
||||
real8[subnorm] = 0 # TODO handle subnormals
|
||||
real8[exp16_biased < -14] = 0 # number is too small
|
||||
|
||||
return real8
|
||||
|
||||
|
||||
def pack_real8(data: Sequence[float]) -> bytes:
|
||||
return encode_real8(numpy.array(data)).astype('>u8').tobytes()
|
||||
|
||||
|
||||
def pack_ascii(data: bytes) -> bytes:
|
||||
size = len(data)
|
||||
if size % 2 != 0:
|
||||
return data + b'\0'
|
||||
return data
|
||||
|
||||
|
||||
def pack_datetime(data: Sequence[datetime]) -> bytes:
|
||||
""" Pack date/time data (12 byte blocks) """
|
||||
parts = sum(((d.year - 1900, d.month, d.day, d.hour, d.minute, d.second)
|
||||
for d in data), start=())
|
||||
return pack_int2(parts)
|
||||
|
||||
|
||||
def read(stream: BinaryIO, size: int) -> bytes:
|
||||
""" Read and check for failure """
|
||||
data = stream.read(size)
|
||||
if len(data) != size:
|
||||
raise EOFError
|
||||
return data
|
@ -0,0 +1,489 @@
|
||||
"""
|
||||
Functionality for reading/writing elements (geometry, text labels,
|
||||
structure references) and associated properties.
|
||||
"""
|
||||
from typing import Dict, Tuple, Optional, BinaryIO, TypeVar, Type
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy # type: ignore
|
||||
|
||||
from .basic import KlamathError
|
||||
from .record import Record
|
||||
|
||||
from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
|
||||
from .records import DATATYPE, PATHTYPE, BOXTYPE, NODETYPE, TEXTTYPE
|
||||
from .records import LAYER, XY, WIDTH, COLROW, PRESENTATION, STRING
|
||||
from .records import STRANS, MAG, ANGLE, PROPATTR, PROPVALUE
|
||||
from .records import ENDEL, BGNEXTN, ENDEXTN, SNAME
|
||||
|
||||
|
||||
E = TypeVar('E', bound='Element')
|
||||
|
||||
R = TypeVar('R', bound='Reference')
|
||||
B = TypeVar('B', bound='Boundary')
|
||||
P = TypeVar('P', bound='Path')
|
||||
N = TypeVar('N', bound='Node')
|
||||
T = TypeVar('T', bound='Text')
|
||||
X = TypeVar('X', bound='Box')
|
||||
|
||||
|
||||
|
||||
def read_properties(stream: BinaryIO) -> Dict[int, bytes]:
|
||||
"""
|
||||
Read element properties.
|
||||
|
||||
Assumes PROPATTR records have unique values.
|
||||
Stops reading after consuming ENDEL record.
|
||||
|
||||
Args:
|
||||
stream: Stream to read from.
|
||||
|
||||
Returns:
|
||||
{propattr: b'propvalue'} mapping.
|
||||
"""
|
||||
properties = {}
|
||||
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != ENDEL.tag:
|
||||
if tag == PROPATTR.tag:
|
||||
key = PROPATTR.read_data(stream, size)[0]
|
||||
value = PROPVALUE.read(stream)
|
||||
if key in properties:
|
||||
raise KlamathError(f'Duplicate property key: {key!r}')
|
||||
properties[key] = value
|
||||
size, tag = Record.read_header(stream)
|
||||
return properties
|
||||
|
||||
|
||||
def write_properties(stream: BinaryIO, properties: Dict[int, bytes]) -> int:
|
||||
"""
|
||||
Write element properties.
|
||||
|
||||
This is does _not_ write the ENDEL record.
|
||||
|
||||
Args:
|
||||
stream: Stream to write to.
|
||||
"""
|
||||
b = 0
|
||||
for key, value in properties.items():
|
||||
b += PROPATTR.write(stream, key)
|
||||
b += PROPVALUE.write(stream, value)
|
||||
return b
|
||||
|
||||
|
||||
class Element(metaclass=ABCMeta):
|
||||
"""
|
||||
Abstract method definition for GDS structure contents
|
||||
"""
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def read(cls: Type[E], stream: BinaryIO) -> E:
|
||||
"""
|
||||
Read from a stream to construct this object.
|
||||
Consumes up to (and including) the ENDEL record.
|
||||
|
||||
Args:
|
||||
Stream to read from.
|
||||
|
||||
Returns:
|
||||
Constructed object.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
"""
|
||||
Write this element to a stream.
|
||||
Finishes with an ENDEL record.
|
||||
|
||||
Args:
|
||||
Stream to write to.
|
||||
|
||||
Returns:
|
||||
Number of bytes written
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Reference(Element):
|
||||
"""
|
||||
Datastructure representing
|
||||
an instance of a structure (SREF / structure reference) or
|
||||
an array of instances (AREF / array reference).
|
||||
Type is determined by the presence of the `colrow` tuple.
|
||||
|
||||
Transforms are applied to each individual instance (_not_
|
||||
to the instance's origin location or array vectors).
|
||||
"""
|
||||
__slots__ = ('struct_name', 'invert_y', 'mag', 'angle_deg', 'xy', 'colrow', 'properties')
|
||||
|
||||
struct_name: bytes
|
||||
""" Name of the structure being referenced. """
|
||||
|
||||
invert_y: bool
|
||||
""" Whether to mirror the pattern (negate y-values / flip across x-axis). Default False. """
|
||||
|
||||
mag: float
|
||||
""" Scaling factor (default 1) """
|
||||
|
||||
angle_deg: float
|
||||
""" Rotation (degrees counterclockwise) """
|
||||
|
||||
xy: numpy.ndarray
|
||||
"""
|
||||
(For SREF) Location in the parent structure corresponding to the instance's origin (0, 0).
|
||||
(For AREF) 3 locations:
|
||||
[`offset`,
|
||||
`offset + col_basis_vector * colrow[0]`,
|
||||
`offset + row_basis_vector * colrow[1]`]
|
||||
which define the first instance's offset and the array's basis vectors.
|
||||
Note that many GDS implementations only support manhattan basis vectors, and some
|
||||
assume a certain axis mapping (e.g. x->columns, y->rows) and "reinterpret" the
|
||||
basis vectors to match it.
|
||||
"""
|
||||
|
||||
colrow: Optional[Tuple[int, int]]
|
||||
""" Number of columns and rows (AREF) or None (SREF) """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties associated with this reference. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[R], stream: BinaryIO) -> R:
|
||||
invert_y = False
|
||||
mag = 1
|
||||
angle_deg = 0
|
||||
colrow = None
|
||||
struct_name = SNAME.skip_and_read(stream)
|
||||
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != XY.tag:
|
||||
if tag == STRANS.tag:
|
||||
strans = STRANS.read_data(stream, size)
|
||||
invert_y = bool(0x8000 & strans)
|
||||
elif tag == MAG.tag:
|
||||
mag = MAG.read_data(stream, size)[0]
|
||||
elif tag == ANGLE.tag:
|
||||
angle_deg = ANGLE.read_data(stream, size)[0]
|
||||
elif tag == COLROW.tag:
|
||||
colrow = COLROW.read_data(stream, size)
|
||||
else:
|
||||
raise KlamathError(f'Unexpected tag {tag:04x}')
|
||||
size, tag = Record.read_header(stream)
|
||||
xy = XY.read_data(stream, size).reshape(-1, 2)
|
||||
properties = read_properties(stream)
|
||||
return cls(struct_name=struct_name, xy=xy, properties=properties, colrow=colrow,
|
||||
invert_y=invert_y, mag=mag, angle_deg=angle_deg)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = 0
|
||||
if self.colrow is None:
|
||||
b += SREF.write(stream, None)
|
||||
else:
|
||||
b += AREF.write(stream, None)
|
||||
|
||||
b += SNAME.write(stream, self.struct_name)
|
||||
if self.angle_deg != 0 or self.mag != 1 or self.invert_y:
|
||||
b += STRANS.write(stream, int(self.invert_y) << 15)
|
||||
if self.mag != 1:
|
||||
b += MAG.write(stream, self.mag)
|
||||
if self.angle_deg !=0:
|
||||
b += ANGLE.write(stream, self.angle_deg)
|
||||
|
||||
if self.colrow is not None:
|
||||
b += COLROW.write(stream, self.colrow)
|
||||
|
||||
b += XY.write(stream, self.xy)
|
||||
b += write_properties(stream, self.properties)
|
||||
b += ENDEL.write(stream, None)
|
||||
return b
|
||||
|
||||
def check(self) -> None:
|
||||
if self.colrow is not None:
|
||||
if self.xy.size != 6:
|
||||
raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}')
|
||||
else:
|
||||
if self.xy.size != 2:
|
||||
raise KlamathError(f'Expected size-2 xy. Got {self.xy}')
|
||||
|
||||
|
||||
@dataclass
|
||||
class Boundary(Element):
|
||||
"""
|
||||
Datastructure representing a Boundary element.
|
||||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties')
|
||||
|
||||
layer: Tuple[int, int]
|
||||
""" (layer, data_type) tuple """
|
||||
|
||||
xy: numpy.ndarray
|
||||
""" Ordered vertices of the shape. First and last points should be identical. """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[B], stream: BinaryIO) -> B:
|
||||
layer = LAYER.skip_and_read(stream)[0]
|
||||
dtype = DATATYPE.read(stream)[0]
|
||||
xy = XY.read(stream).reshape(-1, 2)
|
||||
properties = read_properties(stream)
|
||||
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = BOUNDARY.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += DATATYPE.write(stream, self.layer[1])
|
||||
b += XY.write(stream, self.xy)
|
||||
b += write_properties(stream, self.properties)
|
||||
b += ENDEL.write(stream, None)
|
||||
return b
|
||||
|
||||
|
||||
@dataclass
|
||||
class Path(Element):
|
||||
"""
|
||||
Datastructure representing a Path element.
|
||||
|
||||
If `path_type < 4`, `extension` values are not written.
|
||||
During read, `exension` defaults to (0, 0) even if unused.
|
||||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension')
|
||||
|
||||
layer: Tuple[int, int]
|
||||
""" (layer, data_type) tuple """
|
||||
|
||||
path_type: int
|
||||
""" End-cap type (0: flush, 1: circle, 2: square, 4: custom) """
|
||||
|
||||
width: int
|
||||
""" Path width """
|
||||
|
||||
extension: Tuple[int, int]
|
||||
""" Extension when using path_type=4. Ignored otherwise. """
|
||||
|
||||
xy: numpy.ndarray
|
||||
""" Path centerline coordinates """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[P], stream: BinaryIO) -> P:
|
||||
path_type = 0
|
||||
width = 0
|
||||
bgn_ext = 0
|
||||
end_ext = 0
|
||||
layer = LAYER.skip_and_read(stream)[0]
|
||||
dtype = DATATYPE.read(stream)[0]
|
||||
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != XY.tag:
|
||||
if tag == PATHTYPE.tag:
|
||||
path_type = PATHTYPE.read_data(stream, size)[0]
|
||||
elif tag == WIDTH.tag:
|
||||
width = WIDTH.read_data(stream, size)[0]
|
||||
elif tag == BGNEXTN.tag:
|
||||
bgn_ext = BGNEXTN.read_data(stream, size)[0]
|
||||
elif tag == ENDEXTN.tag:
|
||||
end_ext = ENDEXTN.read_data(stream, size)[0]
|
||||
else:
|
||||
raise KlamathError(f'Unexpected tag {tag:04x}')
|
||||
size, tag = Record.read_header(stream)
|
||||
xy = XY.read_data(stream, size).reshape(-1, 2)
|
||||
properties = read_properties(stream)
|
||||
return cls(layer=(layer, dtype), xy=xy,
|
||||
properties=properties, extension=(bgn_ext, end_ext),
|
||||
path_type=path_type, width=width)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = PATH.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += DATATYPE.write(stream, self.layer[1])
|
||||
if self.path_type != 0:
|
||||
b += PATHTYPE.write(stream, self.path_type)
|
||||
if self.width != 0:
|
||||
b += WIDTH.write(stream, self.width)
|
||||
|
||||
if self.path_type < 4:
|
||||
bgn_ext, end_ext = self.extension
|
||||
if bgn_ext != 0:
|
||||
b += BGNEXTN.write(stream, bgn_ext)
|
||||
if end_ext != 0:
|
||||
b += ENDEXTN.write(stream, end_ext)
|
||||
b += XY.write(stream, self.xy)
|
||||
b += write_properties(stream, self.properties)
|
||||
b += ENDEL.write(stream, None)
|
||||
return b
|
||||
|
||||
|
||||
@dataclass
|
||||
class Box(Element):
|
||||
"""
|
||||
Datastructure representing a Box element. Rarely used.
|
||||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties')
|
||||
|
||||
layer: Tuple[int, int]
|
||||
""" (layer, box_type) tuple """
|
||||
|
||||
xy: numpy.ndarray
|
||||
""" Box coordinates (5 pairs) """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[X], stream: BinaryIO) -> X:
|
||||
layer = LAYER.skip_and_read(stream)
|
||||
dtype = BOXTYPE.read(stream)
|
||||
xy = XY.read(stream).reshape(-1, 2)
|
||||
properties = read_properties(stream)
|
||||
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = BOX.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += BOXTYPE.write(stream, self.layer[1])
|
||||
b += XY.write(stream, self.xy)
|
||||
b += write_properties(stream, self.properties)
|
||||
b += ENDEL.write(stream, None)
|
||||
return b
|
||||
|
||||
|
||||
@dataclass
|
||||
class Node(Element):
|
||||
"""
|
||||
Datastructure representing a Node element. Rarely used.
|
||||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties')
|
||||
|
||||
layer: Tuple[int, int]
|
||||
""" (layer, node_type) tuple """
|
||||
|
||||
xy: numpy.ndarray
|
||||
""" 1-50 pairs of coordinates. """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[N], stream: BinaryIO) -> N:
|
||||
layer = LAYER.skip_and_read(stream)
|
||||
dtype = NODETYPE.read(stream)
|
||||
xy = XY.read(stream).reshape(-1, 2)
|
||||
properties = read_properties(stream)
|
||||
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = NODE.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += NODETYPE.write(stream, self.layer[1])
|
||||
b += XY.write(stream, self.xy)
|
||||
b += write_properties(stream, self.properties)
|
||||
b += ENDEL.write(stream, None)
|
||||
return b
|
||||
|
||||
|
||||
@dataclass
|
||||
class Text(Element):
|
||||
"""
|
||||
Datastructure representing a Node element. Rarely used.
|
||||
"""
|
||||
__slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type',
|
||||
'width', 'invert_y', 'mag', 'angle_deg', 'string')
|
||||
|
||||
layer: Tuple[int, int]
|
||||
""" (layer, node_type) tuple """
|
||||
|
||||
presentation: int
|
||||
""" Bit array. Default all zeros.
|
||||
bits 0-1: 00 left/01 center/10 right
|
||||
bits 2-3: 00 top/01 middle/10 bottom
|
||||
bits 4-5: font number
|
||||
"""
|
||||
|
||||
path_type: int
|
||||
""" Default 0 """
|
||||
|
||||
width: int
|
||||
""" Default 0 """
|
||||
|
||||
invert_y: bool
|
||||
""" Vertical inversion. Default False. """
|
||||
|
||||
mag: float
|
||||
""" Scaling factor. Default 1. """
|
||||
|
||||
angle_deg: float
|
||||
""" Rotation (ccw). Default 0. """
|
||||
|
||||
xy: numpy.ndarray
|
||||
""" Position (1 pair only) """
|
||||
|
||||
string: bytes
|
||||
""" Text content """
|
||||
|
||||
properties: Dict[int, bytes]
|
||||
""" Properties for the element. """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[T], stream: BinaryIO) -> T:
|
||||
path_type = 0
|
||||
presentation = 0
|
||||
invert_y = False
|
||||
width = 0
|
||||
mag = 1
|
||||
angle_deg = 0
|
||||
layer = LAYER.skip_and_read(stream)
|
||||
dtype = TEXTTYPE.read(stream)
|
||||
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != XY.tag:
|
||||
if tag == PRESENTATION.tag:
|
||||
presentation = PRESENTATION.read_data(stream, size)
|
||||
elif tag == PATHTYPE.tag:
|
||||
path_type = PATHTYPE.read_data(stream, size)[0]
|
||||
elif tag == WIDTH.tag:
|
||||
width = WIDTH.read_data(stream, size)[0]
|
||||
elif tag == STRANS.tag:
|
||||
strans = STRANS.read_data(stream, size)
|
||||
invert_y = bool(0x8000 & strans)
|
||||
elif tag == MAG.tag:
|
||||
mag = MAG.read_data(stream, size)[0]
|
||||
elif tag == ANGLE.tag:
|
||||
angle_deg = ANGLE.read_data(stream, size)[0]
|
||||
else:
|
||||
raise KlamathError(f'Unexpected tag {tag:04x}')
|
||||
size, tag = Record.read_header(stream)
|
||||
xy = XY.read_data(stream, size).reshape(-1, 2)
|
||||
|
||||
string = STRING.read(stream)
|
||||
properties = read_properties(stream)
|
||||
return cls(layer=(layer, dtype), xy=xy, properties=properties,
|
||||
string=string, presentation=presentation, path_type=path_type,
|
||||
width=width, invert_y=invert_y, mag=mag, angle_deg=angle_deg)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
b = TEXT.write(stream, None)
|
||||
b += LAYER.write(stream, self.layer[0])
|
||||
b += TEXTTYPE.write(stream, self.layer[1])
|
||||
if self.presentation != 0:
|
||||
b += PRESENTATION.write(stream, self.presentation)
|
||||
if self.path_type != 0:
|
||||
b += PATHTYPE.write(stream, self.path_type)
|
||||
if self.width != 0:
|
||||
b += WIDTH.write(stream, self.width)
|
||||
if self.angle_deg != 0 or self.mag != 1 or self.invert_y:
|
||||
b += STRANS.write(stream, int(self.invert_y) << 15)
|
||||
if self.mag != 1:
|
||||
b += MAG.write(stream, self.mag)
|
||||
if self.angle_deg !=0:
|
||||
b += ANGLE.write(stream, self.angle_deg)
|
||||
b += XY.write(stream, self.xy)
|
||||
b += write_properties(stream, self.properties)
|
||||
b += ENDEL.write(stream, None)
|
||||
return b
|
@ -0,0 +1,187 @@
|
||||
"""
|
||||
File-level read/write functionality.
|
||||
"""
|
||||
from typing import List, Dict, Tuple, Optional, BinaryIO, TypeVar, Type
|
||||
import io
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .basic import KlamathError
|
||||
from .record import Record
|
||||
|
||||
from .records import HEADER, BGNLIB, ENDLIB, UNITS, LIBNAME
|
||||
from .records import BGNSTR, STRNAME, ENDSTR
|
||||
from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
|
||||
from .elements import Element, Reference, Text, Box, Boundary, Path, Node
|
||||
|
||||
|
||||
FH = TypeVar('FH', bound='FileHeader')
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileHeader:
|
||||
"""
|
||||
Representation of the GDS file header.
|
||||
|
||||
File header records: HEADER BGNLIB LIBNAME UNITS
|
||||
Optional record are ignored if present and never written.
|
||||
|
||||
Version is assumed to be `600`.
|
||||
"""
|
||||
name: bytes
|
||||
""" Library name """
|
||||
|
||||
user_units_per_db_unit: float
|
||||
""" Number of user units in one database unit """
|
||||
|
||||
meters_per_db_unit: float
|
||||
""" Number of meters in one database unit """
|
||||
|
||||
mod_time: datetime = datetime(1900, 1, 1)
|
||||
""" Last-modified time """
|
||||
|
||||
acc_time: datetime = datetime(1900, 1, 1)
|
||||
""" Last-accessed time """
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[FH], stream: BinaryIO) -> FH:
|
||||
"""
|
||||
Read and construct a header from the provided stream.
|
||||
|
||||
Args:
|
||||
stream: Seekable stream to read from
|
||||
|
||||
Returns:
|
||||
FileHeader object
|
||||
"""
|
||||
version = HEADER.read(stream)[0]
|
||||
if version != 600:
|
||||
raise KlamathError(f'Got GDS version {version}, expected 600')
|
||||
mod_time, acc_time = BGNLIB.read(stream)
|
||||
name = LIBNAME.skip_and_read(stream)
|
||||
uu, dbu = UNITS.skip_and_read(stream)
|
||||
|
||||
return cls(mod_time=mod_time, acc_time=acc_time, name=name,
|
||||
user_units_per_db_unit=uu, meters_per_db_unit=dbu)
|
||||
|
||||
def write(self, stream: BinaryIO) -> int:
|
||||
"""
|
||||
Write the header to a stream
|
||||
|
||||
Args:
|
||||
stream: Stream to write to
|
||||
|
||||
Returns:
|
||||
number of bytes written
|
||||
"""
|
||||
b = HEADER.write(stream, 600)
|
||||
b += BGNLIB.write(stream, (self.mod_time, self.acc_time))
|
||||
b += LIBNAME.write(stream, self.name)
|
||||
b += UNITS.write(stream, (self.user_units_per_db_unit, self.meters_per_db_unit))
|
||||
return b
|
||||
|
||||
|
||||
def scan_structs(stream: BinaryIO) -> Dict[bytes, int]:
|
||||
"""
|
||||
Scan through a GDS file, building a table of
|
||||
{b'structure_name': byte_offset}.
|
||||
The intent of this function is to enable random access
|
||||
and/or partial (structure-by-structure) reads.
|
||||
|
||||
Args:
|
||||
stream: Seekable stream to read from. Should be positioned
|
||||
before the first structure record, but possibly
|
||||
already past the file header.
|
||||
"""
|
||||
positions = {}
|
||||
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != ENDLIB.tag:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
if tag == BGNSTR.tag:
|
||||
name = STRNAME.read(stream)
|
||||
if name in positions:
|
||||
raise KlamathError(f'Duplicate structure name: {name!r}')
|
||||
positions[name] = stream.tell()
|
||||
size, tag = Record.read_header(stream)
|
||||
|
||||
return positions
|
||||
|
||||
|
||||
def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]:
|
||||
"""
|
||||
Skip to the next structure and attempt to read it.
|
||||
|
||||
Args:
|
||||
stream: Seekable stream to read from.
|
||||
|
||||
Returns:
|
||||
(name, elements) if a structure was found.
|
||||
None if no structure was found before the end of the library.
|
||||
"""
|
||||
if not BGNSTR.skip_past(stream):
|
||||
return None
|
||||
name = STRNAME.read(stream)
|
||||
elements = read_elements(stream)
|
||||
return name, elements
|
||||
|
||||
|
||||
def write_struct(stream: BinaryIO,
|
||||
name: bytes,
|
||||
elements: List[Element],
|
||||
cre_time: datetime = datetime(1900, 1, 1),
|
||||
mod_time: datetime = datetime(1900, 1, 1),
|
||||
) -> int:
|
||||
"""
|
||||
Write a structure to the provided stream.
|
||||
|
||||
Args:
|
||||
name: Structure name (ascii-encoded).
|
||||
elements: List of Elements containing the geometry and text in this struct.
|
||||
cre_time: Creation time (optional).
|
||||
mod_time: Modification time (optional).
|
||||
|
||||
Return:
|
||||
Number of bytes written
|
||||
"""
|
||||
b = BGNSTR.write(stream, (cre_time, mod_time))
|
||||
b += STRNAME.write(stream, name)
|
||||
b += sum(el.write(stream) for el in elements)
|
||||
b += ENDSTR.write(stream, None)
|
||||
return b
|
||||
|
||||
|
||||
def read_elements(stream: BinaryIO) -> List[Element]:
|
||||
"""
|
||||
Read elements from the stream until an ENDSTR
|
||||
record is encountered. The ENDSTR record is also
|
||||
consumed.
|
||||
|
||||
Args:
|
||||
stream: Seekable stream to read from.
|
||||
|
||||
Returns:
|
||||
List of element objects.
|
||||
"""
|
||||
data: List[Element] = []
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != ENDSTR.tag:
|
||||
if tag == BOUNDARY.tag:
|
||||
data.append(Boundary.read(stream))
|
||||
elif tag == PATH.tag:
|
||||
data.append(Path.read(stream))
|
||||
elif tag == NODE.tag:
|
||||
data.append(Node.read(stream))
|
||||
elif tag == BOX.tag:
|
||||
data.append(Box.read(stream))
|
||||
elif tag == TEXT.tag:
|
||||
data.append(Text.read(stream))
|
||||
elif tag == SREF.tag:
|
||||
data.append(Reference.read(stream))
|
||||
elif tag == AREF.tag:
|
||||
data.append(Reference.read(stream))
|
||||
else:
|
||||
# don't care, skip
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
size, tag = Record.read_header(stream)
|
||||
return data
|
@ -0,0 +1,208 @@
|
||||
"""
|
||||
Generic record-level read/write functionality.
|
||||
"""
|
||||
from typing import Optional, Sequence, BinaryIO
|
||||
from typing import TypeVar, List, Tuple, ClassVar, Type
|
||||
import struct
|
||||
import io
|
||||
from datetime import datetime
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy # type: ignore
|
||||
|
||||
from .basic import KlamathError
|
||||
from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray
|
||||
from .basic import pack_int2, pack_int4, pack_real8, pack_datetime, pack_bitarray
|
||||
from .basic import parse_ascii, pack_ascii, read
|
||||
|
||||
|
||||
_RECORD_HEADER_FMT = struct.Struct('>HH')
|
||||
|
||||
|
||||
def write_record_header(stream: BinaryIO, data_size: int, tag: int) -> int:
|
||||
record_size = data_size + 4
|
||||
if record_size > 0xFFFF:
|
||||
raise KlamathError(f'Record size is too big: {record_size}')
|
||||
header = _RECORD_HEADER_FMT.pack(record_size, tag)
|
||||
return stream.write(header)
|
||||
|
||||
|
||||
def read_record_header(stream: BinaryIO) -> Tuple[int, int]:
|
||||
"""
|
||||
Read a record's header (size and tag).
|
||||
Args:
|
||||
stream: stream to read from
|
||||
Returns:
|
||||
data_size: size of data (not including header)
|
||||
tag: Record type tag
|
||||
"""
|
||||
header = read(stream, 4)
|
||||
record_size, tag = _RECORD_HEADER_FMT.unpack(header)
|
||||
if record_size < 4:
|
||||
raise KlamathError(f'Record size is too small: {record_size} @ pos 0x{stream.tell():x}')
|
||||
if record_size % 2:
|
||||
raise KlamathError(f'Record size is odd: {record_size} @ pos 0x{stream.tell():x}')
|
||||
data_size = record_size - 4 # substract header size
|
||||
return data_size, tag
|
||||
|
||||
|
||||
def expect_record(stream: BinaryIO, tag: int) -> int:
|
||||
data_size, actual_tag = read_record_header(stream)
|
||||
if tag != actual_tag:
|
||||
raise KlamathError(f'Unexpected record! Got tag {actual_tag:04x}, expected {tag:04x}')
|
||||
return data_size
|
||||
|
||||
|
||||
R = TypeVar('R', bound='Record')
|
||||
|
||||
|
||||
class Record(metaclass=ABCMeta):
|
||||
tag: ClassVar[int] = -1
|
||||
expected_size: ClassVar[Optional[int]] = None
|
||||
|
||||
@classmethod
|
||||
def check_size(cls, size: int):
|
||||
if cls.expected_size is not None and size != cls.expected_size:
|
||||
raise KlamathError(f'Expected size {cls.expected_size}, got {size}')
|
||||
|
||||
@classmethod
|
||||
def check_data(cls, data):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def pack_data(cls, data) -> bytes:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def read_header(stream: BinaryIO) -> Tuple[int, int]:
|
||||
return read_record_header(stream)
|
||||
|
||||
@classmethod
|
||||
def write_header(cls, stream: BinaryIO, data_size: int) -> int:
|
||||
return write_record_header(stream, data_size, cls.tag)
|
||||
|
||||
@classmethod
|
||||
def skip_past(cls, stream: BinaryIO) -> bool:
|
||||
"""
|
||||
Skip to the end of the next occurence of this record.
|
||||
|
||||
Args:
|
||||
stream: Seekable stream to read from.
|
||||
|
||||
Return:
|
||||
True if the record was encountered and skipped.
|
||||
False if the end of the library was reached.
|
||||
"""
|
||||
from .records import ENDLIB
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != cls.tag:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
if tag == ENDLIB.tag:
|
||||
return False
|
||||
size, tag = Record.read_header(stream)
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def skip_and_read(cls, stream: BinaryIO):
|
||||
size, tag = Record.read_header(stream)
|
||||
while tag != cls.tag:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
size, tag = Record.read_header(stream)
|
||||
data = cls.read_data(stream, size)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def read(cls: Type[R], stream: BinaryIO):
|
||||
size = expect_record(stream, cls.tag)
|
||||
data = cls.read_data(stream, size)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def write(cls, stream: BinaryIO, data) -> int:
|
||||
data_bytes = cls.pack_data(data)
|
||||
b = cls.write_header(stream, len(data_bytes))
|
||||
b += stream.write(data_bytes)
|
||||
return b
|
||||
|
||||
|
||||
class NoDataRecord(Record):
|
||||
expected_size: ClassVar[Optional[int]] = 0
|
||||
|
||||
@classmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> None:
|
||||
stream.read(size)
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: None) -> bytes:
|
||||
if data is not None:
|
||||
raise KlamathError('?? Packing {data} into NoDataRecord??')
|
||||
return b''
|
||||
|
||||
|
||||
class BitArrayRecord(Record):
|
||||
expected_size: ClassVar[Optional[int]] = 2
|
||||
|
||||
@classmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> int:
|
||||
return parse_bitarray(read(stream, 2))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: int) -> bytes:
|
||||
return pack_bitarray(data)
|
||||
|
||||
|
||||
class Int2Record(Record):
|
||||
@classmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
|
||||
return parse_int2(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
||||
return pack_int2(data)
|
||||
|
||||
|
||||
class Int4Record(Record):
|
||||
@classmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
|
||||
return parse_int4(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
||||
return pack_int4(data)
|
||||
|
||||
|
||||
class Real8Record(Record):
|
||||
@classmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
|
||||
return parse_real8(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
||||
return pack_real8(data)
|
||||
|
||||
|
||||
class ASCIIRecord(Record):
|
||||
@classmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> bytes:
|
||||
return parse_ascii(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: bytes) -> bytes:
|
||||
return pack_ascii(data)
|
||||
|
||||
|
||||
class DateTimeRecord(Record):
|
||||
@classmethod
|
||||
def read_data(cls, stream: BinaryIO, size: int) -> List[datetime]:
|
||||
return parse_datetime(read(stream, size))
|
||||
|
||||
@classmethod
|
||||
def pack_data(cls, data: Sequence[datetime]) -> bytes:
|
||||
return pack_datetime(data)
|
@ -0,0 +1,333 @@
|
||||
"""
|
||||
Record type and tag definitions
|
||||
"""
|
||||
from typing import Sequence
|
||||
|
||||
from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record
|
||||
from .record import ASCIIRecord, DateTimeRecord
|
||||
|
||||
|
||||
class HEADER(Int2Record):
|
||||
tag = 0x0002
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class BGNLIB(DateTimeRecord):
|
||||
tag = 0x0102
|
||||
expected_size = 6 * 2
|
||||
|
||||
|
||||
class LIBNAME(ASCIIRecord):
|
||||
tag = 0x0206
|
||||
|
||||
|
||||
class UNITS(Real8Record):
|
||||
""" (user_units_per_db_unit, db_units_per_meter) """
|
||||
tag = 0x0305
|
||||
expected_size = 8 * 2
|
||||
|
||||
|
||||
class ENDLIB(NoDataRecord):
|
||||
tag = 0x0400
|
||||
|
||||
|
||||
class BGNSTR(DateTimeRecord):
|
||||
tag = 0x0502
|
||||
expected_size = 6 * 2
|
||||
|
||||
|
||||
class STRNAME(ASCIIRecord):
|
||||
""" Legal characters are `?A-Za-z0-9_$` """
|
||||
tag = 0x0606
|
||||
|
||||
|
||||
class ENDSTR(NoDataRecord):
|
||||
tag = 0x0700
|
||||
|
||||
|
||||
class BOUNDARY(NoDataRecord):
|
||||
tag = 0x0800
|
||||
|
||||
|
||||
class PATH(NoDataRecord):
|
||||
tag = 0x0900
|
||||
|
||||
|
||||
class SREF(NoDataRecord):
|
||||
tag = 0x0a00
|
||||
|
||||
|
||||
class AREF(NoDataRecord):
|
||||
tag = 0x0b00
|
||||
|
||||
|
||||
class TEXT(NoDataRecord):
|
||||
tag = 0x0c00
|
||||
|
||||
|
||||
class LAYER(Int2Record):
|
||||
tag = 0x0d02
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class DATATYPE(Int2Record):
|
||||
tag = 0x0e02
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class WIDTH(Int4Record):
|
||||
tag = 0x0f03
|
||||
expected_size = 4
|
||||
|
||||
|
||||
class XY(Int4Record):
|
||||
tag = 0x1003
|
||||
|
||||
|
||||
class ENDEL(NoDataRecord):
|
||||
tag = 0x1100
|
||||
|
||||
|
||||
class SNAME(ASCIIRecord):
|
||||
tag = 0x1206
|
||||
|
||||
|
||||
class COLROW(Int2Record):
|
||||
tag = 0x1302
|
||||
expected_size = 4
|
||||
|
||||
|
||||
class NODE(NoDataRecord):
|
||||
tag = 0x1500
|
||||
|
||||
|
||||
class TEXTTYPE(Int2Record):
|
||||
tag = 0x1602
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class PRESENTATION(BitArrayRecord):
|
||||
tag = 0x1701
|
||||
|
||||
|
||||
class SPACING(Int2Record):
|
||||
tag = 0x1802 #Not sure about 02; Unused
|
||||
|
||||
|
||||
class STRING(ASCIIRecord):
|
||||
tag = 0x1906
|
||||
|
||||
|
||||
class STRANS(BitArrayRecord):
|
||||
tag = 0x1a01
|
||||
|
||||
|
||||
class MAG(Real8Record):
|
||||
tag = 0x1b05
|
||||
expected_size = 8
|
||||
|
||||
|
||||
class ANGLE(Real8Record):
|
||||
tag = 0x1c05
|
||||
expected_size = 8
|
||||
|
||||
|
||||
class UINTEGER(Int2Record):
|
||||
tag = 0x1d02 #Unused; not sure about 02
|
||||
|
||||
|
||||
class USTRING(ASCIIRecord):
|
||||
tag = 0x1e06 #Unused; not sure about 06
|
||||
|
||||
|
||||
class REFLIBS(ASCIIRecord):
|
||||
tag = 0x1f06
|
||||
|
||||
@classmethod
|
||||
def check_size(cls, size: int):
|
||||
if size != 0 and size % 44 != 0:
|
||||
raise Exception(f'Expected size to be multiple of 44, got {size}')
|
||||
|
||||
|
||||
class FONTS(ASCIIRecord):
|
||||
tag = 0x2006
|
||||
|
||||
@classmethod
|
||||
def check_size(cls, size: int):
|
||||
if size != 0 and size % 44 != 0:
|
||||
raise Exception(f'Expected size to be multiple of 44, got {size}')
|
||||
|
||||
|
||||
class PATHTYPE(Int2Record):
|
||||
tag = 0x2102
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class GENERATIONS(Int2Record):
|
||||
tag = 0x2202
|
||||
expected_size = 2
|
||||
|
||||
@classmethod
|
||||
def check_data(cls, data: Sequence[int]):
|
||||
if len(data) != 1:
|
||||
raise Exception(f'Expected exactly one integer, got {data}')
|
||||
|
||||
|
||||
class ATTRTABLE(ASCIIRecord):
|
||||
tag = 0x2306
|
||||
|
||||
@classmethod
|
||||
def check_size(cls, size: int):
|
||||
if size > 44:
|
||||
raise Exception(f'Expected size <= 44, got {size}')
|
||||
|
||||
|
||||
class STYPTABLE(ASCIIRecord):
|
||||
tag = 0x2406 #UNUSED, not sure about 06
|
||||
|
||||
|
||||
class STRTYPE(Int2Record):
|
||||
tag = 0x2502 #UNUSED
|
||||
|
||||
|
||||
class ELFLAGS(BitArrayRecord):
|
||||
tag = 0x2601
|
||||
|
||||
|
||||
class ELKEY(Int2Record):
|
||||
tag = 0x2703 # UNUSED
|
||||
|
||||
|
||||
class LINKTYPE(Int2Record):
|
||||
tag = 0x2803 # UNUSED
|
||||
|
||||
|
||||
class LINKKEYS(Int2Record):
|
||||
tag = 0x2903 # UNUSED
|
||||
|
||||
|
||||
class NODETYPE(Int2Record):
|
||||
tag = 0x2a02
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class PROPATTR(Int2Record):
|
||||
tag = 0x2b02
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class PROPVALUE(ASCIIRecord):
|
||||
tag = 0x2c06
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class BOX(NoDataRecord):
|
||||
tag = 0x2d00
|
||||
|
||||
|
||||
class BOXTYPE(Int2Record):
|
||||
tag = 0x2e02
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class PLEX(Int4Record):
|
||||
tag = 0x2f03
|
||||
expected_size = 4
|
||||
|
||||
|
||||
class BGNEXTN(Int4Record):
|
||||
tag = 0x3003
|
||||
|
||||
|
||||
class ENDEXTN(Int4Record):
|
||||
tag = 0x3103
|
||||
|
||||
|
||||
class TAPENUM(Int2Record):
|
||||
tag = 0x3202
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class TAPECODE(Int2Record):
|
||||
tag = 0x3302
|
||||
expected_size = 12
|
||||
|
||||
|
||||
class STRCLASS(Int2Record):
|
||||
tag = 0x3401 # UNUSED
|
||||
|
||||
|
||||
class RESERVED(Int2Record):
|
||||
tag = 0x3503 # UNUSED
|
||||
|
||||
|
||||
class FORMAT(Int2Record):
|
||||
tag = 0x3602
|
||||
expected_size = 2
|
||||
|
||||
@classmethod
|
||||
def check_data(cls, data: Sequence[int]):
|
||||
if len(data) != 1:
|
||||
raise Exception(f'Expected exactly one integer, got {data}')
|
||||
|
||||
|
||||
class MASK(ASCIIRecord):
|
||||
""" List of layers and dtypes """
|
||||
tag = 0x3706
|
||||
|
||||
|
||||
class ENDMASKS(NoDataRecord):
|
||||
""" End of MASKS records """
|
||||
tag = 0x3800
|
||||
|
||||
|
||||
class LIBDIRSIZE(Int2Record):
|
||||
tag = 0x3902
|
||||
|
||||
|
||||
class SRFNAME(ASCIIRecord):
|
||||
tag = 0x3a06
|
||||
|
||||
|
||||
class LIBSECUR(Int2Record):
|
||||
tag = 0x3b02
|
||||
|
||||
|
||||
class BORDER(NoDataRecord):
|
||||
tag = 0x3c00
|
||||
|
||||
|
||||
class SOFTFENCE(NoDataRecord):
|
||||
tag = 0x3d00
|
||||
|
||||
|
||||
class HARDFENCE(NoDataRecord):
|
||||
tag = 0x3f00
|
||||
|
||||
|
||||
class SOFTWIRE(NoDataRecord):
|
||||
tag = 0x3f00
|
||||
|
||||
|
||||
class HARDWIRE(NoDataRecord):
|
||||
tag = 0x4000
|
||||
|
||||
|
||||
class PATHPORT(NoDataRecord):
|
||||
tag = 0x4100
|
||||
|
||||
|
||||
class NODEPORT(NoDataRecord):
|
||||
tag = 0x4200
|
||||
|
||||
|
||||
class USERCONSTRAINT(NoDataRecord):
|
||||
tag = 0x4300
|
||||
|
||||
|
||||
class SPACERERROR(NoDataRecord):
|
||||
tag = 0x4400
|
||||
|
||||
|
||||
class CONTACT(NoDataRecord):
|
||||
tag = 0x4500
|
@ -0,0 +1,119 @@
|
||||
import struct
|
||||
|
||||
import pytest # type: ignore
|
||||
import numpy # type: ignore
|
||||
from numpy.testing import assert_array_equal # type: ignore
|
||||
|
||||
from .basic import parse_bitarray, parse_int2, parse_int4, parse_real8, parse_ascii
|
||||
from .basic import pack_bitarray, pack_int2, pack_int4, pack_real8, pack_ascii
|
||||
from .basic import decode_real8, encode_real8
|
||||
|
||||
from .basic import KlamathError
|
||||
|
||||
|
||||
def test_parse_bitarray():
|
||||
assert(parse_bitarray(b'59') == 13625)
|
||||
assert(parse_bitarray(b'\0\0') == 0)
|
||||
assert(parse_bitarray(b'\xff\xff') == 65535)
|
||||
|
||||
# 4 bytes (too long)
|
||||
with pytest.raises(KlamathError):
|
||||
parse_bitarray(b'4321')
|
||||
|
||||
# empty data
|
||||
with pytest.raises(KlamathError):
|
||||
parse_bitarray(b'')
|
||||
|
||||
|
||||
def test_parse_int2():
|
||||
assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0))
|
||||
|
||||
# odd length
|
||||
with pytest.raises(KlamathError):
|
||||
parse_int2(b'54321')
|
||||
|
||||
# empty data
|
||||
with pytest.raises(KlamathError):
|
||||
parse_int2(b'')
|
||||
|
||||
|
||||
def test_parse_int4():
|
||||
assert_array_equal(parse_int4(b'4321'), (875770417,))
|
||||
|
||||
# length % 4 != 0
|
||||
with pytest.raises(KlamathError):
|
||||
parse_int4(b'654321')
|
||||
|
||||
# empty data
|
||||
with pytest.raises(KlamathError):
|
||||
parse_int4(b'')
|
||||
|
||||
|
||||
def test_decode_real8():
|
||||
# zeroes
|
||||
assert(decode_real8(numpy.array([0x0])) == 0)
|
||||
assert(decode_real8(numpy.array([1<<63])) == 0) # negative
|
||||
assert(decode_real8(numpy.array([0xff << 56])) == 0) # denormalized
|
||||
|
||||
assert(decode_real8(numpy.array([0x4110 << 48])) == 1.0)
|
||||
assert(decode_real8(numpy.array([0xC120 << 48])) == -2.0)
|
||||
|
||||
|
||||
def test_parse_real8():
|
||||
packed = struct.pack('>3Q', 0x0, 0x4110_0000_0000_0000, 0xC120_0000_0000_0000)
|
||||
assert_array_equal(parse_real8(packed), (0.0, 1.0, -2.0))
|
||||
|
||||
# length % 8 != 0
|
||||
with pytest.raises(KlamathError):
|
||||
parse_real8(b'0987654321')
|
||||
|
||||
# empty data
|
||||
with pytest.raises(KlamathError):
|
||||
parse_real8(b'')
|
||||
|
||||
|
||||
def test_parse_ascii():
|
||||
# empty data
|
||||
with pytest.raises(KlamathError):
|
||||
parse_ascii(b'')
|
||||
|
||||
assert(parse_ascii(b'12345') == b'12345')
|
||||
assert(parse_ascii(b'12345\0') == b'12345') # strips trailing null byte
|
||||
|
||||
|
||||
def test_pack_bitarray():
|
||||
packed = pack_bitarray(321)
|
||||
assert(len(packed) == 2)
|
||||
assert(packed == struct.pack('>H', 321))
|
||||
|
||||
|
||||
def test_pack_int2():
|
||||
packed = pack_int2((3, 2, 1))
|
||||
assert(len(packed) == 3*2)
|
||||
assert(packed == struct.pack('>3h', 3, 2, 1))
|
||||
assert(pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1))
|
||||
|
||||
|
||||
def test_pack_int4():
|
||||
packed = pack_int4((3, 2, 1))
|
||||
assert(len(packed) == 3*4)
|
||||
assert(packed == struct.pack('>3l', 3, 2, 1))
|
||||
assert(pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1))
|
||||
|
||||
|
||||
def test_encode_real8():
|
||||
assert(encode_real8(numpy.array([0.0])) == 0)
|
||||
arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12))
|
||||
assert_array_equal(decode_real8(encode_real8(arr)), arr)
|
||||
|
||||
|
||||
def test_pack_real8():
|
||||
reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12)
|
||||
packed = pack_real8(reals)
|
||||
assert(len(packed) == len(reals) * 8)
|
||||
assert_array_equal(parse_real8(packed), reals)
|
||||
|
||||
|
||||
def test_pack_ascii():
|
||||
assert(pack_ascii(b'4321') == b'4321')
|
||||
assert(pack_ascii(b'321') == b'321\0')
|
@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
with open('README.md', 'r') as f:
|
||||
long_description = f.read()
|
||||
|
||||
with open('klamath/VERSION', 'r') as f:
|
||||
version = f.read().strip()
|
||||
|
||||
setup(name='klamath',
|
||||
version=version,
|
||||
description='GDSII format reader/writer',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
author='Jan Petykiewicz',
|
||||
author_email='anewusername@gmail.com',
|
||||
url='https://mpxd.net/code/jan/klamath',
|
||||
packages=find_packages(),
|
||||
package_data={
|
||||
'klamath': ['VERSION',
|
||||
'py.typed',
|
||||
]
|
||||
},
|
||||
install_requires=[
|
||||
'numpy',
|
||||
],
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Information Technology',
|
||||
'Intended Audience :: Manufacturing',
|
||||
'Intended Audience :: Science/Research',
|
||||
'License :: OSI Approved :: GNU General Public License v3',
|
||||
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
|
||||
],
|
||||
keywords=[
|
||||
'layout',
|
||||
'design',
|
||||
'CAD',
|
||||
'EDA',
|
||||
'electronics',
|
||||
'photonics',
|
||||
'IC',
|
||||
'mask',
|
||||
'pattern',
|
||||
'drawing',
|
||||
'lithography',
|
||||
'litho',
|
||||
'geometry',
|
||||
'geometric',
|
||||
'polygon',
|
||||
'gds',
|
||||
'gdsii',
|
||||
'gds2',
|
||||
'stream',
|
||||
'vector',
|
||||
'freeform',
|
||||
'manhattan',
|
||||
'angle',
|
||||
'Calma',
|
||||
],
|
||||
)
|
Loading…
Reference in New Issue