initial commit
This commit is contained in:
commit
034d5fbcb7
200
README.md
Normal file
200
README.md
Normal file
@ -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()
|
||||
```
|
1
klamath/VERSION
Normal file
1
klamath/VERSION
Normal file
@ -0,0 +1 @@
|
||||
0.1
|
42
klamath/__init__.py
Normal file
42
klamath/__init__.py
Normal file
@ -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()
|
BIN
klamath/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
klamath/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
klamath/__pycache__/basic.cpython-38.pyc
Normal file
BIN
klamath/__pycache__/basic.cpython-38.pyc
Normal file
Binary file not shown.
BIN
klamath/__pycache__/elements.cpython-38.pyc
Normal file
BIN
klamath/__pycache__/elements.cpython-38.pyc
Normal file
Binary file not shown.
BIN
klamath/__pycache__/library.cpython-38.pyc
Normal file
BIN
klamath/__pycache__/library.cpython-38.pyc
Normal file
Binary file not shown.
BIN
klamath/__pycache__/record.cpython-38.pyc
Normal file
BIN
klamath/__pycache__/record.cpython-38.pyc
Normal file
Binary file not shown.
BIN
klamath/__pycache__/records.cpython-38.pyc
Normal file
BIN
klamath/__pycache__/records.cpython-38.pyc
Normal file
Binary file not shown.
BIN
klamath/__pycache__/test_basic.cpython-38-PYTEST.pyc
Normal file
BIN
klamath/__pycache__/test_basic.cpython-38-PYTEST.pyc
Normal file
Binary file not shown.
BIN
klamath/__pycache__/test_record.cpython-38-PYTEST.pyc
Normal file
BIN
klamath/__pycache__/test_record.cpython-38-PYTEST.pyc
Normal file
Binary file not shown.
174
klamath/basic.py
Normal file
174
klamath/basic.py
Normal file
@ -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
|
489
klamath/elements.py
Normal file
489
klamath/elements.py
Normal file
@ -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
|
187
klamath/library.py
Normal file
187
klamath/library.py
Normal file
@ -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
klamath/py.typed
Normal file
0
klamath/py.typed
Normal file
208
klamath/record.py
Normal file
208
klamath/record.py
Normal file
@ -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)
|
333
klamath/records.py
Normal file
333
klamath/records.py
Normal file
@ -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
|
119
klamath/test_basic.py
Normal file
119
klamath/test_basic.py
Normal file
@ -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')
|
65
setup.py
Normal file
65
setup.py
Normal file
@ -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
Block a user