Compare commits

..

No commits in common. 'master' and 'v1.2' have entirely different histories.
master ... v1.2

@ -1,30 +0,0 @@
[flake8]
ignore =
# E501 line too long
E501,
# W391 newlines at EOF
W391,
# E241 multiple spaces after comma
E241,
# E302 expected 2 newlines
E302,
# W503 line break before binary operator (to be deprecated)
W503,
# E265 block comment should start with '# '
E265,
# E123 closing bracket does not match indentation of opening bracket's line
E123,
# E124 closing bracket does not match visual indentation
E124,
# E221 multiple spaces before operator
E221,
# E201 whitespace after '['
E201,
# # E741 ambiguous variable name 'I'
# E741,
per-file-ignores =
# F401 import without use
*/__init__.py: F401,
__init__.py: F401,

3
.gitignore vendored

@ -1,5 +1,5 @@
*.pyc
__pycache__/
__pycache__
*.idea
@ -7,7 +7,6 @@ build/
dist/
*.egg-info/
.mypy_cache/
.pytest_cache/
*.swp
*.swo

@ -44,13 +44,12 @@ The goal is to keep this library simple:
### Links
- [Source repository](https://mpxd.net/code/jan/klamath)
- [PyPI](https://pypi.org/project/klamath)
- [Github mirror](https://github.com/anewusername/klamath)
## Installation
Requirements:
* python >= 3.10 (written and tested with 3.11)
* python >= 3.7 (written and tested with 3.8)
* numpy

@ -1 +0,0 @@
../LICENSE.md

@ -1 +0,0 @@
../README.md

@ -0,0 +1,4 @@
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
__version__ = '''
1.2
'''.strip()

@ -33,6 +33,6 @@ from . import records
from . import elements
from . import library
__author__ = 'Jan Petykiewicz'
__version__ = '1.3'
from .VERSION import __version__
__author__ = 'Jan Petykiewicz'

@ -1,25 +1,20 @@
"""
Functionality for encoding/decoding basic datatypes
"""
from typing import Sequence, IO
from typing import Sequence, BinaryIO, List
import struct
import logging
from datetime import datetime
import numpy
from numpy.typing import NDArray
logger = logging.getLogger(__name__)
import numpy # type: ignore
class KlamathError(Exception):
pass
#
# Parse functions
#
"""
Parse functions
"""
def parse_bitarray(data: bytes) -> int:
if len(data) != 2:
raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.')
@ -27,31 +22,31 @@ def parse_bitarray(data: bytes) -> int:
return val
def parse_int2(data: bytes) -> NDArray[numpy.int16]:
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) -> NDArray[numpy.int32]:
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: NDArray[numpy.uint64]) -> NDArray[numpy.float64]:
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, signature=(float, int, float))
return numpy.ldexp(mant, (4 * (exp - 64) - 56).astype(numpy.int64))
def parse_real8(data: bytes) -> NDArray[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}.')
@ -67,25 +62,20 @@ def parse_ascii(data: bytes) -> bytes:
return data
def parse_datetime(data: bytes) -> list[datetime]:
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])
try:
dt = datetime(year + 1900, *date_parts)
except ValueError as err:
dt = datetime(1900, 1, 1, 0, 0, 0)
logger.info(f'Invalid date {[year] + date_parts}, setting {dt} instead')
dts.append(dt)
year, *date_parts = parse_int2(data[ii:ii+12])
dts.append(datetime(year + 1900, *date_parts))
return dts
#
# Pack functions
#
"""
Pack functions
"""
def pack_bitarray(data: int) -> bytes:
if data > 65535 or data < 0:
raise KlamathError(f'bitarray data out of range: {data}')
@ -106,7 +96,7 @@ def pack_int4(data: Sequence[int]) -> bytes:
return arr.astype('>i4').tobytes()
def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]:
def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray:
""" Convert from float64 to GDS REAL8 representation. """
# Split the ieee float bitfields
ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64))
@ -159,9 +149,9 @@ def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]:
real8 = sign | gds_exp_bits | gds_mant
real8[zero] = 0
real8[gds_exp < -14] = 0 # number is too small
real8[gds_exp < -14] = 0 # number is too small
return real8.astype(numpy.uint64, copy=False)
return real8
def pack_real8(data: Sequence[float]) -> bytes:
@ -182,7 +172,7 @@ def pack_datetime(data: Sequence[datetime]) -> bytes:
return pack_int2(parts)
def read(stream: IO[bytes], size: int) -> bytes:
def read(stream: BinaryIO, size: int) -> bytes:
""" Read and check for failure """
data = stream.read(size)
if len(data) != size:

@ -2,12 +2,11 @@
Functionality for reading/writing elements (geometry, text labels,
structure references) and associated properties.
"""
from typing import Optional, IO, TypeVar, Type, Union
from typing import Dict, Tuple, Optional, BinaryIO, TypeVar, Type, Union
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
import numpy
from numpy.typing import NDArray
import numpy # type: ignore
from .basic import KlamathError
from .record import Record
@ -29,7 +28,8 @@ T = TypeVar('T', bound='Text')
X = TypeVar('X', bound='Box')
def read_properties(stream: IO[bytes]) -> dict[int, bytes]:
def read_properties(stream: BinaryIO) -> Dict[int, bytes]:
"""
Read element properties.
@ -56,7 +56,7 @@ def read_properties(stream: IO[bytes]) -> dict[int, bytes]:
return properties
def write_properties(stream: IO[bytes], properties: dict[int, bytes]) -> int:
def write_properties(stream: BinaryIO, properties: Dict[int, bytes]) -> int:
"""
Write element properties.
@ -78,7 +78,7 @@ class Element(metaclass=ABCMeta):
"""
@classmethod
@abstractmethod
def read(cls: Type[E], stream: IO[bytes]) -> E:
def read(cls: Type[E], stream: BinaryIO) -> E:
"""
Read from a stream to construct this object.
Consumes up to (and including) the ENDEL record.
@ -92,7 +92,7 @@ class Element(metaclass=ABCMeta):
pass
@abstractmethod
def write(self, stream: IO[bytes]) -> int:
def write(self, stream: BinaryIO) -> int:
"""
Write this element to a stream.
Finishes with an ENDEL record.
@ -131,7 +131,7 @@ class Reference(Element):
angle_deg: float
""" Rotation (degrees counterclockwise) """
xy: NDArray[numpy.int32]
xy: numpy.ndarray
"""
(For SREF) Location in the parent structure corresponding to the instance's origin (0, 0).
(For AREF) 3 locations:
@ -144,14 +144,14 @@ class Reference(Element):
basis vectors to match it.
"""
colrow: tuple[int, int] | NDArray[numpy.int16] | None
colrow: Optional[Union[Tuple[int, int], numpy.ndarray]]
""" Number of columns and rows (AREF) or None (SREF) """
properties: dict[int, bytes]
properties: Dict[int, bytes]
""" Properties associated with this reference. """
@classmethod
def read(cls: Type[R], stream: IO[bytes]) -> R:
def read(cls: Type[R], stream: BinaryIO) -> R:
invert_y = False
mag = 1
angle_deg = 0
@ -174,17 +174,10 @@ class Reference(Element):
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,
)
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: IO[bytes]) -> int:
def write(self, stream: BinaryIO) -> int:
b = 0
if self.colrow is None:
b += SREF.write(stream, None)
@ -196,7 +189,7 @@ class Reference(Element):
b += STRANS.write(stream, int(self.invert_y) << 15)
if self.mag != 1:
b += MAG.write(stream, self.mag)
if self.angle_deg != 0:
if self.angle_deg !=0:
b += ANGLE.write(stream, self.angle_deg)
if self.colrow is not None:
@ -223,24 +216,24 @@ class Boundary(Element):
"""
__slots__ = ('layer', 'xy', 'properties')
layer: tuple[int, int]
layer: Tuple[int, int]
""" (layer, data_type) tuple """
xy: NDArray[numpy.int32]
xy: numpy.ndarray
""" Ordered vertices of the shape. First and last points should be identical. """
properties: dict[int, bytes]
properties: Dict[int, bytes]
""" Properties for the element. """
@classmethod
def read(cls: Type[B], stream: IO[bytes]) -> B:
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: IO[bytes]) -> int:
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])
@ -260,7 +253,7 @@ class Path(Element):
"""
__slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension')
layer: tuple[int, int]
layer: Tuple[int, int]
""" (layer, data_type) tuple """
path_type: int
@ -269,17 +262,17 @@ class Path(Element):
width: int
""" Path width """
extension: tuple[int, int]
extension: Tuple[int, int]
""" Extension when using path_type=4. Ignored otherwise. """
xy: NDArray[numpy.int32]
xy: numpy.ndarray
""" Path centerline coordinates """
properties: dict[int, bytes]
properties: Dict[int, bytes]
""" Properties for the element. """
@classmethod
def read(cls: Type[P], stream: IO[bytes]) -> P:
def read(cls: Type[P], stream: BinaryIO) -> P:
path_type = 0
width = 0
bgn_ext = 0
@ -306,7 +299,7 @@ class Path(Element):
properties=properties, extension=(bgn_ext, end_ext),
path_type=path_type, width=width)
def write(self, stream: IO[bytes]) -> int:
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])
@ -334,24 +327,24 @@ class Box(Element):
"""
__slots__ = ('layer', 'xy', 'properties')
layer: tuple[int, int]
layer: Tuple[int, int]
""" (layer, box_type) tuple """
xy: NDArray[numpy.int32]
xy: numpy.ndarray
""" Box coordinates (5 pairs) """
properties: dict[int, bytes]
properties: Dict[int, bytes]
""" Properties for the element. """
@classmethod
def read(cls: Type[X], stream: IO[bytes]) -> X:
def read(cls: Type[X], stream: BinaryIO) -> X:
layer = LAYER.skip_and_read(stream)[0]
dtype = BOXTYPE.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: IO[bytes]) -> int:
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])
@ -368,24 +361,24 @@ class Node(Element):
"""
__slots__ = ('layer', 'xy', 'properties')
layer: tuple[int, int]
layer: Tuple[int, int]
""" (layer, node_type) tuple """
xy: NDArray[numpy.int32]
xy: numpy.ndarray
""" 1-50 pairs of coordinates. """
properties: dict[int, bytes]
properties: Dict[int, bytes]
""" Properties for the element. """
@classmethod
def read(cls: Type[N], stream: IO[bytes]) -> N:
def read(cls: Type[N], stream: BinaryIO) -> N:
layer = LAYER.skip_and_read(stream)[0]
dtype = NODETYPE.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: IO[bytes]) -> int:
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])
@ -403,7 +396,7 @@ class Text(Element):
__slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type',
'width', 'invert_y', 'mag', 'angle_deg', 'string')
layer: tuple[int, int]
layer: Tuple[int, int]
""" (layer, node_type) tuple """
presentation: int
@ -428,17 +421,17 @@ class Text(Element):
angle_deg: float
""" Rotation (ccw). Default 0. """
xy: NDArray[numpy.int32]
xy: numpy.ndarray
""" Position (1 pair only) """
string: bytes
""" Text content """
properties: dict[int, bytes]
properties: Dict[int, bytes]
""" Properties for the element. """
@classmethod
def read(cls: Type[T], stream: IO[bytes]) -> T:
def read(cls: Type[T], stream: BinaryIO) -> T:
path_type = 0
presentation = 0
invert_y = False
@ -474,7 +467,7 @@ class Text(Element):
string=string, presentation=presentation, path_type=path_type,
width=width, invert_y=invert_y, mag=mag, angle_deg=angle_deg)
def write(self, stream: IO[bytes]) -> int:
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])
@ -488,7 +481,7 @@ class Text(Element):
b += STRANS.write(stream, int(self.invert_y) << 15)
if self.mag != 1:
b += MAG.write(stream, self.mag)
if self.angle_deg != 0:
if self.angle_deg !=0:
b += ANGLE.write(stream, self.angle_deg)
b += XY.write(stream, self.xy)
b += STRING.write(stream, self.string)

@ -1,7 +1,7 @@
"""
File-level read/write functionality.
"""
from typing import IO, TypeVar, Type, MutableMapping
from typing import List, Dict, Tuple, Optional, BinaryIO, TypeVar, Type, MutableMapping
import io
from datetime import datetime
from dataclasses import dataclass
@ -45,7 +45,7 @@ class FileHeader:
""" Last-accessed time """
@classmethod
def read(cls: Type[FH], stream: IO[bytes]) -> FH:
def read(cls: Type[FH], stream: BinaryIO) -> FH:
"""
Read and construct a header from the provided stream.
@ -55,7 +55,7 @@ class FileHeader:
Returns:
FileHeader object
"""
_version = HEADER.read(stream)[0] # noqa: F841 # var is unused
version = HEADER.read(stream)[0]
mod_time, acc_time = BGNLIB.read(stream)
name = LIBNAME.skip_and_read(stream)
uu, dbu = UNITS.skip_and_read(stream)
@ -63,7 +63,7 @@ class FileHeader:
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: IO[bytes]) -> int:
def write(self, stream: BinaryIO) -> int:
"""
Write the header to a stream
@ -80,7 +80,7 @@ class FileHeader:
return b
def scan_structs(stream: IO[bytes]) -> dict[bytes, int]:
def scan_structs(stream: BinaryIO) -> Dict[bytes, int]:
"""
Scan through a GDS file, building a table of
{b'structure_name': byte_offset}.
@ -107,7 +107,7 @@ def scan_structs(stream: IO[bytes]) -> dict[bytes, int]:
return positions
def try_read_struct(stream: IO[bytes]) -> tuple[bytes, list[Element]] | None:
def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]:
"""
Skip to the next structure and attempt to read it.
@ -125,13 +125,12 @@ def try_read_struct(stream: IO[bytes]) -> tuple[bytes, list[Element]] | None:
return name, elements
def write_struct(
stream: IO[bytes],
name: bytes,
elements: list[Element],
cre_time: datetime = datetime(1900, 1, 1),
mod_time: datetime = datetime(1900, 1, 1),
) -> int:
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.
@ -151,7 +150,7 @@ def write_struct(
return b
def read_elements(stream: IO[bytes]) -> list[Element]:
def read_elements(stream: BinaryIO) -> List[Element]:
"""
Read elements from the stream until an ENDSTR
record is encountered. The ENDSTR record is also
@ -163,7 +162,7 @@ def read_elements(stream: IO[bytes]) -> list[Element]:
Returns:
List of element objects.
"""
data: list[Element] = []
data: List[Element] = []
size, tag = Record.read_header(stream)
while tag != ENDSTR.tag:
if tag == BOUNDARY.tag:
@ -187,7 +186,7 @@ def read_elements(stream: IO[bytes]) -> list[Element]:
return data
def scan_hierarchy(stream: IO[bytes]) -> dict[bytes, dict[bytes, int]]:
def scan_hierarchy(stream: BinaryIO) -> Dict[bytes, Dict[bytes, int]]:
"""
Scan through a GDS file, building a table of instance counts
`{b'structure_name': {b'ref_name': count}}`.
@ -224,7 +223,7 @@ def scan_hierarchy(stream: IO[bytes]) -> dict[bytes, dict[bytes, int]]:
elif tag == ENDEL.tag:
if ref_count is None:
ref_count = 1
assert ref_name is not None
assert(ref_name is not None)
cur_structure[ref_name] += ref_count
else:
stream.seek(size, io.SEEK_CUR)

@ -1,14 +1,14 @@
"""
Generic record-level read/write functionality.
"""
from typing import Sequence, IO, TypeVar, ClassVar, Type
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
from numpy.typing import NDArray
import numpy # type: ignore
from .basic import KlamathError
from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray
@ -19,7 +19,7 @@ from .basic import parse_ascii, pack_ascii, read
_RECORD_HEADER_FMT = struct.Struct('>HH')
def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int:
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}')
@ -27,7 +27,7 @@ def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int:
return stream.write(header)
def read_record_header(stream: IO[bytes]) -> tuple[int, int]:
def read_record_header(stream: BinaryIO) -> Tuple[int, int]:
"""
Read a record's header (size and tag).
Args:
@ -46,7 +46,7 @@ def read_record_header(stream: IO[bytes]) -> tuple[int, int]:
return data_size, tag
def expect_record(stream: IO[bytes], tag: int) -> int:
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 0x{actual_tag:04x}, expected 0x{tag:04x}')
@ -58,7 +58,7 @@ R = TypeVar('R', bound='Record')
class Record(metaclass=ABCMeta):
tag: ClassVar[int] = -1
expected_size: ClassVar[int | None] = None
expected_size: ClassVar[Optional[int]] = None
@classmethod
def check_size(cls, size: int):
@ -71,7 +71,7 @@ class Record(metaclass=ABCMeta):
@classmethod
@abstractmethod
def read_data(cls, stream: IO[bytes], size: int):
def read_data(cls, stream: BinaryIO, size: int):
pass
@classmethod
@ -80,15 +80,15 @@ class Record(metaclass=ABCMeta):
pass
@staticmethod
def read_header(stream: IO[bytes]) -> tuple[int, int]:
def read_header(stream: BinaryIO) -> Tuple[int, int]:
return read_record_header(stream)
@classmethod
def write_header(cls, stream: IO[bytes], data_size: int) -> int:
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: IO[bytes]) -> bool:
def skip_past(cls, stream: BinaryIO) -> bool:
"""
Skip to the end of the next occurence of this record.
@ -110,7 +110,7 @@ class Record(metaclass=ABCMeta):
return True
@classmethod
def skip_and_read(cls, stream: IO[bytes]):
def skip_and_read(cls, stream: BinaryIO):
size, tag = Record.read_header(stream)
while tag != cls.tag:
stream.seek(size, io.SEEK_CUR)
@ -119,13 +119,13 @@ class Record(metaclass=ABCMeta):
return data
@classmethod
def read(cls: Type[R], stream: IO[bytes]):
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: IO[bytes], data) -> int:
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)
@ -133,10 +133,10 @@ class Record(metaclass=ABCMeta):
class NoDataRecord(Record):
expected_size: ClassVar[int | None] = 0
expected_size: ClassVar[Optional[int]] = 0
@classmethod
def read_data(cls, stream: IO[bytes], size: int) -> None:
def read_data(cls, stream: BinaryIO, size: int) -> None:
stream.read(size)
@classmethod
@ -147,10 +147,10 @@ class NoDataRecord(Record):
class BitArrayRecord(Record):
expected_size: ClassVar[int | None] = 2
expected_size: ClassVar[Optional[int]] = 2
@classmethod
def read_data(cls, stream: IO[bytes], size: int) -> int:
def read_data(cls, stream: BinaryIO, size: int) -> int:
return parse_bitarray(read(stream, 2))
@classmethod
@ -160,7 +160,7 @@ class BitArrayRecord(Record):
class Int2Record(Record):
@classmethod
def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int16]:
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
return parse_int2(read(stream, size))
@classmethod
@ -170,7 +170,7 @@ class Int2Record(Record):
class Int4Record(Record):
@classmethod
def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.int32]:
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
return parse_int4(read(stream, size))
@classmethod
@ -180,7 +180,7 @@ class Int4Record(Record):
class Real8Record(Record):
@classmethod
def read_data(cls, stream: IO[bytes], size: int) -> NDArray[numpy.float64]:
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
return parse_real8(read(stream, size))
@classmethod
@ -190,7 +190,7 @@ class Real8Record(Record):
class ASCIIRecord(Record):
@classmethod
def read_data(cls, stream: IO[bytes], size: int) -> bytes:
def read_data(cls, stream: BinaryIO, size: int) -> bytes:
return parse_ascii(read(stream, size))
@classmethod
@ -200,7 +200,7 @@ class ASCIIRecord(Record):
class DateTimeRecord(Record):
@classmethod
def read_data(cls, stream: IO[bytes], size: int) -> list[datetime]:
def read_data(cls, stream: BinaryIO, size: int) -> List[datetime]:
return parse_datetime(read(stream, size))
@classmethod

@ -111,7 +111,7 @@ class PRESENTATION(BitArrayRecord):
class SPACING(Int2Record):
tag = 0x1802 # Not sure about 02; Unused
tag = 0x1802 #Not sure about 02; Unused
class STRING(ASCIIRecord):
@ -133,11 +133,11 @@ class ANGLE(Real8Record):
class UINTEGER(Int2Record):
tag = 0x1d02 # Unused; not sure about 02
tag = 0x1d02 #Unused; not sure about 02
class USTRING(ASCIIRecord):
tag = 0x1e06 # Unused; not sure about 06
tag = 0x1e06 #Unused; not sure about 06
class REFLIBS(ASCIIRecord):
@ -183,11 +183,11 @@ class ATTRTABLE(ASCIIRecord):
class STYPTABLE(ASCIIRecord):
tag = 0x2406 # UNUSED, not sure about 06
tag = 0x2406 #UNUSED, not sure about 06
class STRTYPE(Int2Record):
tag = 0x2502 # UNUSED
tag = 0x2502 #UNUSED
class ELFLAGS(BitArrayRecord):

@ -1,21 +1,20 @@
import struct
import pytest # type: ignore
import numpy
from datetime import datetime
from numpy.testing import assert_array_equal
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, parse_datetime
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
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):
@ -52,12 +51,12 @@ def test_parse_int4():
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([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
assert(decode_real8(numpy.array([0x4110 << 48])) == 1.0)
assert(decode_real8(numpy.array([0xC120 << 48])) == -2.0)
def test_parse_real8():
@ -74,36 +73,36 @@ def test_parse_real8():
def test_parse_ascii():
# # empty data Now allowed!
# with pytest.raises(KlamathError):
# parse_ascii(b'')
# # empty data Now allowed!
# 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
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)
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)
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)
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
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)
@ -111,21 +110,10 @@ def test_encode_real8():
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(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'
def test_invalid_date():
default = [datetime(1900, 1, 1, 0, 0, 0)]
assert parse_datetime(pack_int2((0, 0, 0, 0, 0, 0))) == default
assert parse_datetime(pack_int2((0, 1, 32, 0, 0, 0))) == default
assert parse_datetime(pack_int2((0, 2, 30, 0, 0, 0))) == default
assert parse_datetime(pack_int2((0, 1, 1, 24, 0, 0))) == default
assert parse_datetime(pack_int2((0, 1, 1, 25, 0, 0))) == default
assert parse_datetime(pack_int2((0, 1, 1, 0, 61, 0))) == default
assert parse_datetime(pack_int2((0, 1, 1, 0, 0, 61))) == default
assert(pack_ascii(b'4321') == b'4321')
assert(pack_ascii(b'321') == b'321\0')

@ -1,58 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "klamath"
description = "GDSII format reader/writer"
readme = "README.md"
license = { file = "LICENSE.md" }
authors = [
{ name="Jan Petykiewicz", email="jan@mpxd.net" },
]
homepage = "https://mpxd.net/code/jan/klamath"
repository = "https://mpxd.net/code/jan/klamath"
keywords = [
"layout",
"gds",
"gdsii",
"gds2",
"Calma",
"stream",
"design",
"CAD",
"EDA",
"electronics",
"photonics",
"IC",
"mask",
"pattern",
"drawing",
"lithography",
"litho",
"geometry",
"geometric",
"polygon",
"vector",
]
classifiers = [
"Programming Language :: Python :: 3",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: Manufacturing",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
]
requires-python = ">=3.8"
include = [
"LICENSE.md"
]
dynamic = ["version"]
dependencies = [
"numpy~=1.21",
]
[tool.hatch.version]
path = "klamath/__init__.py"

@ -0,0 +1,63 @@
#!/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.py', 'rt') as f:
version = f.readlines()[2].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='jan@mpxd.net',
url='https://mpxd.net/code/jan/klamath',
packages=find_packages(),
package_data={
'klamath': ['py.typed'],
},
install_requires=[
'numpy',
],
classifiers=[
'Programming Language :: Python :: 3',
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: Manufacturing',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'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…
Cancel
Save