Compare commits

...

9 Commits

Author SHA1 Message Date
76511b95e6 gdsii_arrow wip 2025-04-21 19:07:26 -07:00
88bd5e897e split out _read_to_arrow
for ease of debugging
2025-04-21 19:07:26 -07:00
dc89491694 actually make use of raw mode 2025-04-21 19:07:26 -07:00
jan
de9714041f add gdsii_arrow 2025-04-21 19:07:26 -07:00
35e28acb89 [polygon] Only call rotate if necessary 2025-04-21 19:07:21 -07:00
jan
c1bfee1ddd [library] minor stylistic cleanup 2025-04-15 17:34:05 -07:00
jan
560c165f2e remove deprecated rule from ignore list 2025-04-15 17:26:33 -07:00
jan
284c7e4fd0 Use quoted first arg for cast()
ruff rule TC006
2025-04-15 17:25:56 -07:00
jan
1eac3baf6a [pattern] add arg to , useful for whole-library scaling 2025-04-15 17:21:49 -07:00
16 changed files with 436 additions and 65 deletions

View File

@ -169,11 +169,11 @@ def ell(
'emax', 'max_extension', 'emax', 'max_extension',
'min_past_furthest',): 'min_past_furthest',):
if numpy.size(bound) == 2: if numpy.size(bound) == 2:
bound = cast(Sequence[float], bound) bound = cast('Sequence[float]', bound)
rot_bound = (rot_matrix @ ((bound[0], 0), rot_bound = (rot_matrix @ ((bound[0], 0),
(0, bound[1])))[0, :] (0, bound[1])))[0, :]
else: else:
bound = cast(float, bound) bound = cast('float', bound)
rot_bound = numpy.array(bound) rot_bound = numpy.array(bound)
if rot_bound < 0: if rot_bound < 0:
@ -185,10 +185,10 @@ def ell(
offsets += rot_bound.min() - offsets.max() offsets += rot_bound.min() - offsets.max()
else: else:
if numpy.size(bound) == 2: if numpy.size(bound) == 2:
bound = cast(Sequence[float], bound) bound = cast('Sequence[float]', bound)
rot_bound = (rot_matrix @ bound)[0] rot_bound = (rot_matrix @ bound)[0]
else: else:
bound = cast(float, bound) bound = cast('float', bound)
neg = (direction + pi / 4) % (2 * pi) > pi neg = (direction + pi / 4) % (2 * pi) > pi
rot_bound = -bound if neg else bound rot_bound = -bound if neg else bound

View File

@ -132,7 +132,7 @@ def writefile(
with tmpfile(path) as base_stream: with tmpfile(path) as base_stream:
streams: tuple[Any, ...] = (base_stream,) streams: tuple[Any, ...] = (base_stream,)
if path.suffix == '.gz': if path.suffix == '.gz':
gz_stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb')) gz_stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
streams = (gz_stream,) + streams streams = (gz_stream,) + streams
else: else:
gz_stream = base_stream gz_stream = base_stream

View File

@ -145,7 +145,7 @@ def writefile(
with tmpfile(path) as base_stream: with tmpfile(path) as base_stream:
streams: tuple[Any, ...] = (base_stream,) streams: tuple[Any, ...] = (base_stream,)
if path.suffix == '.gz': if path.suffix == '.gz':
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6)) stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
streams = (stream,) + streams streams = (stream,) + streams
else: else:
stream = base_stream stream = base_stream

364
masque/file/gdsii_arrow.py Normal file
View File

@ -0,0 +1,364 @@
"""
GDSII file format readers and writers using the `klamath` library.
Note that GDSII references follow the same convention as `masque`,
with this order of operations:
1. Mirroring
2. Rotation
3. Scaling
4. Offset and array expansion (no mirroring/rotation/scaling applied to offsets)
Scaling, rotation, and mirroring apply to individual instances, not grid
vectors or offsets.
Notes:
* absolute positioning is not supported
* PLEX is not supported
* ELFLAGS are not supported
* GDS does not support library- or structure-level annotations
* GDS creation/modification/access times are set to 1900-01-01 for reproducibility.
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
"""
from typing import IO, cast, Any
from collections.abc import Iterable, Mapping, Callable
import io
import mmap
import logging
import pathlib
import gzip
import string
from pprint import pformat
import numpy
from numpy.typing import ArrayLike, NDArray
from numpy.testing import assert_equal
import pyarrow
from pyarrow.cffi import ffi
from .utils import is_gzipped, tmpfile
from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
from ..shapes import Polygon, Path
from ..repetition import Grid
from ..utils import layer_t, annotations_t
from ..library import LazyLibrary, Library, ILibrary, ILibraryView
logger = logging.getLogger(__name__)
clib = ffi.dlopen('/home/jan/projects/klamath-rs/target/release/libklamath_rs_ext.so')
ffi.cdef('void read_path(char* path, struct ArrowArray* array, struct ArrowSchema* schema);')
path_cap_map = {
0: Path.Cap.Flush,
1: Path.Cap.Circle,
2: Path.Cap.Square,
4: Path.Cap.SquareCustom,
}
def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
return numpy.rint(val).astype(numpy.int32)
def _read_to_arrow(
filename: str | pathlib.Path,
*args,
**kwargs,
) -> pyarrow.Array:
path = pathlib.Path(filename)
path.resolve()
ptr_array = ffi.new('struct ArrowArray[]', 1)
ptr_schema = ffi.new('struct ArrowSchema[]', 1)
clib.read_path(str(path).encode(), ptr_array, ptr_schema)
iptr_schema = int(ffi.cast('uintptr_t', ptr_schema))
iptr_array = int(ffi.cast('uintptr_t', ptr_array))
arrow_arr = pyarrow.Array._import_from_c(iptr_array, iptr_schema)
return arrow_arr
def readfile(
filename: str | pathlib.Path,
*args,
**kwargs,
) -> tuple[Library, dict[str, Any]]:
"""
Wrapper for `read()` that takes a filename or path instead of a stream.
Will automatically decompress gzipped files.
Args:
filename: Filename to save to.
*args: passed to `read()`
**kwargs: passed to `read()`
"""
arrow_arr = _read_to_arrow(filename)
assert len(arrow_arr) == 1
results = read_arrow(arrow_arr[0])
return results
def read_arrow(
libarr: pyarrow.Array,
raw_mode: bool = True,
) -> tuple[Library, dict[str, Any]]:
"""
# TODO check GDSII file for cycles!
Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
are translated into Ref objects.
Additional library info is returned in a dict, containing:
'name': name of the library
'meters_per_unit': number of meters per database unit (all values are in database units)
'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns)
per database unit
Args:
stream: Stream to read from.
raw_mode: If True, constructs shapes in raw mode, bypassing most data validation, Default True.
Returns:
- dict of pattern_name:Patterns generated from GDSII structures
- dict of GDSII library info
"""
library_info = _read_header(libarr)
layer_names_np = libarr['layers'].values.to_numpy().view('i2').reshape((-1, 2))
layer_tups = [tuple(pair) for pair in layer_names_np]
cell_ids = libarr['cells'].values.field('id').to_numpy()
cell_names = libarr['cell_names'].as_py()
bnd = libarr['cells'].values.field('boundaries')
boundary = dict(
offsets = bnd.offsets.to_numpy(),
xy_arr = bnd.values.field('xy').values.to_numpy().reshape((-1, 2)),
xy_off = bnd.values.field('xy').offsets.to_numpy() // 2,
layer_tups = layer_tups,
layer_inds = bnd.values.field('layer').to_numpy(),
prop_off = bnd.values.field('properties').offsets.to_numpy(),
prop_key = bnd.values.field('properties').values.field('key').to_numpy(),
prop_val = bnd.values.field('properties').values.field('value').to_pylist(),
)
pth = libarr['cells'].values.field('boundaries')
path = dict(
offsets = pth.offsets.to_numpy(),
xy_arr = pth.values.field('xy').values.to_numpy().reshape((-1, 2)),
xy_off = pth.values.field('xy').offsets.to_numpy() // 2,
layer_tups = layer_tups,
layer_inds = pth.values.field('layer').to_numpy(),
prop_off = pth.values.field('properties').offsets.to_numpy(),
prop_key = pth.values.field('properties').values.field('key').to_numpy(),
prop_val = pth.values.field('properties').values.field('value').to_pylist(),
)
mlib = Library()
for cc, cell in enumerate(libarr['cells']):
name = cell_names[cell_ids[cc]]
pat = read_cell(cc, cell, libarr['cell_names'], raw_mode=raw_mode, boundary=boundary)
mlib[name] = pat
return mlib, library_info
def _read_header(libarr: pyarrow.Array) -> dict[str, Any]:
"""
Read the file header and create the library_info dict.
"""
library_info = dict(
name = libarr['lib_name'],
meters_per_unit = libarr['meters_per_db_unit'],
logical_units_per_unit = libarr['user_units_per_db_unit'],
)
return library_info
def read_cell(
cc: int,
cellarr: pyarrow.Array,
cell_names: pyarrow.Array,
boundary: dict[str, NDArray],
raw_mode: bool = True,
) -> Pattern:
"""
TODO
Read elements from a GDS structure and build a Pattern from them.
Args:
stream: Seekable stream, positioned at a record boundary.
Will be read until an ENDSTR record is consumed.
name: Name of the resulting Pattern
raw_mode: If True, bypass per-shape data validation. Default True.
Returns:
A pattern containing the elements that were read.
"""
pat = Pattern()
for refarr in cellarr['refs']:
target = cell_names[refarr['target'].as_py()].as_py()
args = dict(
offset = (refarr['x'].as_py(), refarr['y'].as_py()),
)
if (mirr := refarr['invert_y']).is_valid:
args['mirrored'] = mirr.as_py()
if (rot := refarr['angle_deg']).is_valid:
args['rotation'] = numpy.deg2rad(rot.as_py())
if (mag := refarr['mag']).is_valid:
args['scale'] = mag.as_py()
if (rep := refarr['repetition']).is_valid:
repetition = Grid(
a_vector = (rep['x0'].as_py(), rep['y0'].as_py()),
b_vector = (rep['x1'].as_py(), rep['y1'].as_py()),
a_count = rep['count0'].as_py(),
b_count = rep['count1'].as_py(),
)
args['repetition'] = repetition
ref = Ref(**args)
pat.refs[target].append(ref)
_boundaries_to_polygons(pat, cellarr)
for gpath in cellarr['paths']:
layer = (gpath['layer'].as_py(),)
args = dict(
vertices = gpath['xy'].values.to_numpy().reshape((-1, 2)),
offset = numpy.zeros(2),
raw = raw_mode,
)
if (gcap := gpath['path_type']).is_valid:
mcap = path_cap_map[gcap.as_py()]
args['cap'] = mcap
if mcap == Path.Cap.SquareCustom:
extensions = [0, 0]
if (ext0 := gpath['extension_start']).is_valid:
extensions[0] = ext0.as_py()
if (ext1 := gpath['extension_end']).is_valid:
extensions[1] = ext1.as_py()
args['extensions'] = extensions
if (width := gpath['width']).is_valid:
args['width'] = width.as_py()
else:
args['width'] = 0
if (props := gpath['properties']).is_valid:
args['annotations'] = _properties_to_annotations(props)
mpath = Path(**args)
pat.shapes[layer].append(mpath)
for gtext in cellarr['texts']:
layer = (gtext['layer'].as_py(),)
args = dict(
offset = (gtext['x'].as_py(), gtext['y'].as_py()),
string = gtext['string'].as_py(),
)
if (props := gtext['properties']).is_valid:
args['annotations'] = _properties_to_annotations(props)
mlabel = Label(**args)
pat.labels[layer].append(mlabel)
return pat
def _paths_to_paths(pat: Pattern, paths: dict[str, Any], cc: int) -> None:
elem_off = elem['offsets'] # which elements belong to each cell
xy_val = elem['xy_arr']
layer_tups = elem['layer_tups']
layer_inds = elem['layer_inds']
prop_key = elem['prop_key']
prop_val = elem['prop_val']
elem_count = elem_off[cc + 1] - elem_off[cc]
elem_slc = slice(elem_off[cc], elem_off[cc] + elem_count + 1) # +1 to capture ending location for last elem
xy_offs = elem['xy_off'][elem_slc] # which xy coords belong to each element
prop_offs = elem['prop_off'][elem_slc] # which props belong to each element
zeros = numpy.zeros((elem_count, 2))
for ee in range(elem_count):
layer = layer_tups[layer_inds[ee]]
vertices = xy_val[xy_offs[ee]:xy_offs[ee + 1]]
prop_ii, prop_ff = prop_offs[ee], prop_offs[ee + 1]
if prop_ii < prop_ff:
ann = {prop_key[off]: prop_val[off] for off in range(prop_ii, prop_ff)}
args = dict(annotations = ann)
path = Polygon(vertices=vertices, offset=zeros[ee], raw=raw_mode)
pat.shapes[layer].append(path)
def _boundaries_to_polygons(pat: Pattern, elem: dict[str, Any], cc: int) -> None:
elem_off = elem['offsets'] # which elements belong to each cell
xy_val = elem['xy_arr']
layer_tups = elem['layer_tups']
layer_inds = elem['layer_inds']
prop_key = elem['prop_key']
prop_val = elem['prop_val']
elem_slc = slice(elem_off[cc], elem_off[cc + 1] + 1)
xy_offs = elem['xy_off'][elem_slc] # which xy coords belong to each element
prop_offs = elem['prop_off'][elem_slc] # which props belong to each element
zeros = numpy.zeros((len(xy_offs) - 1, 2))
for ee in range(len(xy_offs) - 1):
layer = layer_tups[layer_inds[ee]]
vertices = xy_val[xy_offs[ee]:xy_offs[ee + 1] - 1] # -1 to drop closing point
prop_ii, prop_ff = prop_offs[ee], prop_offs[ee + 1]
if prop_ii < prop_ff:
ann = {prop_key[off]: prop_val[off] for off in range(prop_ii, prop_ff)}
args = dict(annotations = ann)
poly = Polygon(vertices=vertices, offset=zeros[ee], raw=raw_mode)
pat.shapes[layer].append(poly)
def _properties_to_annotations(properties: pyarrow.Array) -> annotations_t:
return {prop['key'].as_py(): prop['value'].as_py() for prop in properties}
def check_valid_names(
names: Iterable[str],
max_length: int = 32,
) -> None:
"""
Check all provided names to see if they're valid GDSII cell names.
Args:
names: Collection of names to check
max_length: Max allowed length
"""
allowed_chars = set(string.ascii_letters + string.digits + '_?$')
bad_chars = [
name for name in names
if not set(name).issubset(allowed_chars)
]
bad_lengths = [
name for name in names
if len(name) > max_length
]
if bad_chars:
logger.error('Names contain invalid characters:\n' + pformat(bad_chars))
if bad_lengths:
logger.error(f'Names too long (>{max_length}:\n' + pformat(bad_chars))
if bad_chars or bad_lengths:
raise LibraryError('Library contains invalid names, see log above')

View File

@ -190,7 +190,7 @@ def writefile(
with tmpfile(path) as base_stream: with tmpfile(path) as base_stream:
streams: tuple[Any, ...] = (base_stream,) streams: tuple[Any, ...] = (base_stream,)
if path.suffix == '.gz': if path.suffix == '.gz':
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb')) stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
streams += (stream,) streams += (stream,)
else: else:
stream = base_stream stream = base_stream
@ -551,7 +551,7 @@ def _shapes_to_elements(
circle = fatrec.Circle( circle = fatrec.Circle(
layer=layer, layer=layer,
datatype=datatype, datatype=datatype,
radius=cast(int, radius), radius=cast('int', radius),
x=offset[0], x=offset[0],
y=offset[1], y=offset[1],
properties=properties, properties=properties,
@ -568,8 +568,8 @@ def _shapes_to_elements(
path = fatrec.Path( path = fatrec.Path(
layer=layer, layer=layer,
datatype=datatype, datatype=datatype,
point_list=cast(Sequence[Sequence[int]], deltas), point_list=cast('Sequence[Sequence[int]]', deltas),
half_width=cast(int, half_width), half_width=cast('int', half_width),
x=xy[0], x=xy[0],
y=xy[1], y=xy[1],
extension_start=extension_start, # TODO implement multiple cap types? extension_start=extension_start, # TODO implement multiple cap types?
@ -587,7 +587,7 @@ def _shapes_to_elements(
datatype=datatype, datatype=datatype,
x=xy[0], x=xy[0],
y=xy[1], y=xy[1],
point_list=cast(list[list[int]], points), point_list=cast('list[list[int]]', points),
properties=properties, properties=properties,
repetition=repetition, repetition=repetition,
)) ))
@ -651,10 +651,10 @@ def repetition_masq2fata(
a_count = rint_cast(rep.a_count) a_count = rint_cast(rep.a_count)
b_count = rint_cast(rep.b_count) if rep.b_count is not None else None b_count = rint_cast(rep.b_count) if rep.b_count is not None else None
frep = fatamorgana.GridRepetition( frep = fatamorgana.GridRepetition(
a_vector=cast(list[int], a_vector), a_vector=cast('list[int]', a_vector),
b_vector=cast(list[int] | None, b_vector), b_vector=cast('list[int] | None', b_vector),
a_count=cast(int, a_count), a_count=cast('int', a_count),
b_count=cast(int | None, b_count), b_count=cast('int | None', b_count),
) )
offset = (0, 0) offset = (0, 0)
elif isinstance(rep, Arbitrary): elif isinstance(rep, Arbitrary):

View File

@ -211,7 +211,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
if isinstance(tops, str): if isinstance(tops, str):
tops = (tops,) tops = (tops,)
keep = cast(set[str], self.referenced_patterns(tops) - {None}) keep = cast('set[str]', self.referenced_patterns(tops) - {None})
keep |= set(tops) keep |= set(tops)
filtered = {kk: vv for kk, vv in self.items() if kk in keep} filtered = {kk: vv for kk, vv in self.items() if kk in keep}
@ -314,7 +314,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
flatten_single(top) flatten_single(top)
assert None not in flattened.values() assert None not in flattened.values()
return cast(dict[str, 'Pattern'], flattened) return cast('dict[str, Pattern]', flattened)
def get_name( def get_name(
self, self,
@ -504,7 +504,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
raise LibraryError('visit_* functions returned a new `Pattern` object' raise LibraryError('visit_* functions returned a new `Pattern` object'
' but no top-level name was provided in `hierarchy`') ' but no top-level name was provided in `hierarchy`')
cast(ILibrary, self)[name] = pattern cast('ILibrary', self)[name] = pattern
return self return self
@ -542,7 +542,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
Return: Return:
Topologically sorted list of pattern names. Topologically sorted list of pattern names.
""" """
return cast(list[str], list(TopologicalSorter(self.child_graph()).static_order())) return cast('list[str]', list(TopologicalSorter(self.child_graph()).static_order()))
def find_refs_local( def find_refs_local(
self, self,
@ -827,7 +827,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
for old_name in temp: for old_name in temp:
new_name = rename_map.get(old_name, old_name) new_name = rename_map.get(old_name, old_name)
pat = self[new_name] pat = self[new_name]
pat.refs = map_targets(pat.refs, lambda tt: cast(dict[str | None, str | None], rename_map).get(tt, tt)) pat.refs = map_targets(pat.refs, lambda tt: cast('dict[str | None, str | None]', rename_map).get(tt, tt))
return rename_map return rename_map
@ -944,8 +944,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
shape_table: dict[tuple, list] = defaultdict(list) shape_table: dict[tuple, list] = defaultdict(list)
for layer, sseq in pat.shapes.items(): for layer, sseq in pat.shapes.items():
for i, shape in enumerate(sseq): for ii, shape in enumerate(sseq):
if any(isinstance(shape, t) for t in exclude_types): if any(isinstance(shape, tt) for tt in exclude_types):
continue continue
base_label, values, _func = shape.normalized_form(norm_value) base_label, values, _func = shape.normalized_form(norm_value)
@ -954,16 +954,16 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
if label not in shape_pats: if label not in shape_pats:
continue continue
shape_table[label].append((i, values)) shape_table[label].append((ii, values))
# For repeated shapes, create a `Pattern` holding a normalized shape object, # For repeated shapes, create a `Pattern` holding a normalized shape object,
# and add `pat.refs` entries for each occurrence in pat. Also, note down that # and add `pat.refs` entries for each occurrence in pat. Also, note down that
# we should delete the `pat.shapes` entries for which we made `Ref`s. # we should delete the `pat.shapes` entries for which we made `Ref`s.
shapes_to_remove = [] shapes_to_remove = []
for label in shape_table: for label, shape_entries in shape_table.items():
layer = label[-1] layer = label[-1]
target = label2name(label) target = label2name(label)
for ii, values in shape_table[label]: for ii, values in shape_entries:
offset, scale, rotation, mirror_x = values offset, scale, rotation, mirror_x = values
pat.ref(target=target, offset=offset, scale=scale, pat.ref(target=target, offset=offset, scale=scale,
rotation=rotation, mirrored=(mirror_x, False)) rotation=rotation, mirrored=(mirror_x, False))
@ -1047,7 +1047,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
if isinstance(tops, str): if isinstance(tops, str):
tops = (tops,) tops = (tops,)
keep = cast(set[str], self.referenced_patterns(tops) - {None}) keep = cast('set[str]', self.referenced_patterns(tops) - {None})
keep |= set(tops) keep |= set(tops)
new = type(self)() new = type(self)()

View File

@ -491,7 +491,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
""" """
pat = self.deepcopy().polygonize().flatten(library=library) pat = self.deepcopy().polygonize().flatten(library=library)
polys = [ polys = [
cast(Polygon, shape).vertices + cast(Polygon, shape).offset cast('Polygon', shape).vertices + cast('Polygon', shape).offset
for shape in chain_elements(pat.shapes) for shape in chain_elements(pat.shapes)
] ]
return polys return polys
@ -533,7 +533,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
n_elems = sum(1 for _ in chain_elements(self.shapes, self.labels)) n_elems = sum(1 for _ in chain_elements(self.shapes, self.labels))
ebounds = numpy.full((n_elems, 2, 2), nan) ebounds = numpy.full((n_elems, 2, 2), nan)
for ee, entry in enumerate(chain_elements(self.shapes, self.labels)): for ee, entry in enumerate(chain_elements(self.shapes, self.labels)):
maybe_ebounds = cast(Bounded, entry).get_bounds() maybe_ebounds = cast('Bounded', entry).get_bounds()
if maybe_ebounds is not None: if maybe_ebounds is not None:
ebounds[ee] = maybe_ebounds ebounds[ee] = maybe_ebounds
mask = ~numpy.isnan(ebounds[:, 0, 0]) mask = ~numpy.isnan(ebounds[:, 0, 0])
@ -631,7 +631,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self self
""" """
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()): for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
cast(Positionable, entry).translate(offset) cast('Positionable', entry).translate(offset)
return self return self
def scale_elements(self, c: float) -> Self: def scale_elements(self, c: float) -> Self:
@ -645,33 +645,37 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self self
""" """
for entry in chain_elements(self.shapes, self.refs): for entry in chain_elements(self.shapes, self.refs):
cast(Scalable, entry).scale_by(c) cast('Scalable', entry).scale_by(c)
return self return self
def scale_by(self, c: float) -> Self: def scale_by(self, c: float, scale_refs: bool = True) -> Self:
""" """
Scale this Pattern by the given value Scale this Pattern by the given value
(all shapes and refs and their offsets are scaled, All shapes and (optionally) refs and their offsets are scaled,
as are all label and port offsets) as are all label and port offsets.
Args: Args:
c: factor to scale by c: factor to scale by
scale_refs: Whether to scale refs. Ref offsets are always scaled,
but it may be desirable to not scale the ref itself (e.g. if
the target cell was also scaled).
Returns: Returns:
self self
""" """
for entry in chain_elements(self.shapes, self.refs): for entry in chain_elements(self.shapes, self.refs):
cast(Positionable, entry).offset *= c cast('Positionable', entry).offset *= c
cast(Scalable, entry).scale_by(c) if scale_refs or not isinstance(entry, Ref):
cast('Scalable', entry).scale_by(c)
rep = cast(Repeatable, entry).repetition rep = cast('Repeatable', entry).repetition
if rep: if rep:
rep.scale_by(c) rep.scale_by(c)
for label in chain_elements(self.labels): for label in chain_elements(self.labels):
cast(Positionable, label).offset *= c cast('Positionable', label).offset *= c
rep = cast(Repeatable, label).repetition rep = cast('Repeatable', label).repetition
if rep: if rep:
rep.scale_by(c) rep.scale_by(c)
@ -708,8 +712,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self self
""" """
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()): for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
old_offset = cast(Positionable, entry).offset old_offset = cast('Positionable', entry).offset
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset) cast('Positionable', entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
return self return self
def rotate_elements(self, rotation: float) -> Self: def rotate_elements(self, rotation: float) -> Self:
@ -723,7 +727,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self self
""" """
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()): for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
cast(Rotatable, entry).rotate(rotation) cast('Rotatable', entry).rotate(rotation)
return self return self
def mirror_element_centers(self, across_axis: int = 0) -> Self: def mirror_element_centers(self, across_axis: int = 0) -> Self:
@ -738,7 +742,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self self
""" """
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()): for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
cast(Positionable, entry).offset[across_axis - 1] *= -1 cast('Positionable', entry).offset[across_axis - 1] *= -1
return self return self
def mirror_elements(self, across_axis: int = 0) -> Self: def mirror_elements(self, across_axis: int = 0) -> Self:
@ -754,7 +758,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
self self
""" """
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()): for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
cast(Mirrorable, entry).mirror(across_axis) cast('Mirrorable', entry).mirror(across_axis)
return self return self
def mirror(self, across_axis: int = 0) -> Self: def mirror(self, across_axis: int = 0) -> Self:

View File

@ -294,7 +294,7 @@ class Grid(Repetition):
def __le__(self, other: Repetition) -> bool: def __le__(self, other: Repetition) -> bool:
if type(self) is not type(other): if type(self) is not type(other):
return repr(type(self)) < repr(type(other)) return repr(type(self)) < repr(type(other))
other = cast(Grid, other) other = cast('Grid', other)
if self.a_count != other.a_count: if self.a_count != other.a_count:
return self.a_count < other.a_count return self.a_count < other.a_count
if self.b_count != other.b_count: if self.b_count != other.b_count:
@ -357,7 +357,7 @@ class Arbitrary(Repetition):
def __le__(self, other: Repetition) -> bool: def __le__(self, other: Repetition) -> bool:
if type(self) is not type(other): if type(self) is not type(other):
return repr(type(self)) < repr(type(other)) return repr(type(self)) < repr(type(other))
other = cast(Arbitrary, other) other = cast('Arbitrary', other)
if self.displacements.size != other.displacements.size: if self.displacements.size != other.displacements.size:
return self.displacements.size < other.displacements.size return self.displacements.size < other.displacements.size

View File

@ -206,7 +206,7 @@ class Arc(Shape):
if repr(type(self)) != repr(type(other)): if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other)) return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other)) return id(type(self)) < id(type(other))
other = cast(Arc, other) other = cast('Arc', other)
if self.width != other.width: if self.width != other.width:
return self.width < other.width return self.width < other.width
if not numpy.array_equal(self.radii, other.radii): if not numpy.array_equal(self.radii, other.radii):
@ -233,7 +233,7 @@ class Arc(Shape):
r0, r1 = self.radii r0, r1 = self.radii
# Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation) # Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
a_ranges = cast(_array2x2_t, self._angles_to_parameters()) a_ranges = cast('_array2x2_t', self._angles_to_parameters())
# Approximate perimeter via numerical integration # Approximate perimeter via numerical integration
@ -321,7 +321,7 @@ class Arc(Shape):
If the extrema are innaccessible due to arc constraints, check the arc endpoints instead. If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
""" """
a_ranges = cast(_array2x2_t, self._angles_to_parameters()) a_ranges = cast('_array2x2_t', self._angles_to_parameters())
mins = [] mins = []
maxs = [] maxs = []
@ -432,7 +432,7 @@ class Arc(Shape):
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse. [[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
``` ```
""" """
a_ranges = cast(_array2x2_t, self._angles_to_parameters()) a_ranges = cast('_array2x2_t', self._angles_to_parameters())
mins = [] mins = []
maxs = [] maxs = []

View File

@ -84,7 +84,7 @@ class Circle(Shape):
if repr(type(self)) != repr(type(other)): if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other)) return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other)) return id(type(self)) < id(type(other))
other = cast(Circle, other) other = cast('Circle', other)
if not self.radius == other.radius: if not self.radius == other.radius:
return self.radius < other.radius return self.radius < other.radius
if not numpy.array_equal(self.offset, other.offset): if not numpy.array_equal(self.offset, other.offset):

View File

@ -134,7 +134,7 @@ class Ellipse(Shape):
if repr(type(self)) != repr(type(other)): if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other)) return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other)) return id(type(self)) < id(type(other))
other = cast(Ellipse, other) other = cast('Ellipse', other)
if not numpy.array_equal(self.radii, other.radii): if not numpy.array_equal(self.radii, other.radii):
return tuple(self.radii) < tuple(other.radii) return tuple(self.radii) < tuple(other.radii)
if not numpy.array_equal(self.offset, other.offset): if not numpy.array_equal(self.offset, other.offset):

View File

@ -223,7 +223,7 @@ class Path(Shape):
if repr(type(self)) != repr(type(other)): if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other)) return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other)) return id(type(self)) < id(type(other))
other = cast(Path, other) other = cast('Path', other)
if self.width != other.width: if self.width != other.width:
return self.width < other.width return self.width < other.width
if self.cap != other.cap: if self.cap != other.cap:
@ -405,7 +405,7 @@ class Path(Shape):
x_min = rotated_vertices[:, 0].argmin() x_min = rotated_vertices[:, 0].argmin()
if not is_scalar(x_min): if not is_scalar(x_min):
y_min = rotated_vertices[x_min, 1].argmin() y_min = rotated_vertices[x_min, 1].argmin()
x_min = cast(Sequence, x_min)[y_min] x_min = cast('Sequence', x_min)[y_min]
reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0) reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
width0 = self.width / norm_value width0 = self.width / norm_value

View File

@ -1,5 +1,4 @@
from typing import Any, cast from typing import Any, cast, TYPE_CHECKING
from collections.abc import Sequence
import copy import copy
import functools import functools
@ -13,6 +12,9 @@ from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
if TYPE_CHECKING:
from collections.abc import Sequence
@functools.total_ordering @functools.total_ordering
class Polygon(Shape): class Polygon(Shape):
@ -105,6 +107,7 @@ class Polygon(Shape):
self.offset = offset self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
if rotation:
self.rotate(rotation) self.rotate(rotation)
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon': def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
@ -129,7 +132,7 @@ class Polygon(Shape):
if repr(type(self)) != repr(type(other)): if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other)) return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other)) return id(type(self)) < id(type(other))
other = cast(Polygon, other) other = cast('Polygon', other)
if not numpy.array_equal(self.vertices, other.vertices): if not numpy.array_equal(self.vertices, other.vertices):
min_len = min(self.vertices.shape[0], other.vertices.shape[0]) min_len = min(self.vertices.shape[0], other.vertices.shape[0])
eq_mask = self.vertices[:min_len] != other.vertices[:min_len] eq_mask = self.vertices[:min_len] != other.vertices[:min_len]
@ -395,7 +398,7 @@ class Polygon(Shape):
x_min = rotated_vertices[:, 0].argmin() x_min = rotated_vertices[:, 0].argmin()
if not is_scalar(x_min): if not is_scalar(x_min):
y_min = rotated_vertices[x_min, 1].argmin() y_min = rotated_vertices[x_min, 1].argmin()
x_min = cast(Sequence, x_min)[y_min] x_min = cast('Sequence', x_min)[y_min]
reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0) reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
# TODO: normalize mirroring? # TODO: normalize mirroring?

View File

@ -115,7 +115,7 @@ class Text(RotatableImpl, Shape):
if repr(type(self)) != repr(type(other)): if repr(type(self)) != repr(type(other)):
return repr(type(self)) < repr(type(other)) return repr(type(self)) < repr(type(other))
return id(type(self)) < id(type(other)) return id(type(self)) < id(type(other))
other = cast(Text, other) other = cast('Text', other)
if not self.height == other.height: if not self.height == other.height:
return self.height < other.height return self.height < other.height
if not self.string == other.string: if not self.string == other.string:

View File

@ -1,14 +1,15 @@
from typing import Self, cast, Any from typing import Self, cast, Any, TYPE_CHECKING
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
import numpy import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike from numpy.typing import ArrayLike
from .positionable import Positionable
from ..error import MasqueError from ..error import MasqueError
from ..utils import rotation_matrix_2d from ..utils import rotation_matrix_2d
if TYPE_CHECKING:
from .positionable import Positionable
_empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass _empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass
@ -113,9 +114,9 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self: def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
pivot = numpy.asarray(pivot, dtype=float) pivot = numpy.asarray(pivot, dtype=float)
cast(Positionable, self).translate(-pivot) cast('Positionable', self).translate(-pivot)
cast(Rotatable, self).rotate(rotation) cast('Rotatable', self).rotate(rotation)
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004 self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004
cast(Positionable, self).translate(+pivot) cast('Positionable', self).translate(+pivot)
return self return self

View File

@ -78,7 +78,6 @@ lint.ignore = [
"ANN002", # *args "ANN002", # *args
"ANN003", # **kwargs "ANN003", # **kwargs
"ANN401", # Any "ANN401", # Any
"ANN101", # self: Self
"SIM108", # single-line if / else assignment "SIM108", # single-line if / else assignment
"RET504", # x=y+z; return x "RET504", # x=y+z; return x
"PIE790", # unnecessary pass "PIE790", # unnecessary pass