Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
ae9c2d7b5f | |||
428e396b9b | |||
4ffb87d361 | |||
a50d53b508 | |||
6ad3358665 | |||
65a33d2eca | |||
7d6cea1c4a | |||
95976cd637 | |||
8061d6cd37 | |||
cae970e65c | |||
2ea9d32984 | |||
15af9078f0 | |||
f12a1c6421 | |||
438cde513e | |||
59c94f7c17 | |||
e7e42a2ef8 | |||
dc58159cdf | |||
e94b93d5af | |||
6a0019010f | |||
46b3349935 | |||
97527a5948 | |||
61d04f89ad | |||
9d14bf27c6 | |||
4d362f8e09 | |||
0bbc8f8e08 | |||
e90e44bd15 | |||
38fc306644 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
__pycache__
|
__pycache__/
|
||||||
|
|
||||||
*.idea
|
*.idea
|
||||||
|
|
||||||
@ -7,6 +7,7 @@ build/
|
|||||||
dist/
|
dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
@ -44,12 +44,13 @@ The goal is to keep this library simple:
|
|||||||
### Links
|
### Links
|
||||||
- [Source repository](https://mpxd.net/code/jan/klamath)
|
- [Source repository](https://mpxd.net/code/jan/klamath)
|
||||||
- [PyPI](https://pypi.org/project/klamath)
|
- [PyPI](https://pypi.org/project/klamath)
|
||||||
|
- [Github mirror](https://github.com/anewusername/klamath)
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
* python >= 3.7 (written and tested with 3.8)
|
* python >= 3.11
|
||||||
* numpy
|
* numpy
|
||||||
|
|
||||||
|
|
||||||
|
1
klamath/LICENSE.md
Symbolic link
1
klamath/LICENSE.md
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../LICENSE.md
|
1
klamath/README.md
Symbolic link
1
klamath/README.md
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../README.md
|
@ -1,4 +0,0 @@
|
|||||||
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
|
|
||||||
__version__ = '''
|
|
||||||
1.2
|
|
||||||
'''.strip()
|
|
@ -27,12 +27,14 @@ The goal is to keep this library simple:
|
|||||||
tools for working with hierarchical design data and supports multiple
|
tools for working with hierarchical design data and supports multiple
|
||||||
file formats.
|
file formats.
|
||||||
"""
|
"""
|
||||||
from . import basic
|
from . import (
|
||||||
from . import record
|
basic as basic,
|
||||||
from . import records
|
record as record,
|
||||||
from . import elements
|
records as records,
|
||||||
from . import library
|
elements as elements,
|
||||||
|
library as library,
|
||||||
from .VERSION import __version__
|
)
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
__version__ = '1.4'
|
||||||
|
|
||||||
|
@ -1,20 +1,26 @@
|
|||||||
"""
|
"""
|
||||||
Functionality for encoding/decoding basic datatypes
|
Functionality for encoding/decoding basic datatypes
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, BinaryIO, List
|
from typing import IO
|
||||||
|
from collections.abc import Sequence
|
||||||
import struct
|
import struct
|
||||||
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import numpy # type: ignore
|
import numpy
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class KlamathError(Exception):
|
class KlamathError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
"""
|
#
|
||||||
Parse functions
|
# Parse functions
|
||||||
"""
|
#
|
||||||
def parse_bitarray(data: bytes) -> int:
|
def parse_bitarray(data: bytes) -> int:
|
||||||
if len(data) != 2:
|
if len(data) != 2:
|
||||||
raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.')
|
raise KlamathError(f'Incorrect bitarray size ({len(data)}). Data is {data!r}.')
|
||||||
@ -22,31 +28,31 @@ def parse_bitarray(data: bytes) -> int:
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
def parse_int2(data: bytes) -> numpy.ndarray:
|
def parse_int2(data: bytes) -> NDArray[numpy.int16]:
|
||||||
data_len = len(data)
|
data_len = len(data)
|
||||||
if data_len == 0 or (data_len % 2) != 0:
|
if data_len == 0 or (data_len % 2) != 0:
|
||||||
raise KlamathError(f'Incorrect int2 size ({len(data)}). Data is {data!r}.')
|
raise KlamathError(f'Incorrect int2 size ({len(data)}). Data is {data!r}.')
|
||||||
return numpy.frombuffer(data, dtype='>i2', count=data_len // 2)
|
return numpy.frombuffer(data, dtype='>i2', count=data_len // 2)
|
||||||
|
|
||||||
|
|
||||||
def parse_int4(data: bytes) -> numpy.ndarray:
|
def parse_int4(data: bytes) -> NDArray[numpy.int32]:
|
||||||
data_len = len(data)
|
data_len = len(data)
|
||||||
if data_len == 0 or (data_len % 4) != 0:
|
if data_len == 0 or (data_len % 4) != 0:
|
||||||
raise KlamathError(f'Incorrect int4 size ({len(data)}). Data is {data!r}.')
|
raise KlamathError(f'Incorrect int4 size ({len(data)}). Data is {data!r}.')
|
||||||
return numpy.frombuffer(data, dtype='>i4', count=data_len // 4)
|
return numpy.frombuffer(data, dtype='>i4', count=data_len // 4)
|
||||||
|
|
||||||
|
|
||||||
def decode_real8(nums: numpy.ndarray) -> numpy.ndarray:
|
def decode_real8(nums: NDArray[numpy.uint64]) -> NDArray[numpy.float64]:
|
||||||
""" Convert GDS REAL8 data to IEEE float64. """
|
""" Convert GDS REAL8 data to IEEE float64. """
|
||||||
nums = nums.astype(numpy.uint64)
|
nums = nums.astype(numpy.uint64)
|
||||||
neg = nums & 0x8000_0000_0000_0000
|
neg = nums & 0x8000_0000_0000_0000
|
||||||
exp = (nums >> 56) & 0x7f
|
exp = (nums >> 56) & 0x7f
|
||||||
mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64)
|
mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64)
|
||||||
mant[neg != 0] *= -1
|
mant[neg != 0] *= -1
|
||||||
return numpy.ldexp(mant, (4 * (exp - 64) - 56).astype(numpy.int64))
|
return numpy.ldexp(mant, 4 * (exp - 64) - 56, signature=(float, int, float))
|
||||||
|
|
||||||
|
|
||||||
def parse_real8(data: bytes) -> numpy.ndarray:
|
def parse_real8(data: bytes) -> NDArray[numpy.float64]:
|
||||||
data_len = len(data)
|
data_len = len(data)
|
||||||
if data_len == 0 or (data_len % 8) != 0:
|
if data_len == 0 or (data_len % 8) != 0:
|
||||||
raise KlamathError(f'Incorrect real8 size ({len(data)}). Data is {data!r}.')
|
raise KlamathError(f'Incorrect real8 size ({len(data)}). Data is {data!r}.')
|
||||||
@ -62,41 +68,46 @@ def parse_ascii(data: bytes) -> bytes:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def parse_datetime(data: bytes) -> List[datetime]:
|
def parse_datetime(data: bytes) -> list[datetime]:
|
||||||
""" Parse date/time data (12 byte blocks) """
|
""" Parse date/time data (12 byte blocks) """
|
||||||
if len(data) == 0 or len(data) % 12 != 0:
|
if len(data) == 0 or len(data) % 12 != 0:
|
||||||
raise KlamathError(f'Incorrect datetime size ({len(data)}). Data is {data!r}.')
|
raise KlamathError(f'Incorrect datetime size ({len(data)}). Data is {data!r}.')
|
||||||
dts = []
|
dts = []
|
||||||
for ii in range(0, len(data), 12):
|
for ii in range(0, len(data), 12):
|
||||||
year, *date_parts = parse_int2(data[ii:ii+12])
|
year, *date_parts = parse_int2(data[ii:ii + 12])
|
||||||
dts.append(datetime(year + 1900, *date_parts))
|
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)
|
||||||
return dts
|
return dts
|
||||||
|
|
||||||
|
|
||||||
"""
|
#
|
||||||
Pack functions
|
# Pack functions
|
||||||
"""
|
#
|
||||||
def pack_bitarray(data: int) -> bytes:
|
def pack_bitarray(data: int) -> bytes:
|
||||||
if data > 65535 or data < 0:
|
if data > 65535 or data < 0:
|
||||||
raise KlamathError(f'bitarray data out of range: {data}')
|
raise KlamathError(f'bitarray data out of range: {data}')
|
||||||
return struct.pack('>H', data)
|
return struct.pack('>H', data)
|
||||||
|
|
||||||
|
|
||||||
def pack_int2(data: Sequence[int]) -> bytes:
|
def pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||||
arr = numpy.array(data)
|
arr = numpy.asarray(data)
|
||||||
if (arr > 32767).any() or (arr < -32768).any():
|
if (arr > 32767).any() or (arr < -32768).any():
|
||||||
raise KlamathError(f'int2 data out of range: {arr}')
|
raise KlamathError(f'int2 data out of range: {arr}')
|
||||||
return arr.astype('>i2').tobytes()
|
return arr.astype('>i2').tobytes()
|
||||||
|
|
||||||
|
|
||||||
def pack_int4(data: Sequence[int]) -> bytes:
|
def pack_int4(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||||
arr = numpy.array(data)
|
arr = numpy.asarray(data)
|
||||||
if (arr > 2147483647).any() or (arr < -2147483648).any():
|
if (arr > 2147483647).any() or (arr < -2147483648).any():
|
||||||
raise KlamathError(f'int4 data out of range: {arr}')
|
raise KlamathError(f'int4 data out of range: {arr}')
|
||||||
return arr.astype('>i4').tobytes()
|
return arr.astype('>i4').tobytes()
|
||||||
|
|
||||||
|
|
||||||
def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray:
|
def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]:
|
||||||
""" Convert from float64 to GDS REAL8 representation. """
|
""" Convert from float64 to GDS REAL8 representation. """
|
||||||
# Split the ieee float bitfields
|
# Split the ieee float bitfields
|
||||||
ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64))
|
ieee = numpy.atleast_1d(fnums.astype(numpy.float64).view(numpy.uint64))
|
||||||
@ -149,13 +160,13 @@ def encode_real8(fnums: numpy.ndarray) -> numpy.ndarray:
|
|||||||
|
|
||||||
real8 = sign | gds_exp_bits | gds_mant
|
real8 = sign | gds_exp_bits | gds_mant
|
||||||
real8[zero] = 0
|
real8[zero] = 0
|
||||||
real8[gds_exp < -14] = 0 # number is too small
|
real8[gds_exp < -14] = 0 # number is too small
|
||||||
|
|
||||||
return real8
|
return real8.astype(numpy.uint64, copy=False)
|
||||||
|
|
||||||
|
|
||||||
def pack_real8(data: Sequence[float]) -> bytes:
|
def pack_real8(data: NDArray[numpy.floating] | Sequence[float] | float) -> bytes:
|
||||||
return encode_real8(numpy.array(data)).astype('>u8').tobytes()
|
return encode_real8(numpy.asarray(data)).astype('>u8').tobytes()
|
||||||
|
|
||||||
|
|
||||||
def pack_ascii(data: bytes) -> bytes:
|
def pack_ascii(data: bytes) -> bytes:
|
||||||
@ -172,7 +183,7 @@ def pack_datetime(data: Sequence[datetime]) -> bytes:
|
|||||||
return pack_int2(parts)
|
return pack_int2(parts)
|
||||||
|
|
||||||
|
|
||||||
def read(stream: BinaryIO, size: int) -> bytes:
|
def read(stream: IO[bytes], size: int) -> bytes:
|
||||||
""" Read and check for failure """
|
""" Read and check for failure """
|
||||||
data = stream.read(size)
|
data = stream.read(size)
|
||||||
if len(data) != size:
|
if len(data) != size:
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
Functionality for reading/writing elements (geometry, text labels,
|
Functionality for reading/writing elements (geometry, text labels,
|
||||||
structure references) and associated properties.
|
structure references) and associated properties.
|
||||||
"""
|
"""
|
||||||
from typing import Dict, Tuple, Optional, BinaryIO, TypeVar, Type, Union
|
from typing import IO, TypeVar
|
||||||
|
from collections.abc import Mapping
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import numpy # type: ignore
|
import numpy
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from .basic import KlamathError
|
from .basic import KlamathError
|
||||||
from .record import Record
|
from .record import Record
|
||||||
@ -28,8 +30,7 @@ T = TypeVar('T', bound='Text')
|
|||||||
X = TypeVar('X', bound='Box')
|
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.
|
Read element properties.
|
||||||
|
|
||||||
@ -51,12 +52,12 @@ def read_properties(stream: BinaryIO) -> Dict[int, bytes]:
|
|||||||
value = PROPVALUE.read(stream)
|
value = PROPVALUE.read(stream)
|
||||||
if key in properties:
|
if key in properties:
|
||||||
raise KlamathError(f'Duplicate property key: {key!r}')
|
raise KlamathError(f'Duplicate property key: {key!r}')
|
||||||
properties[key] = value
|
properties[key] = value
|
||||||
size, tag = Record.read_header(stream)
|
size, tag = Record.read_header(stream)
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
|
|
||||||
def write_properties(stream: BinaryIO, properties: Dict[int, bytes]) -> int:
|
def write_properties(stream: IO[bytes], properties: Mapping[int, bytes]) -> int:
|
||||||
"""
|
"""
|
||||||
Write element properties.
|
Write element properties.
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ class Element(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def read(cls: Type[E], stream: BinaryIO) -> E:
|
def read(cls: type[E], stream: IO[bytes]) -> E:
|
||||||
"""
|
"""
|
||||||
Read from a stream to construct this object.
|
Read from a stream to construct this object.
|
||||||
Consumes up to (and including) the ENDEL record.
|
Consumes up to (and including) the ENDEL record.
|
||||||
@ -92,7 +93,7 @@ class Element(metaclass=ABCMeta):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def write(self, stream: BinaryIO) -> int:
|
def write(self, stream: IO[bytes]) -> int:
|
||||||
"""
|
"""
|
||||||
Write this element to a stream.
|
Write this element to a stream.
|
||||||
Finishes with an ENDEL record.
|
Finishes with an ENDEL record.
|
||||||
@ -131,7 +132,7 @@ class Reference(Element):
|
|||||||
angle_deg: float
|
angle_deg: float
|
||||||
""" Rotation (degrees counterclockwise) """
|
""" Rotation (degrees counterclockwise) """
|
||||||
|
|
||||||
xy: numpy.ndarray
|
xy: NDArray[numpy.int32]
|
||||||
"""
|
"""
|
||||||
(For SREF) Location in the parent structure corresponding to the instance's origin (0, 0).
|
(For SREF) Location in the parent structure corresponding to the instance's origin (0, 0).
|
||||||
(For AREF) 3 locations:
|
(For AREF) 3 locations:
|
||||||
@ -144,14 +145,14 @@ class Reference(Element):
|
|||||||
basis vectors to match it.
|
basis vectors to match it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
colrow: Optional[Union[Tuple[int, int], numpy.ndarray]]
|
colrow: tuple[int, int] | NDArray[numpy.int16] | None
|
||||||
""" Number of columns and rows (AREF) or None (SREF) """
|
""" Number of columns and rows (AREF) or None (SREF) """
|
||||||
|
|
||||||
properties: Dict[int, bytes]
|
properties: Mapping[int, bytes]
|
||||||
""" Properties associated with this reference. """
|
""" Properties associated with this reference. """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls: Type[R], stream: BinaryIO) -> R:
|
def read(cls: type[R], stream: IO[bytes]) -> R:
|
||||||
invert_y = False
|
invert_y = False
|
||||||
mag = 1
|
mag = 1
|
||||||
angle_deg = 0
|
angle_deg = 0
|
||||||
@ -174,10 +175,17 @@ class Reference(Element):
|
|||||||
size, tag = Record.read_header(stream)
|
size, tag = Record.read_header(stream)
|
||||||
xy = XY.read_data(stream, size).reshape(-1, 2)
|
xy = XY.read_data(stream, size).reshape(-1, 2)
|
||||||
properties = read_properties(stream)
|
properties = read_properties(stream)
|
||||||
return cls(struct_name=struct_name, xy=xy, properties=properties, colrow=colrow,
|
return cls(
|
||||||
invert_y=invert_y, mag=mag, angle_deg=angle_deg)
|
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:
|
def write(self, stream: IO[bytes]) -> int:
|
||||||
b = 0
|
b = 0
|
||||||
if self.colrow is None:
|
if self.colrow is None:
|
||||||
b += SREF.write(stream, None)
|
b += SREF.write(stream, None)
|
||||||
@ -189,7 +197,7 @@ class Reference(Element):
|
|||||||
b += STRANS.write(stream, int(self.invert_y) << 15)
|
b += STRANS.write(stream, int(self.invert_y) << 15)
|
||||||
if self.mag != 1:
|
if self.mag != 1:
|
||||||
b += MAG.write(stream, self.mag)
|
b += MAG.write(stream, self.mag)
|
||||||
if self.angle_deg !=0:
|
if self.angle_deg != 0:
|
||||||
b += ANGLE.write(stream, self.angle_deg)
|
b += ANGLE.write(stream, self.angle_deg)
|
||||||
|
|
||||||
if self.colrow is not None:
|
if self.colrow is not None:
|
||||||
@ -204,7 +212,7 @@ class Reference(Element):
|
|||||||
if self.colrow is not None:
|
if self.colrow is not None:
|
||||||
if self.xy.size != 6:
|
if self.xy.size != 6:
|
||||||
raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}')
|
raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}')
|
||||||
else:
|
else: # noqa: PLR5501
|
||||||
if self.xy.size != 2:
|
if self.xy.size != 2:
|
||||||
raise KlamathError(f'Expected size-2 xy. Got {self.xy}')
|
raise KlamathError(f'Expected size-2 xy. Got {self.xy}')
|
||||||
|
|
||||||
@ -216,24 +224,24 @@ class Boundary(Element):
|
|||||||
"""
|
"""
|
||||||
__slots__ = ('layer', 'xy', 'properties')
|
__slots__ = ('layer', 'xy', 'properties')
|
||||||
|
|
||||||
layer: Tuple[int, int]
|
layer: tuple[int, int]
|
||||||
""" (layer, data_type) tuple """
|
""" (layer, data_type) tuple """
|
||||||
|
|
||||||
xy: numpy.ndarray
|
xy: NDArray[numpy.int32]
|
||||||
""" Ordered vertices of the shape. First and last points should be identical. """
|
""" Ordered vertices of the shape. First and last points should be identical. """
|
||||||
|
|
||||||
properties: Dict[int, bytes]
|
properties: Mapping[int, bytes]
|
||||||
""" Properties for the element. """
|
""" Properties for the element. """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls: Type[B], stream: BinaryIO) -> B:
|
def read(cls: type[B], stream: IO[bytes]) -> B:
|
||||||
layer = LAYER.skip_and_read(stream)[0]
|
layer = LAYER.skip_and_read(stream)[0]
|
||||||
dtype = DATATYPE.read(stream)[0]
|
dtype = DATATYPE.read(stream)[0]
|
||||||
xy = XY.read(stream).reshape(-1, 2)
|
xy = XY.read(stream).reshape(-1, 2)
|
||||||
properties = read_properties(stream)
|
properties = read_properties(stream)
|
||||||
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
||||||
|
|
||||||
def write(self, stream: BinaryIO) -> int:
|
def write(self, stream: IO[bytes]) -> int:
|
||||||
b = BOUNDARY.write(stream, None)
|
b = BOUNDARY.write(stream, None)
|
||||||
b += LAYER.write(stream, self.layer[0])
|
b += LAYER.write(stream, self.layer[0])
|
||||||
b += DATATYPE.write(stream, self.layer[1])
|
b += DATATYPE.write(stream, self.layer[1])
|
||||||
@ -253,7 +261,7 @@ class Path(Element):
|
|||||||
"""
|
"""
|
||||||
__slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension')
|
__slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension')
|
||||||
|
|
||||||
layer: Tuple[int, int]
|
layer: tuple[int, int]
|
||||||
""" (layer, data_type) tuple """
|
""" (layer, data_type) tuple """
|
||||||
|
|
||||||
path_type: int
|
path_type: int
|
||||||
@ -262,17 +270,17 @@ class Path(Element):
|
|||||||
width: int
|
width: int
|
||||||
""" Path width """
|
""" Path width """
|
||||||
|
|
||||||
extension: Tuple[int, int]
|
extension: tuple[int, int]
|
||||||
""" Extension when using path_type=4. Ignored otherwise. """
|
""" Extension when using path_type=4. Ignored otherwise. """
|
||||||
|
|
||||||
xy: numpy.ndarray
|
xy: NDArray[numpy.int32]
|
||||||
""" Path centerline coordinates """
|
""" Path centerline coordinates """
|
||||||
|
|
||||||
properties: Dict[int, bytes]
|
properties: Mapping[int, bytes]
|
||||||
""" Properties for the element. """
|
""" Properties for the element. """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls: Type[P], stream: BinaryIO) -> P:
|
def read(cls: type[P], stream: IO[bytes]) -> P:
|
||||||
path_type = 0
|
path_type = 0
|
||||||
width = 0
|
width = 0
|
||||||
bgn_ext = 0
|
bgn_ext = 0
|
||||||
@ -299,7 +307,7 @@ class Path(Element):
|
|||||||
properties=properties, extension=(bgn_ext, end_ext),
|
properties=properties, extension=(bgn_ext, end_ext),
|
||||||
path_type=path_type, width=width)
|
path_type=path_type, width=width)
|
||||||
|
|
||||||
def write(self, stream: BinaryIO) -> int:
|
def write(self, stream: IO[bytes]) -> int:
|
||||||
b = PATH.write(stream, None)
|
b = PATH.write(stream, None)
|
||||||
b += LAYER.write(stream, self.layer[0])
|
b += LAYER.write(stream, self.layer[0])
|
||||||
b += DATATYPE.write(stream, self.layer[1])
|
b += DATATYPE.write(stream, self.layer[1])
|
||||||
@ -327,24 +335,24 @@ class Box(Element):
|
|||||||
"""
|
"""
|
||||||
__slots__ = ('layer', 'xy', 'properties')
|
__slots__ = ('layer', 'xy', 'properties')
|
||||||
|
|
||||||
layer: Tuple[int, int]
|
layer: tuple[int, int]
|
||||||
""" (layer, box_type) tuple """
|
""" (layer, box_type) tuple """
|
||||||
|
|
||||||
xy: numpy.ndarray
|
xy: NDArray[numpy.int32]
|
||||||
""" Box coordinates (5 pairs) """
|
""" Box coordinates (5 pairs) """
|
||||||
|
|
||||||
properties: Dict[int, bytes]
|
properties: Mapping[int, bytes]
|
||||||
""" Properties for the element. """
|
""" Properties for the element. """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls: Type[X], stream: BinaryIO) -> X:
|
def read(cls: type[X], stream: IO[bytes]) -> X:
|
||||||
layer = LAYER.skip_and_read(stream)[0]
|
layer = LAYER.skip_and_read(stream)[0]
|
||||||
dtype = BOXTYPE.read(stream)[0]
|
dtype = BOXTYPE.read(stream)[0]
|
||||||
xy = XY.read(stream).reshape(-1, 2)
|
xy = XY.read(stream).reshape(-1, 2)
|
||||||
properties = read_properties(stream)
|
properties = read_properties(stream)
|
||||||
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
||||||
|
|
||||||
def write(self, stream: BinaryIO) -> int:
|
def write(self, stream: IO[bytes]) -> int:
|
||||||
b = BOX.write(stream, None)
|
b = BOX.write(stream, None)
|
||||||
b += LAYER.write(stream, self.layer[0])
|
b += LAYER.write(stream, self.layer[0])
|
||||||
b += BOXTYPE.write(stream, self.layer[1])
|
b += BOXTYPE.write(stream, self.layer[1])
|
||||||
@ -361,24 +369,24 @@ class Node(Element):
|
|||||||
"""
|
"""
|
||||||
__slots__ = ('layer', 'xy', 'properties')
|
__slots__ = ('layer', 'xy', 'properties')
|
||||||
|
|
||||||
layer: Tuple[int, int]
|
layer: tuple[int, int]
|
||||||
""" (layer, node_type) tuple """
|
""" (layer, node_type) tuple """
|
||||||
|
|
||||||
xy: numpy.ndarray
|
xy: NDArray[numpy.int32]
|
||||||
""" 1-50 pairs of coordinates. """
|
""" 1-50 pairs of coordinates. """
|
||||||
|
|
||||||
properties: Dict[int, bytes]
|
properties: Mapping[int, bytes]
|
||||||
""" Properties for the element. """
|
""" Properties for the element. """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls: Type[N], stream: BinaryIO) -> N:
|
def read(cls: type[N], stream: IO[bytes]) -> N:
|
||||||
layer = LAYER.skip_and_read(stream)[0]
|
layer = LAYER.skip_and_read(stream)[0]
|
||||||
dtype = NODETYPE.read(stream)[0]
|
dtype = NODETYPE.read(stream)[0]
|
||||||
xy = XY.read(stream).reshape(-1, 2)
|
xy = XY.read(stream).reshape(-1, 2)
|
||||||
properties = read_properties(stream)
|
properties = read_properties(stream)
|
||||||
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
||||||
|
|
||||||
def write(self, stream: BinaryIO) -> int:
|
def write(self, stream: IO[bytes]) -> int:
|
||||||
b = NODE.write(stream, None)
|
b = NODE.write(stream, None)
|
||||||
b += LAYER.write(stream, self.layer[0])
|
b += LAYER.write(stream, self.layer[0])
|
||||||
b += NODETYPE.write(stream, self.layer[1])
|
b += NODETYPE.write(stream, self.layer[1])
|
||||||
@ -396,7 +404,7 @@ class Text(Element):
|
|||||||
__slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type',
|
__slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type',
|
||||||
'width', 'invert_y', 'mag', 'angle_deg', 'string')
|
'width', 'invert_y', 'mag', 'angle_deg', 'string')
|
||||||
|
|
||||||
layer: Tuple[int, int]
|
layer: tuple[int, int]
|
||||||
""" (layer, node_type) tuple """
|
""" (layer, node_type) tuple """
|
||||||
|
|
||||||
presentation: int
|
presentation: int
|
||||||
@ -421,17 +429,17 @@ class Text(Element):
|
|||||||
angle_deg: float
|
angle_deg: float
|
||||||
""" Rotation (ccw). Default 0. """
|
""" Rotation (ccw). Default 0. """
|
||||||
|
|
||||||
xy: numpy.ndarray
|
xy: NDArray[numpy.int32]
|
||||||
""" Position (1 pair only) """
|
""" Position (1 pair only) """
|
||||||
|
|
||||||
string: bytes
|
string: bytes
|
||||||
""" Text content """
|
""" Text content """
|
||||||
|
|
||||||
properties: Dict[int, bytes]
|
properties: Mapping[int, bytes]
|
||||||
""" Properties for the element. """
|
""" Properties for the element. """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls: Type[T], stream: BinaryIO) -> T:
|
def read(cls: type[T], stream: IO[bytes]) -> T:
|
||||||
path_type = 0
|
path_type = 0
|
||||||
presentation = 0
|
presentation = 0
|
||||||
invert_y = False
|
invert_y = False
|
||||||
@ -467,7 +475,7 @@ class Text(Element):
|
|||||||
string=string, presentation=presentation, path_type=path_type,
|
string=string, presentation=presentation, path_type=path_type,
|
||||||
width=width, invert_y=invert_y, mag=mag, angle_deg=angle_deg)
|
width=width, invert_y=invert_y, mag=mag, angle_deg=angle_deg)
|
||||||
|
|
||||||
def write(self, stream: BinaryIO) -> int:
|
def write(self, stream: IO[bytes]) -> int:
|
||||||
b = TEXT.write(stream, None)
|
b = TEXT.write(stream, None)
|
||||||
b += LAYER.write(stream, self.layer[0])
|
b += LAYER.write(stream, self.layer[0])
|
||||||
b += TEXTTYPE.write(stream, self.layer[1])
|
b += TEXTTYPE.write(stream, self.layer[1])
|
||||||
@ -481,7 +489,7 @@ class Text(Element):
|
|||||||
b += STRANS.write(stream, int(self.invert_y) << 15)
|
b += STRANS.write(stream, int(self.invert_y) << 15)
|
||||||
if self.mag != 1:
|
if self.mag != 1:
|
||||||
b += MAG.write(stream, self.mag)
|
b += MAG.write(stream, self.mag)
|
||||||
if self.angle_deg !=0:
|
if self.angle_deg != 0:
|
||||||
b += ANGLE.write(stream, self.angle_deg)
|
b += ANGLE.write(stream, self.angle_deg)
|
||||||
b += XY.write(stream, self.xy)
|
b += XY.write(stream, self.xy)
|
||||||
b += STRING.write(stream, self.string)
|
b += STRING.write(stream, self.string)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
File-level read/write functionality.
|
File-level read/write functionality.
|
||||||
"""
|
"""
|
||||||
from typing import List, Dict, Tuple, Optional, BinaryIO, TypeVar, Type, MutableMapping
|
from typing import IO, Self, TYPE_CHECKING
|
||||||
import io
|
import io
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -15,8 +15,8 @@ from .records import BGNSTR, STRNAME, ENDSTR, SNAME, COLROW, ENDEL
|
|||||||
from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
|
from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
|
||||||
from .elements import Element, Reference, Text, Box, Boundary, Path, Node
|
from .elements import Element, Reference, Text, Box, Boundary, Path, Node
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
FH = TypeVar('FH', bound='FileHeader')
|
from collections.abc import MutableMapping
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -45,7 +45,7 @@ class FileHeader:
|
|||||||
""" Last-accessed time """
|
""" Last-accessed time """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls: Type[FH], stream: BinaryIO) -> FH:
|
def read(cls: type[Self], stream: IO[bytes]) -> Self:
|
||||||
"""
|
"""
|
||||||
Read and construct a header from the provided stream.
|
Read and construct a header from the provided stream.
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ class FileHeader:
|
|||||||
Returns:
|
Returns:
|
||||||
FileHeader object
|
FileHeader object
|
||||||
"""
|
"""
|
||||||
version = HEADER.read(stream)[0]
|
_version = HEADER.read(stream)[0] # noqa: F841 # var is unused
|
||||||
mod_time, acc_time = BGNLIB.read(stream)
|
mod_time, acc_time = BGNLIB.read(stream)
|
||||||
name = LIBNAME.skip_and_read(stream)
|
name = LIBNAME.skip_and_read(stream)
|
||||||
uu, dbu = UNITS.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,
|
return cls(mod_time=mod_time, acc_time=acc_time, name=name,
|
||||||
user_units_per_db_unit=uu, meters_per_db_unit=dbu)
|
user_units_per_db_unit=uu, meters_per_db_unit=dbu)
|
||||||
|
|
||||||
def write(self, stream: BinaryIO) -> int:
|
def write(self, stream: IO[bytes]) -> int:
|
||||||
"""
|
"""
|
||||||
Write the header to a stream
|
Write the header to a stream
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ class FileHeader:
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
def scan_structs(stream: BinaryIO) -> Dict[bytes, int]:
|
def scan_structs(stream: IO[bytes]) -> dict[bytes, int]:
|
||||||
"""
|
"""
|
||||||
Scan through a GDS file, building a table of
|
Scan through a GDS file, building a table of
|
||||||
{b'structure_name': byte_offset}.
|
{b'structure_name': byte_offset}.
|
||||||
@ -107,7 +107,7 @@ def scan_structs(stream: BinaryIO) -> Dict[bytes, int]:
|
|||||||
return positions
|
return positions
|
||||||
|
|
||||||
|
|
||||||
def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]:
|
def try_read_struct(stream: IO[bytes]) -> tuple[bytes, list[Element]] | None:
|
||||||
"""
|
"""
|
||||||
Skip to the next structure and attempt to read it.
|
Skip to the next structure and attempt to read it.
|
||||||
|
|
||||||
@ -125,12 +125,13 @@ def try_read_struct(stream: BinaryIO) -> Optional[Tuple[bytes, List[Element]]]:
|
|||||||
return name, elements
|
return name, elements
|
||||||
|
|
||||||
|
|
||||||
def write_struct(stream: BinaryIO,
|
def write_struct(
|
||||||
name: bytes,
|
stream: IO[bytes],
|
||||||
elements: List[Element],
|
name: bytes,
|
||||||
cre_time: datetime = datetime(1900, 1, 1),
|
elements: list[Element],
|
||||||
mod_time: datetime = datetime(1900, 1, 1),
|
cre_time: datetime = datetime(1900, 1, 1),
|
||||||
) -> int:
|
mod_time: datetime = datetime(1900, 1, 1),
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Write a structure to the provided stream.
|
Write a structure to the provided stream.
|
||||||
|
|
||||||
@ -150,7 +151,7 @@ def write_struct(stream: BinaryIO,
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
def read_elements(stream: BinaryIO) -> List[Element]:
|
def read_elements(stream: IO[bytes]) -> list[Element]:
|
||||||
"""
|
"""
|
||||||
Read elements from the stream until an ENDSTR
|
Read elements from the stream until an ENDSTR
|
||||||
record is encountered. The ENDSTR record is also
|
record is encountered. The ENDSTR record is also
|
||||||
@ -162,7 +163,7 @@ def read_elements(stream: BinaryIO) -> List[Element]:
|
|||||||
Returns:
|
Returns:
|
||||||
List of element objects.
|
List of element objects.
|
||||||
"""
|
"""
|
||||||
data: List[Element] = []
|
data: list[Element] = []
|
||||||
size, tag = Record.read_header(stream)
|
size, tag = Record.read_header(stream)
|
||||||
while tag != ENDSTR.tag:
|
while tag != ENDSTR.tag:
|
||||||
if tag == BOUNDARY.tag:
|
if tag == BOUNDARY.tag:
|
||||||
@ -175,9 +176,7 @@ def read_elements(stream: BinaryIO) -> List[Element]:
|
|||||||
data.append(Box.read(stream))
|
data.append(Box.read(stream))
|
||||||
elif tag == TEXT.tag:
|
elif tag == TEXT.tag:
|
||||||
data.append(Text.read(stream))
|
data.append(Text.read(stream))
|
||||||
elif tag == SREF.tag:
|
elif tag in (SREF.tag, AREF.tag):
|
||||||
data.append(Reference.read(stream))
|
|
||||||
elif tag == AREF.tag:
|
|
||||||
data.append(Reference.read(stream))
|
data.append(Reference.read(stream))
|
||||||
else:
|
else:
|
||||||
# don't care, skip
|
# don't care, skip
|
||||||
@ -186,7 +185,7 @@ def read_elements(stream: BinaryIO) -> List[Element]:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def scan_hierarchy(stream: BinaryIO) -> Dict[bytes, Dict[bytes, int]]:
|
def scan_hierarchy(stream: IO[bytes]) -> dict[bytes, dict[bytes, int]]:
|
||||||
"""
|
"""
|
||||||
Scan through a GDS file, building a table of instance counts
|
Scan through a GDS file, building a table of instance counts
|
||||||
`{b'structure_name': {b'ref_name': count}}`.
|
`{b'structure_name': {b'ref_name': count}}`.
|
||||||
@ -223,7 +222,7 @@ def scan_hierarchy(stream: BinaryIO) -> Dict[bytes, Dict[bytes, int]]:
|
|||||||
elif tag == ENDEL.tag:
|
elif tag == ENDEL.tag:
|
||||||
if ref_count is None:
|
if ref_count is None:
|
||||||
ref_count = 1
|
ref_count = 1
|
||||||
assert(ref_name is not None)
|
assert ref_name is not None
|
||||||
cur_structure[ref_name] += ref_count
|
cur_structure[ref_name] += ref_count
|
||||||
else:
|
else:
|
||||||
stream.seek(size, io.SEEK_CUR)
|
stream.seek(size, io.SEEK_CUR)
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
Generic record-level read/write functionality.
|
Generic record-level read/write functionality.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, Sequence, BinaryIO
|
from typing import IO, ClassVar, Self, Generic, TypeVar
|
||||||
from typing import TypeVar, List, Tuple, ClassVar, Type
|
from collections.abc import Sequence
|
||||||
import struct
|
import struct
|
||||||
import io
|
import io
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
import numpy # type: ignore
|
import numpy
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from .basic import KlamathError
|
from .basic import KlamathError
|
||||||
from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray
|
from .basic import parse_int2, parse_int4, parse_real8, parse_datetime, parse_bitarray
|
||||||
@ -17,9 +18,11 @@ from .basic import parse_ascii, pack_ascii, read
|
|||||||
|
|
||||||
|
|
||||||
_RECORD_HEADER_FMT = struct.Struct('>HH')
|
_RECORD_HEADER_FMT = struct.Struct('>HH')
|
||||||
|
II = TypeVar('II') # Input type
|
||||||
|
OO = TypeVar('OO') # Output type
|
||||||
|
|
||||||
|
|
||||||
def write_record_header(stream: BinaryIO, data_size: int, tag: int) -> int:
|
def write_record_header(stream: IO[bytes], data_size: int, tag: int) -> int:
|
||||||
record_size = data_size + 4
|
record_size = data_size + 4
|
||||||
if record_size > 0xFFFF:
|
if record_size > 0xFFFF:
|
||||||
raise KlamathError(f'Record size is too big: {record_size}')
|
raise KlamathError(f'Record size is too big: {record_size}')
|
||||||
@ -27,7 +30,7 @@ def write_record_header(stream: BinaryIO, data_size: int, tag: int) -> int:
|
|||||||
return stream.write(header)
|
return stream.write(header)
|
||||||
|
|
||||||
|
|
||||||
def read_record_header(stream: BinaryIO) -> Tuple[int, int]:
|
def read_record_header(stream: IO[bytes]) -> tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
Read a record's header (size and tag).
|
Read a record's header (size and tag).
|
||||||
Args:
|
Args:
|
||||||
@ -46,49 +49,46 @@ def read_record_header(stream: BinaryIO) -> Tuple[int, int]:
|
|||||||
return data_size, tag
|
return data_size, tag
|
||||||
|
|
||||||
|
|
||||||
def expect_record(stream: BinaryIO, tag: int) -> int:
|
def expect_record(stream: IO[bytes], tag: int) -> int:
|
||||||
data_size, actual_tag = read_record_header(stream)
|
data_size, actual_tag = read_record_header(stream)
|
||||||
if tag != actual_tag:
|
if tag != actual_tag:
|
||||||
raise KlamathError(f'Unexpected record! Got tag 0x{actual_tag:04x}, expected 0x{tag:04x}')
|
raise KlamathError(f'Unexpected record! Got tag 0x{actual_tag:04x}, expected 0x{tag:04x}')
|
||||||
return data_size
|
return data_size
|
||||||
|
|
||||||
|
|
||||||
R = TypeVar('R', bound='Record')
|
class Record(Generic[II, OO], metaclass=ABCMeta):
|
||||||
|
|
||||||
|
|
||||||
class Record(metaclass=ABCMeta):
|
|
||||||
tag: ClassVar[int] = -1
|
tag: ClassVar[int] = -1
|
||||||
expected_size: ClassVar[Optional[int]] = None
|
expected_size: ClassVar[int | None] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_size(cls, size: int):
|
def check_size(cls: type[Self], size: int) -> None:
|
||||||
if cls.expected_size is not None and size != cls.expected_size:
|
if cls.expected_size is not None and size != cls.expected_size:
|
||||||
raise KlamathError(f'Expected size {cls.expected_size}, got {size}')
|
raise KlamathError(f'Expected size {cls.expected_size}, got {size}')
|
||||||
|
|
||||||
@classmethod
|
@classmethod # noqa: B027 Intentionally non-abstract
|
||||||
def check_data(cls, data):
|
def check_data(cls: type[Self], data: II) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def read_data(cls, stream: BinaryIO, size: int):
|
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> OO:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def pack_data(cls, data) -> bytes:
|
def pack_data(cls: type[Self], data: II) -> bytes:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def read_header(stream: BinaryIO) -> Tuple[int, int]:
|
def read_header(stream: IO[bytes]) -> tuple[int, int]:
|
||||||
return read_record_header(stream)
|
return read_record_header(stream)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write_header(cls, stream: BinaryIO, data_size: int) -> int:
|
def write_header(cls: type[Self], stream: IO[bytes], data_size: int) -> int:
|
||||||
return write_record_header(stream, data_size, cls.tag)
|
return write_record_header(stream, data_size, cls.tag)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def skip_past(cls, stream: BinaryIO) -> bool:
|
def skip_past(cls: type[Self], stream: IO[bytes]) -> bool:
|
||||||
"""
|
"""
|
||||||
Skip to the end of the next occurence of this record.
|
Skip to the end of the next occurence of this record.
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ class Record(metaclass=ABCMeta):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def skip_and_read(cls, stream: BinaryIO):
|
def skip_and_read(cls: type[Self], stream: IO[bytes]) -> OO:
|
||||||
size, tag = Record.read_header(stream)
|
size, tag = Record.read_header(stream)
|
||||||
while tag != cls.tag:
|
while tag != cls.tag:
|
||||||
stream.seek(size, io.SEEK_CUR)
|
stream.seek(size, io.SEEK_CUR)
|
||||||
@ -119,90 +119,90 @@ class Record(metaclass=ABCMeta):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls: Type[R], stream: BinaryIO):
|
def read(cls: type[Self], stream: IO[bytes]) -> OO:
|
||||||
size = expect_record(stream, cls.tag)
|
size = expect_record(stream, cls.tag)
|
||||||
data = cls.read_data(stream, size)
|
data = cls.read_data(stream, size)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write(cls, stream: BinaryIO, data) -> int:
|
def write(cls: type[Self], stream: IO[bytes], data: II) -> int:
|
||||||
data_bytes = cls.pack_data(data)
|
data_bytes = cls.pack_data(data)
|
||||||
b = cls.write_header(stream, len(data_bytes))
|
b = cls.write_header(stream, len(data_bytes))
|
||||||
b += stream.write(data_bytes)
|
b += stream.write(data_bytes)
|
||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
class NoDataRecord(Record):
|
class NoDataRecord(Record[None, None]):
|
||||||
expected_size: ClassVar[Optional[int]] = 0
|
expected_size: ClassVar[int | None] = 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_data(cls, stream: BinaryIO, size: int) -> None:
|
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> None:
|
||||||
stream.read(size)
|
stream.read(size)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pack_data(cls, data: None) -> bytes:
|
def pack_data(cls: type[Self], data: None) -> bytes:
|
||||||
if data is not None:
|
if data is not None:
|
||||||
raise KlamathError('?? Packing {data} into NoDataRecord??')
|
raise KlamathError('?? Packing {data!r} into NoDataRecord??')
|
||||||
return b''
|
return b''
|
||||||
|
|
||||||
|
|
||||||
class BitArrayRecord(Record):
|
class BitArrayRecord(Record[int, int]):
|
||||||
expected_size: ClassVar[Optional[int]] = 2
|
expected_size: ClassVar[int | None] = 2
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_data(cls, stream: BinaryIO, size: int) -> int:
|
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> int: # noqa: ARG003 size unused
|
||||||
return parse_bitarray(read(stream, 2))
|
return parse_bitarray(read(stream, 2))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pack_data(cls, data: int) -> bytes:
|
def pack_data(cls: type[Self], data: int) -> bytes:
|
||||||
return pack_bitarray(data)
|
return pack_bitarray(data)
|
||||||
|
|
||||||
|
|
||||||
class Int2Record(Record):
|
class Int2Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int16]]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
|
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int16]:
|
||||||
return parse_int2(read(stream, size))
|
return parse_int2(read(stream, size))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||||
return pack_int2(data)
|
return pack_int2(data)
|
||||||
|
|
||||||
|
|
||||||
class Int4Record(Record):
|
class Int4Record(Record[NDArray[numpy.integer] | Sequence[int] | int, NDArray[numpy.int32]]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
|
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.int32]:
|
||||||
return parse_int4(read(stream, size))
|
return parse_int4(read(stream, size))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
def pack_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||||
return pack_int4(data)
|
return pack_int4(data)
|
||||||
|
|
||||||
|
|
||||||
class Real8Record(Record):
|
class Real8Record(Record[Sequence[float] | float, NDArray[numpy.float64]]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_data(cls, stream: BinaryIO, size: int) -> numpy.ndarray:
|
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> NDArray[numpy.float64]:
|
||||||
return parse_real8(read(stream, size))
|
return parse_real8(read(stream, size))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pack_data(cls, data: Sequence[int]) -> bytes:
|
def pack_data(cls: type[Self], data: Sequence[float] | float) -> bytes:
|
||||||
return pack_real8(data)
|
return pack_real8(data)
|
||||||
|
|
||||||
|
|
||||||
class ASCIIRecord(Record):
|
class ASCIIRecord(Record[bytes, bytes]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_data(cls, stream: BinaryIO, size: int) -> bytes:
|
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> bytes:
|
||||||
return parse_ascii(read(stream, size))
|
return parse_ascii(read(stream, size))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pack_data(cls, data: bytes) -> bytes:
|
def pack_data(cls: type[Self], data: bytes) -> bytes:
|
||||||
return pack_ascii(data)
|
return pack_ascii(data)
|
||||||
|
|
||||||
|
|
||||||
class DateTimeRecord(Record):
|
class DateTimeRecord(Record[Sequence[datetime], list[datetime]]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_data(cls, stream: BinaryIO, size: int) -> List[datetime]:
|
def read_data(cls: type[Self], stream: IO[bytes], size: int) -> list[datetime]:
|
||||||
return parse_datetime(read(stream, size))
|
return parse_datetime(read(stream, size))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pack_data(cls, data: Sequence[datetime]) -> bytes:
|
def pack_data(cls: type[Self], data: Sequence[datetime]) -> bytes:
|
||||||
return pack_datetime(data)
|
return pack_datetime(data)
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Record type and tag definitions
|
Record type and tag definitions
|
||||||
"""
|
"""
|
||||||
from typing import Sequence
|
from typing import Self
|
||||||
|
from collections.abc import Sequence, Sized
|
||||||
|
import numpy
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
from .basic import KlamathError
|
||||||
from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record
|
from .record import NoDataRecord, BitArrayRecord, Int2Record, Int4Record, Real8Record
|
||||||
from .record import ASCIIRecord, DateTimeRecord
|
from .record import ASCIIRecord, DateTimeRecord
|
||||||
|
|
||||||
@ -111,7 +115,7 @@ class PRESENTATION(BitArrayRecord):
|
|||||||
|
|
||||||
|
|
||||||
class SPACING(Int2Record):
|
class SPACING(Int2Record):
|
||||||
tag = 0x1802 #Not sure about 02; Unused
|
tag = 0x1802 # Not sure about 02; Unused
|
||||||
|
|
||||||
|
|
||||||
class STRING(ASCIIRecord):
|
class STRING(ASCIIRecord):
|
||||||
@ -133,29 +137,29 @@ class ANGLE(Real8Record):
|
|||||||
|
|
||||||
|
|
||||||
class UINTEGER(Int2Record):
|
class UINTEGER(Int2Record):
|
||||||
tag = 0x1d02 #Unused; not sure about 02
|
tag = 0x1d02 # Unused; not sure about 02
|
||||||
|
|
||||||
|
|
||||||
class USTRING(ASCIIRecord):
|
class USTRING(ASCIIRecord):
|
||||||
tag = 0x1e06 #Unused; not sure about 06
|
tag = 0x1e06 # Unused; not sure about 06
|
||||||
|
|
||||||
|
|
||||||
class REFLIBS(ASCIIRecord):
|
class REFLIBS(ASCIIRecord):
|
||||||
tag = 0x1f06
|
tag = 0x1f06
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_size(cls, size: int):
|
def check_size(cls: type[Self], size: int) -> None:
|
||||||
if size != 0 and size % 44 != 0:
|
if size != 0 and size % 44 != 0:
|
||||||
raise Exception(f'Expected size to be multiple of 44, got {size}')
|
raise KlamathError(f'Expected size to be multiple of 44, got {size}')
|
||||||
|
|
||||||
|
|
||||||
class FONTS(ASCIIRecord):
|
class FONTS(ASCIIRecord):
|
||||||
tag = 0x2006
|
tag = 0x2006
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_size(cls, size: int):
|
def check_size(cls: type[Self], size: int) -> None:
|
||||||
if size != 0 and size % 44 != 0:
|
if size != 0 and size % 44 != 0:
|
||||||
raise Exception(f'Expected size to be multiple of 44, got {size}')
|
raise KlamathError(f'Expected size to be multiple of 44, got {size}')
|
||||||
|
|
||||||
|
|
||||||
class PATHTYPE(Int2Record):
|
class PATHTYPE(Int2Record):
|
||||||
@ -168,26 +172,26 @@ class GENERATIONS(Int2Record):
|
|||||||
expected_size = 2
|
expected_size = 2
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_data(cls, data: Sequence[int]):
|
def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None:
|
||||||
if len(data) != 1:
|
if not isinstance(data, Sized) or len(data) != 1:
|
||||||
raise Exception(f'Expected exactly one integer, got {data}')
|
raise KlamathError(f'Expected exactly one integer, got {data}')
|
||||||
|
|
||||||
|
|
||||||
class ATTRTABLE(ASCIIRecord):
|
class ATTRTABLE(ASCIIRecord):
|
||||||
tag = 0x2306
|
tag = 0x2306
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_size(cls, size: int):
|
def check_size(cls: type[Self], size: int) -> None:
|
||||||
if size > 44:
|
if size > 44:
|
||||||
raise Exception(f'Expected size <= 44, got {size}')
|
raise KlamathError(f'Expected size <= 44, got {size}')
|
||||||
|
|
||||||
|
|
||||||
class STYPTABLE(ASCIIRecord):
|
class STYPTABLE(ASCIIRecord):
|
||||||
tag = 0x2406 #UNUSED, not sure about 06
|
tag = 0x2406 # UNUSED, not sure about 06
|
||||||
|
|
||||||
|
|
||||||
class STRTYPE(Int2Record):
|
class STRTYPE(Int2Record):
|
||||||
tag = 0x2502 #UNUSED
|
tag = 0x2502 # UNUSED
|
||||||
|
|
||||||
|
|
||||||
class ELFLAGS(BitArrayRecord):
|
class ELFLAGS(BitArrayRecord):
|
||||||
@ -266,9 +270,9 @@ class FORMAT(Int2Record):
|
|||||||
expected_size = 2
|
expected_size = 2
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_data(cls, data: Sequence[int]):
|
def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None:
|
||||||
if len(data) != 1:
|
if not isinstance(data, Sized) or len(data) != 1:
|
||||||
raise Exception(f'Expected exactly one integer, got {data}')
|
raise KlamathError(f'Expected exactly one integer, got {data}')
|
||||||
|
|
||||||
|
|
||||||
class MASK(ASCIIRecord):
|
class MASK(ASCIIRecord):
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
import numpy # type: ignore
|
import numpy
|
||||||
from numpy.testing import assert_array_equal # type: ignore
|
from datetime import datetime
|
||||||
|
from numpy.testing import assert_array_equal
|
||||||
|
|
||||||
from .basic import parse_bitarray, parse_int2, parse_int4, parse_real8, parse_ascii
|
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 pack_bitarray, pack_int2, pack_int4, pack_real8, pack_ascii
|
||||||
from .basic import decode_real8, encode_real8
|
from .basic import decode_real8, encode_real8, parse_datetime
|
||||||
|
|
||||||
from .basic import KlamathError
|
from .basic import KlamathError
|
||||||
|
|
||||||
|
|
||||||
def test_parse_bitarray():
|
def test_parse_bitarray() -> None:
|
||||||
assert(parse_bitarray(b'59') == 13625)
|
assert parse_bitarray(b'59') == 13625
|
||||||
assert(parse_bitarray(b'\0\0') == 0)
|
assert parse_bitarray(b'\0\0') == 0
|
||||||
assert(parse_bitarray(b'\xff\xff') == 65535)
|
assert parse_bitarray(b'\xff\xff') == 65535
|
||||||
|
|
||||||
# 4 bytes (too long)
|
# 4 bytes (too long)
|
||||||
with pytest.raises(KlamathError):
|
with pytest.raises(KlamathError):
|
||||||
@ -25,7 +26,7 @@ def test_parse_bitarray():
|
|||||||
parse_bitarray(b'')
|
parse_bitarray(b'')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_int2():
|
def test_parse_int2() -> None:
|
||||||
assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0))
|
assert_array_equal(parse_int2(b'59\xff\xff\0\0'), (13625, -1, 0))
|
||||||
|
|
||||||
# odd length
|
# odd length
|
||||||
@ -37,7 +38,7 @@ def test_parse_int2():
|
|||||||
parse_int2(b'')
|
parse_int2(b'')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_int4():
|
def test_parse_int4() -> None:
|
||||||
assert_array_equal(parse_int4(b'4321'), (875770417,))
|
assert_array_equal(parse_int4(b'4321'), (875770417,))
|
||||||
|
|
||||||
# length % 4 != 0
|
# length % 4 != 0
|
||||||
@ -49,17 +50,17 @@ def test_parse_int4():
|
|||||||
parse_int4(b'')
|
parse_int4(b'')
|
||||||
|
|
||||||
|
|
||||||
def test_decode_real8():
|
def test_decode_real8() -> None:
|
||||||
# zeroes
|
# zeroes
|
||||||
assert(decode_real8(numpy.array([0x0])) == 0)
|
assert decode_real8(numpy.array([0x0])) == 0
|
||||||
assert(decode_real8(numpy.array([1<<63])) == 0) # negative
|
assert decode_real8(numpy.array([1 << 63])) == 0 # negative
|
||||||
assert(decode_real8(numpy.array([0xff << 56])) == 0) # denormalized
|
assert decode_real8(numpy.array([0xff << 56])) == 0 # denormalized
|
||||||
|
|
||||||
assert(decode_real8(numpy.array([0x4110 << 48])) == 1.0)
|
assert decode_real8(numpy.array([0x4110 << 48])) == 1.0
|
||||||
assert(decode_real8(numpy.array([0xC120 << 48])) == -2.0)
|
assert decode_real8(numpy.array([0xC120 << 48])) == -2.0
|
||||||
|
|
||||||
|
|
||||||
def test_parse_real8():
|
def test_parse_real8() -> None:
|
||||||
packed = struct.pack('>3Q', 0x0, 0x4110_0000_0000_0000, 0xC120_0000_0000_0000)
|
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))
|
assert_array_equal(parse_real8(packed), (0.0, 1.0, -2.0))
|
||||||
|
|
||||||
@ -72,48 +73,59 @@ def test_parse_real8():
|
|||||||
parse_real8(b'')
|
parse_real8(b'')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_ascii():
|
def test_parse_ascii() -> None:
|
||||||
# # empty data Now allowed!
|
# # empty data Now allowed!
|
||||||
# with pytest.raises(KlamathError):
|
# with pytest.raises(KlamathError):
|
||||||
# parse_ascii(b'')
|
# parse_ascii(b'')
|
||||||
|
|
||||||
assert(parse_ascii(b'12345') == b'12345')
|
assert parse_ascii(b'12345') == b'12345'
|
||||||
assert(parse_ascii(b'12345\0') == b'12345') # strips trailing null byte
|
assert parse_ascii(b'12345\0') == b'12345' # strips trailing null byte
|
||||||
|
|
||||||
|
|
||||||
def test_pack_bitarray():
|
def test_pack_bitarray() -> None:
|
||||||
packed = pack_bitarray(321)
|
packed = pack_bitarray(321)
|
||||||
assert(len(packed) == 2)
|
assert len(packed) == 2
|
||||||
assert(packed == struct.pack('>H', 321))
|
assert packed == struct.pack('>H', 321)
|
||||||
|
|
||||||
|
|
||||||
def test_pack_int2():
|
def test_pack_int2() -> None:
|
||||||
packed = pack_int2((3, 2, 1))
|
packed = pack_int2((3, 2, 1))
|
||||||
assert(len(packed) == 3*2)
|
assert len(packed) == 3 * 2
|
||||||
assert(packed == struct.pack('>3h', 3, 2, 1))
|
assert packed == struct.pack('>3h', 3, 2, 1)
|
||||||
assert(pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1))
|
assert pack_int2([-3, 2, -1]) == struct.pack('>3h', -3, 2, -1)
|
||||||
|
|
||||||
|
|
||||||
def test_pack_int4():
|
def test_pack_int4() -> None:
|
||||||
packed = pack_int4((3, 2, 1))
|
packed = pack_int4((3, 2, 1))
|
||||||
assert(len(packed) == 3*4)
|
assert len(packed) == 3 * 4
|
||||||
assert(packed == struct.pack('>3l', 3, 2, 1))
|
assert packed == struct.pack('>3l', 3, 2, 1)
|
||||||
assert(pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1))
|
assert pack_int4([-3, 2, -1]) == struct.pack('>3l', -3, 2, -1)
|
||||||
|
|
||||||
|
|
||||||
def test_encode_real8():
|
def test_encode_real8() -> None:
|
||||||
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))
|
arr = numpy.array((1.0, -2.0, 1e-9, 1e-3, 1e-12))
|
||||||
assert_array_equal(decode_real8(encode_real8(arr)), arr)
|
assert_array_equal(decode_real8(encode_real8(arr)), arr)
|
||||||
|
|
||||||
|
|
||||||
def test_pack_real8():
|
def test_pack_real8() -> None:
|
||||||
reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12)
|
reals = (0, 1, -1, 0.5, 1e-9, 1e-3, 1e-12)
|
||||||
packed = pack_real8(reals)
|
packed = pack_real8(reals)
|
||||||
assert(len(packed) == len(reals) * 8)
|
assert len(packed) == len(reals) * 8
|
||||||
assert_array_equal(parse_real8(packed), reals)
|
assert_array_equal(parse_real8(packed), reals)
|
||||||
|
|
||||||
|
|
||||||
def test_pack_ascii():
|
def test_pack_ascii() -> None:
|
||||||
assert(pack_ascii(b'4321') == b'4321')
|
assert pack_ascii(b'4321') == b'4321'
|
||||||
assert(pack_ascii(b'321') == b'321\0')
|
assert pack_ascii(b'321') == b'321\0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_date() -> None:
|
||||||
|
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
|
||||||
|
92
pyproject.toml
Normal file
92
pyproject.toml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
[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.11"
|
||||||
|
include = [
|
||||||
|
"LICENSE.md"
|
||||||
|
]
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = [
|
||||||
|
"numpy>=1.26",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
path = "klamath/__init__.py"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
exclude = [
|
||||||
|
".git",
|
||||||
|
"dist",
|
||||||
|
]
|
||||||
|
line-length = 145
|
||||||
|
indent-width = 4
|
||||||
|
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
lint.select = [
|
||||||
|
"NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG",
|
||||||
|
"C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT",
|
||||||
|
"ARG", "PL", "R", "TRY",
|
||||||
|
"G010", "G101", "G201", "G202",
|
||||||
|
"Q002", "Q003", "Q004",
|
||||||
|
]
|
||||||
|
lint.ignore = [
|
||||||
|
#"ANN001", # No annotation
|
||||||
|
"ANN002", # *args
|
||||||
|
"ANN003", # **kwargs
|
||||||
|
"ANN401", # Any
|
||||||
|
"ANN101", # self: Self
|
||||||
|
"SIM108", # single-line if / else assignment
|
||||||
|
"RET504", # x=y+z; return x
|
||||||
|
"PIE790", # unnecessary pass
|
||||||
|
"ISC003", # non-implicit string concatenation
|
||||||
|
"C408", # dict(x=y) instead of {'x': y}
|
||||||
|
"PLR09", # Too many xxx
|
||||||
|
"PLR2004", # magic number
|
||||||
|
"PLC0414", # import x as x
|
||||||
|
"TRY003", # Long exception message
|
||||||
|
]
|
||||||
|
|
63
setup.py
63
setup.py
@ -1,63 +0,0 @@
|
|||||||
#!/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…
x
Reference in New Issue
Block a user