WIP: make libraries and names first-class!
This commit is contained in:
parent
f834ec6be5
commit
7aaf73cb37
34 changed files with 1780 additions and 1812 deletions
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
DXF file format readers and writers
|
||||
"""
|
||||
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable
|
||||
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping
|
||||
import re
|
||||
import io
|
||||
import base64
|
||||
|
|
@ -10,7 +10,7 @@ import logging
|
|||
import pathlib
|
||||
import gzip
|
||||
|
||||
import numpy # type: ignore
|
||||
import numpy
|
||||
import ezdxf # type: ignore
|
||||
|
||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
||||
|
|
@ -29,12 +29,13 @@ DEFAULT_LAYER = 'DEFAULT'
|
|||
|
||||
|
||||
def write(
|
||||
pattern: Pattern,
|
||||
top_name: str,
|
||||
library: Mapping[str, Pattern],
|
||||
stream: io.TextIOBase,
|
||||
*,
|
||||
modify_originals: bool = False,
|
||||
dxf_version='AC1024',
|
||||
disambiguate_func: Callable[[Iterable[Pattern]], None] = None,
|
||||
disambiguate_func: Callable[[Iterable[str]], List[str]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
||||
|
|
@ -60,10 +61,12 @@ def write(
|
|||
array with rotated instances must be manhattan _after_ having a compensating rotation applied.
|
||||
|
||||
Args:
|
||||
patterns: A Pattern or list of patterns to write to the stream.
|
||||
top_name: Name of the top-level pattern to write.
|
||||
library: A {name: Pattern} mapping of patterns. Only `top_name` and patterns referenced
|
||||
by it are written.
|
||||
stream: Stream object to write to.
|
||||
modify_original: If `True`, the original pattern is modified as part of the writing
|
||||
process. Otherwise, a copy is made and `deepunlock()`-ed.
|
||||
process. Otherwise, a copy is made.
|
||||
Default `False`.
|
||||
disambiguate_func: Function which takes a list of patterns and alters them
|
||||
to make their names valid and unique. Default is `disambiguate_pattern_names`.
|
||||
|
|
@ -75,11 +78,14 @@ def write(
|
|||
assert(disambiguate_func is not None)
|
||||
|
||||
if not modify_originals:
|
||||
pattern = pattern.deepcopy().deepunlock()
|
||||
library = library.deepcopy()
|
||||
|
||||
# Get a dict of id(pattern) -> pattern
|
||||
patterns_by_id = pattern.referenced_patterns_by_id()
|
||||
disambiguate_func(patterns_by_id.values())
|
||||
pattern = library[top_name]
|
||||
|
||||
old_names = list(library.keys())
|
||||
new_names = disambiguate_func(old_names)
|
||||
renamed_lib = {new_name: library[old_name]
|
||||
for old_name, new_name in zip(old_names, new_names)}
|
||||
|
||||
# Create library
|
||||
lib = ezdxf.new(dxf_version, setup=True)
|
||||
|
|
@ -89,9 +95,9 @@ def write(
|
|||
_subpatterns_to_refs(msp, pattern.subpatterns)
|
||||
|
||||
# Now create a block for each referenced pattern, and add in any shapes
|
||||
for pat in patterns_by_id.values():
|
||||
for name, pat in renamed_lib.items():
|
||||
assert(pat is not None)
|
||||
block = lib.blocks.new(name=pat.name)
|
||||
block = lib.blocks.new(name=name)
|
||||
|
||||
_shapes_to_elements(block, pat.shapes)
|
||||
_labels_to_texts(block, pat.labels)
|
||||
|
|
@ -101,7 +107,8 @@ def write(
|
|||
|
||||
|
||||
def writefile(
|
||||
pattern: Pattern,
|
||||
top_name: str,
|
||||
library: Mapping[str, Pattern],
|
||||
filename: Union[str, pathlib.Path],
|
||||
*args,
|
||||
**kwargs,
|
||||
|
|
@ -112,7 +119,9 @@ def writefile(
|
|||
Will automatically compress the file if it has a .gz suffix.
|
||||
|
||||
Args:
|
||||
pattern: `Pattern` to save
|
||||
top_name: Name of the top-level pattern to write.
|
||||
library: A {name: Pattern} mapping of patterns. Only `top_name` and patterns referenced
|
||||
by it are written.
|
||||
filename: Filename to save to.
|
||||
*args: passed to `dxf.write`
|
||||
**kwargs: passed to `dxf.write`
|
||||
|
|
@ -124,7 +133,7 @@ def writefile(
|
|||
open_func = open
|
||||
|
||||
with open_func(path, mode='wt') as stream:
|
||||
write(pattern, stream, *args, **kwargs)
|
||||
write(top_name, library, stream, *args, **kwargs)
|
||||
|
||||
|
||||
def readfile(
|
||||
|
|
@ -156,7 +165,7 @@ def readfile(
|
|||
def read(
|
||||
stream: io.TextIOBase,
|
||||
clean_vertices: bool = True,
|
||||
) -> Tuple[Pattern, Dict[str, Any]]:
|
||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||
"""
|
||||
Read a dxf file and translate it into a dict of `Pattern` objects. DXF `Block`s are
|
||||
translated into `Pattern` objects; `LWPolyline`s are translated into polygons, and `Insert`s
|
||||
|
|
@ -176,26 +185,20 @@ def read(
|
|||
lib = ezdxf.read(stream)
|
||||
msp = lib.modelspace()
|
||||
|
||||
pat = _read_block(msp, clean_vertices)
|
||||
patterns = [pat] + [_read_block(bb, clean_vertices) for bb in lib.blocks if bb.name != '*Model_Space']
|
||||
|
||||
# Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
|
||||
# according to the subpattern.identifier (which is deleted after use).
|
||||
patterns_dict = dict(((p.name, p) for p in patterns))
|
||||
for p in patterns_dict.values():
|
||||
for sp in p.subpatterns:
|
||||
sp.pattern = patterns_dict[sp.identifier[0]]
|
||||
del sp.identifier
|
||||
npat = _read_block(msp, clean_vertices)
|
||||
patterns_dict = dict([npat]
|
||||
+ [_read_block(bb, clean_vertices) for bb in lib.blocks if bb.name != '*Model_Space'])
|
||||
|
||||
library_info = {
|
||||
'layers': [ll.dxfattribs() for ll in lib.layers]
|
||||
}
|
||||
|
||||
return pat, library_info
|
||||
return patterns_dict, library_info
|
||||
|
||||
|
||||
def _read_block(block, clean_vertices: bool) -> Pattern:
|
||||
pat = Pattern(block.name)
|
||||
def _read_block(block, clean_vertices: bool) -> Tuple[str, Pattern]:
|
||||
name = block.name
|
||||
pat = Pattern()
|
||||
for element in block:
|
||||
eltype = element.dxftype()
|
||||
if eltype in ('POLYLINE', 'LWPOLYLINE'):
|
||||
|
|
@ -258,12 +261,12 @@ def _read_block(block, clean_vertices: bool) -> Pattern:
|
|||
offset = numpy.array(attr.get('insert', (0, 0, 0)))[:2]
|
||||
|
||||
args = {
|
||||
'target': (attr.get('name', None),),
|
||||
'offset': offset,
|
||||
'scale': scale,
|
||||
'mirrored': mirrored,
|
||||
'rotation': rotation,
|
||||
'pattern': None,
|
||||
'identifier': (attr.get('name', None),),
|
||||
}
|
||||
|
||||
if 'column_count' in attr:
|
||||
|
|
@ -274,7 +277,7 @@ def _read_block(block, clean_vertices: bool) -> Pattern:
|
|||
pat.subpatterns.append(SubPattern(**args))
|
||||
else:
|
||||
logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')
|
||||
return pat
|
||||
return name, pat
|
||||
|
||||
|
||||
def _subpatterns_to_refs(
|
||||
|
|
@ -282,9 +285,9 @@ def _subpatterns_to_refs(
|
|||
subpatterns: List[SubPattern],
|
||||
) -> None:
|
||||
for subpat in subpatterns:
|
||||
if subpat.pattern is None:
|
||||
if subpat.target is None:
|
||||
continue
|
||||
encoded_name = subpat.pattern.name
|
||||
encoded_name = subpat.target
|
||||
|
||||
rotation = (subpat.rotation * 180 / numpy.pi) % 360
|
||||
attribs = {
|
||||
|
|
@ -360,18 +363,24 @@ def _mlayer2dxf(layer: layer_t) -> str:
|
|||
|
||||
|
||||
def disambiguate_pattern_names(
|
||||
patterns: Iterable[Pattern],
|
||||
names: Iterable[str],
|
||||
max_name_length: int = 32,
|
||||
suffix_length: int = 6,
|
||||
dup_warn_filter: Callable[[str], bool] = None, # If returns False, don't warn about this name
|
||||
) -> None:
|
||||
used_names = []
|
||||
for pat in patterns:
|
||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', pat.name)
|
||||
) -> List[str]:
|
||||
"""
|
||||
Args:
|
||||
names: List of pattern names to disambiguate
|
||||
max_name_length: Names longer than this will be truncated
|
||||
suffix_length: Names which get truncated are truncated by this many extra characters. This is to
|
||||
leave room for a suffix if one is necessary.
|
||||
"""
|
||||
new_names = []
|
||||
for name in names:
|
||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', name)
|
||||
|
||||
i = 0
|
||||
suffixed_name = sanitized_name
|
||||
while suffixed_name in used_names or suffixed_name == '':
|
||||
while suffixed_name in new_names or suffixed_name == '':
|
||||
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
||||
|
||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||
|
|
@ -380,17 +389,16 @@ def disambiguate_pattern_names(
|
|||
if sanitized_name == '':
|
||||
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
||||
elif suffixed_name != sanitized_name:
|
||||
if dup_warn_filter is None or dup_warn_filter(pat.name):
|
||||
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
|
||||
if dup_warn_filter is None or dup_warn_filter(name):
|
||||
logger.warning(f'Pattern name "{name}" ({sanitized_name}) appears multiple times;\n'
|
||||
+ f' renaming to "{suffixed_name}"')
|
||||
|
||||
if len(suffixed_name) == 0:
|
||||
# Should never happen since zero-length names are replaced
|
||||
raise PatternError(f'Zero-length name after sanitize,\n originally "{pat.name}"')
|
||||
raise PatternError(f'Zero-length name after sanitize,\n originally "{name}"')
|
||||
if len(suffixed_name) > max_name_length:
|
||||
raise PatternError(f'Pattern name "{suffixed_name!r}" length > {max_name_length} after encode,\n'
|
||||
+ f' originally "{pat.name}"')
|
||||
|
||||
pat.name = suffixed_name
|
||||
used_names.append(suffixed_name)
|
||||
+ f' originally "{name}"')
|
||||
|
||||
new_names.append(suffixed_name)
|
||||
return new_names
|
||||
|
|
|
|||
|
|
@ -53,18 +53,22 @@ path_cap_map = {
|
|||
}
|
||||
|
||||
|
||||
def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
||||
return numpy.rint(val, dtype=numpy.int32, casting='unsafe')
|
||||
|
||||
|
||||
def write(
|
||||
patterns: Union[Pattern, Sequence[Pattern]],
|
||||
library: Mapping[str, Pattern],
|
||||
stream: BinaryIO,
|
||||
meters_per_unit: float,
|
||||
logical_units_per_unit: float = 1,
|
||||
library_name: str = 'masque-klamath',
|
||||
*,
|
||||
modify_originals: bool = False,
|
||||
disambiguate_func: Callable[[Iterable[Pattern]], None] = None,
|
||||
disambiguate_func: Callable[[Iterable[str]], List[str]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Convert a `Pattern` or list of patterns to a GDSII stream, and then mapping data as follows:
|
||||
Convert a library to a GDSII stream, mapping data as follows:
|
||||
Pattern -> GDSII structure
|
||||
SubPattern -> GDSII SREF or AREF
|
||||
Path -> GSDII path
|
||||
|
|
@ -85,7 +89,7 @@ def write(
|
|||
prior to calling this function.
|
||||
|
||||
Args:
|
||||
patterns: A Pattern or list of patterns to convert.
|
||||
library: A {name: Pattern} mapping of patterns to write.
|
||||
meters_per_unit: Written into the GDSII file, meters per (database) length unit.
|
||||
All distances are assumed to be an integer multiple of this unit, and are stored as such.
|
||||
logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
|
||||
|
|
@ -94,52 +98,48 @@ def write(
|
|||
library_name: Library name written into the GDSII file.
|
||||
Default 'masque-klamath'.
|
||||
modify_originals: If `True`, the original pattern is modified as part of the writing
|
||||
process. Otherwise, a copy is made and `deepunlock()`-ed.
|
||||
process. Otherwise, a copy is made.
|
||||
Default `False`.
|
||||
disambiguate_func: Function which takes a list of patterns and alters them
|
||||
to make their names valid and unique. Default is `disambiguate_pattern_names`, which
|
||||
attempts to adhere to the GDSII standard as well as possible.
|
||||
disambiguate_func: Function which takes a list of pattern names and returns a list of names
|
||||
altered to be valid and unique. Default is `disambiguate_pattern_names`, which
|
||||
attempts to adhere to the GDSII standard reasonably well.
|
||||
WARNING: No additional error checking is performed on the results.
|
||||
"""
|
||||
if isinstance(patterns, Pattern):
|
||||
patterns = [patterns]
|
||||
|
||||
if disambiguate_func is None:
|
||||
disambiguate_func = disambiguate_pattern_names # type: ignore
|
||||
assert(disambiguate_func is not None) # placate mypy
|
||||
disambiguate_func = disambiguate_pattern_names
|
||||
|
||||
if not modify_originals:
|
||||
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)]
|
||||
library = copy.deepcopy(library)
|
||||
|
||||
patterns = [p.wrap_repeated_shapes() for p in patterns]
|
||||
for p in library.values():
|
||||
library.add(p.wrap_repeated_shapes())
|
||||
|
||||
old_names = list(library.keys())
|
||||
new_names = disambiguate_func(old_names)
|
||||
renamed_lib = {new_name: library[old_name]
|
||||
for old_name, new_name in zip(old_names, new_names)}
|
||||
|
||||
# Create library
|
||||
header = klamath.library.FileHeader(name=library_name.encode('ASCII'),
|
||||
user_units_per_db_unit=logical_units_per_unit,
|
||||
meters_per_db_unit=meters_per_unit)
|
||||
header = klamath.library.FileHeader(
|
||||
name=library_name.encode('ASCII'),
|
||||
user_units_per_db_unit=logical_units_per_unit,
|
||||
meters_per_db_unit=meters_per_unit,
|
||||
)
|
||||
header.write(stream)
|
||||
|
||||
# Get a dict of id(pattern) -> pattern
|
||||
patterns_by_id = {id(pattern): pattern for pattern in patterns}
|
||||
for pattern in patterns:
|
||||
for i, p in pattern.referenced_patterns_by_id().items():
|
||||
patterns_by_id[i] = p
|
||||
|
||||
disambiguate_func(patterns_by_id.values())
|
||||
|
||||
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
||||
for pat in patterns_by_id.values():
|
||||
for name, pat in renamed_lib.items():
|
||||
elements: List[klamath.elements.Element] = []
|
||||
elements += _shapes_to_elements(pat.shapes)
|
||||
elements += _labels_to_texts(pat.labels)
|
||||
elements += _subpatterns_to_refs(pat.subpatterns)
|
||||
|
||||
klamath.library.write_struct(stream, name=pat.name.encode('ASCII'), elements=elements)
|
||||
klamath.library.write_struct(stream, name=name.encode('ASCII'), elements=elements)
|
||||
records.ENDLIB.write(stream, None)
|
||||
|
||||
|
||||
def writefile(
|
||||
patterns: Union[Sequence[Pattern], Pattern],
|
||||
library: Mapping[str, Pattern],
|
||||
filename: Union[str, pathlib.Path],
|
||||
*args,
|
||||
**kwargs,
|
||||
|
|
@ -150,7 +150,7 @@ def writefile(
|
|||
Will automatically compress the file if it has a .gz suffix.
|
||||
|
||||
Args:
|
||||
patterns: `Pattern` or list of patterns to save
|
||||
library: {name: Pattern} pairs to save.
|
||||
filename: Filename to save to.
|
||||
*args: passed to `write()`
|
||||
**kwargs: passed to `write()`
|
||||
|
|
@ -216,22 +216,14 @@ def read(
|
|||
"""
|
||||
library_info = _read_header(stream)
|
||||
|
||||
patterns = []
|
||||
patterns_dict = {}
|
||||
found_struct = records.BGNSTR.skip_past(stream)
|
||||
while found_struct:
|
||||
name = records.STRNAME.skip_and_read(stream)
|
||||
pat = read_elements(stream, name=name.decode('ASCII'), raw_mode=raw_mode)
|
||||
patterns.append(pat)
|
||||
pat = read_elements(stream, raw_mode=raw_mode)
|
||||
patterns_dict[name.decode('ASCII')] = pat
|
||||
found_struct = records.BGNSTR.skip_past(stream)
|
||||
|
||||
# Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
|
||||
# according to the subpattern.identifier (which is deleted after use).
|
||||
patterns_dict = dict(((p.name, p) for p in patterns))
|
||||
for p in patterns_dict.values():
|
||||
for sp in p.subpatterns:
|
||||
sp.pattern = patterns_dict[sp.identifier[0]]
|
||||
del sp.identifier
|
||||
|
||||
return patterns_dict, library_info
|
||||
|
||||
|
||||
|
|
@ -250,7 +242,6 @@ def _read_header(stream: BinaryIO) -> Dict[str, Any]:
|
|||
|
||||
def read_elements(
|
||||
stream: BinaryIO,
|
||||
name: str,
|
||||
raw_mode: bool = True,
|
||||
) -> Pattern:
|
||||
"""
|
||||
|
|
@ -265,7 +256,7 @@ def read_elements(
|
|||
Returns:
|
||||
A pattern containing the elements that were read.
|
||||
"""
|
||||
pat = Pattern(name)
|
||||
pat = Pattern()
|
||||
|
||||
elements = klamath.library.read_elements(stream)
|
||||
for element in elements:
|
||||
|
|
@ -276,10 +267,12 @@ def read_elements(
|
|||
path = _gpath_to_mpath(element, raw_mode)
|
||||
pat.shapes.append(path)
|
||||
elif isinstance(element, klamath.elements.Text):
|
||||
label = Label(offset=element.xy.astype(float),
|
||||
layer=element.layer,
|
||||
string=element.string.decode('ASCII'),
|
||||
annotations=_properties_to_annotations(element.properties))
|
||||
label = Label(
|
||||
offset=element.xy.astype(float),
|
||||
layer=element.layer,
|
||||
string=element.string.decode('ASCII'),
|
||||
annotations=_properties_to_annotations(element.properties),
|
||||
)
|
||||
pat.labels.append(label)
|
||||
elif isinstance(element, klamath.elements.Reference):
|
||||
pat.subpatterns.append(_ref_to_subpat(element))
|
||||
|
|
@ -304,8 +297,7 @@ def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]:
|
|||
|
||||
def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
||||
"""
|
||||
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None
|
||||
and sets the instance .identifier to (struct_name,).
|
||||
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.target to struct_name.
|
||||
"""
|
||||
xy = ref.xy.astype(float)
|
||||
offset = xy[0]
|
||||
|
|
@ -317,14 +309,15 @@ def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
|||
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
|
||||
a_count=a_count, b_count=b_count)
|
||||
|
||||
subpat = SubPattern(pattern=None,
|
||||
offset=offset,
|
||||
rotation=numpy.deg2rad(ref.angle_deg),
|
||||
scale=ref.mag,
|
||||
mirrored=(ref.invert_y, False),
|
||||
annotations=_properties_to_annotations(ref.properties),
|
||||
repetition=repetition)
|
||||
subpat.identifier = (ref.struct_name.decode('ASCII'),)
|
||||
subpat = SubPattern(
|
||||
pattern=ref.struct_name.decode('ASCII'),
|
||||
offset=offset,
|
||||
rotation=numpy.deg2rad(ref.angle_deg),
|
||||
scale=ref.mag,
|
||||
mirrored=(ref.invert_y, False),
|
||||
annotations=_properties_to_annotations(ref.properties),
|
||||
repetition=repetition,
|
||||
)
|
||||
return subpat
|
||||
|
||||
|
||||
|
|
@ -334,34 +327,36 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
|||
else:
|
||||
raise PatternError(f'Unrecognized path type: {gpath.path_type}')
|
||||
|
||||
mpath = Path(vertices=gpath.xy.astype(float),
|
||||
layer=gpath.layer,
|
||||
width=gpath.width,
|
||||
cap=cap,
|
||||
offset=numpy.zeros(2),
|
||||
annotations=_properties_to_annotations(gpath.properties),
|
||||
raw=raw_mode,
|
||||
)
|
||||
mpath = Path(
|
||||
vertices=gpath.xy.astype(float),
|
||||
layer=gpath.layer,
|
||||
width=gpath.width,
|
||||
cap=cap,
|
||||
offset=numpy.zeros(2),
|
||||
annotations=_properties_to_annotations(gpath.properties),
|
||||
raw=raw_mode,
|
||||
)
|
||||
if cap == Path.Cap.SquareCustom:
|
||||
mpath.cap_extensions = gpath.extension
|
||||
return mpath
|
||||
|
||||
|
||||
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon:
|
||||
return Polygon(vertices=boundary.xy[:-1].astype(float),
|
||||
layer=boundary.layer,
|
||||
offset=numpy.zeros(2),
|
||||
annotations=_properties_to_annotations(boundary.properties),
|
||||
raw=raw_mode,
|
||||
)
|
||||
return Polygon(
|
||||
vertices=boundary.xy[:-1].astype(float),
|
||||
layer=boundary.layer,
|
||||
offset=numpy.zeros(2),
|
||||
annotations=_properties_to_annotations(boundary.properties),
|
||||
raw=raw_mode,
|
||||
)
|
||||
|
||||
|
||||
def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.Reference]:
|
||||
refs = []
|
||||
for subpat in subpatterns:
|
||||
if subpat.pattern is None:
|
||||
if subpat.target is None:
|
||||
continue
|
||||
encoded_name = subpat.pattern.name.encode('ASCII')
|
||||
encoded_name = subpat.target.encode('ASCII')
|
||||
|
||||
# Note: GDS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
||||
|
|
@ -377,32 +372,39 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.
|
|||
rep.a_vector * rep.a_count,
|
||||
b_vector * b_count,
|
||||
]
|
||||
aref = klamath.library.Reference(struct_name=encoded_name,
|
||||
xy=numpy.round(xy).astype(int),
|
||||
colrow=(numpy.round(rep.a_count), numpy.round(rep.b_count)),
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=subpat.scale,
|
||||
properties=properties)
|
||||
aref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast(xy),
|
||||
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=subpat.scale,
|
||||
properties=properties,
|
||||
)
|
||||
refs.append(aref)
|
||||
elif rep is None:
|
||||
ref = klamath.library.Reference(struct_name=encoded_name,
|
||||
xy=numpy.round([subpat.offset]).astype(int),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=subpat.scale,
|
||||
properties=properties)
|
||||
ref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([subpat.offset]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=subpat.scale,
|
||||
properties=properties,
|
||||
)
|
||||
refs.append(ref)
|
||||
else:
|
||||
new_srefs = [klamath.library.Reference(struct_name=encoded_name,
|
||||
xy=numpy.round([subpat.offset + dd]).astype(int),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=subpat.scale,
|
||||
properties=properties)
|
||||
for dd in rep.displacements]
|
||||
new_srefs = [
|
||||
klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([subpat.offset + dd]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=subpat.scale,
|
||||
properties=properties,
|
||||
)
|
||||
for dd in rep.displacements]
|
||||
refs += new_srefs
|
||||
return refs
|
||||
|
||||
|
|
@ -443,8 +445,8 @@ def _shapes_to_elements(
|
|||
layer, data_type = _mlayer2gds(shape.layer)
|
||||
properties = _annotations_to_properties(shape.annotations, 128)
|
||||
if isinstance(shape, Path) and not polygonize_paths:
|
||||
xy = numpy.round(shape.vertices + shape.offset).astype(int)
|
||||
width = numpy.round(shape.width).astype(int)
|
||||
xy = rint_cast(shape.vertices + shape.offset)
|
||||
width = rint_cast(shape.width)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
|
||||
extension: Tuple[int, int]
|
||||
|
|
@ -453,30 +455,36 @@ def _shapes_to_elements(
|
|||
else:
|
||||
extension = (0, 0)
|
||||
|
||||
path = klamath.elements.Path(layer=(layer, data_type),
|
||||
xy=xy,
|
||||
path_type=path_type,
|
||||
width=width,
|
||||
extension=extension,
|
||||
properties=properties)
|
||||
path = klamath.elements.Path(
|
||||
layer=(layer, data_type),
|
||||
xy=xy,
|
||||
path_type=path_type,
|
||||
width=width,
|
||||
extension=extension,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(path)
|
||||
elif isinstance(shape, Polygon):
|
||||
polygon = shape
|
||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||
xy_closed[-1] = xy_closed[0]
|
||||
boundary = klamath.elements.Boundary(layer=(layer, data_type),
|
||||
xy=xy_closed,
|
||||
properties=properties)
|
||||
boundary = klamath.elements.Boundary(
|
||||
layer=(layer, data_type),
|
||||
xy=xy_closed,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(boundary)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||
xy_closed[-1] = xy_closed[0]
|
||||
boundary = klamath.elements.Boundary(layer=(layer, data_type),
|
||||
xy=xy_closed,
|
||||
properties=properties)
|
||||
boundary = klamath.elements.Boundary(
|
||||
layer=(layer, data_type),
|
||||
xy=xy_closed,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(boundary)
|
||||
return elements
|
||||
|
||||
|
|
@ -486,46 +494,44 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]:
|
|||
for label in labels:
|
||||
properties = _annotations_to_properties(label.annotations, 128)
|
||||
layer, text_type = _mlayer2gds(label.layer)
|
||||
xy = numpy.round([label.offset]).astype(int)
|
||||
text = klamath.elements.Text(layer=(layer, text_type),
|
||||
xy=xy,
|
||||
string=label.string.encode('ASCII'),
|
||||
properties=properties,
|
||||
presentation=0, # TODO maybe set some of these?
|
||||
angle_deg=0,
|
||||
invert_y=False,
|
||||
width=0,
|
||||
path_type=0,
|
||||
mag=1)
|
||||
xy = rint_cast([label.offset])
|
||||
text = klamath.elements.Text(
|
||||
layer=(layer, text_type),
|
||||
xy=xy,
|
||||
string=label.string.encode('ASCII'),
|
||||
properties=properties,
|
||||
presentation=0, # TODO maybe set some of these?
|
||||
angle_deg=0,
|
||||
invert_y=False,
|
||||
width=0,
|
||||
path_type=0,
|
||||
mag=1,
|
||||
)
|
||||
texts.append(text)
|
||||
return texts
|
||||
|
||||
|
||||
def disambiguate_pattern_names(
|
||||
patterns: Sequence[Pattern],
|
||||
names: Iterable[str],
|
||||
max_name_length: int = 32,
|
||||
suffix_length: int = 6,
|
||||
dup_warn_filter: Optional[Callable[[str], bool]] = None,
|
||||
) -> None:
|
||||
) -> List[str]:
|
||||
"""
|
||||
Args:
|
||||
patterns: List of patterns to disambiguate
|
||||
names: List of pattern names to disambiguate
|
||||
max_name_length: Names longer than this will be truncated
|
||||
suffix_length: Names which get truncated are truncated by this many extra characters. This is to
|
||||
leave room for a suffix if one is necessary.
|
||||
dup_warn_filter: (optional) Function for suppressing warnings about cell names changing. Receives
|
||||
the cell name and returns `False` if the warning should be suppressed and `True` if it should
|
||||
be displayed. Default displays all warnings.
|
||||
"""
|
||||
used_names = []
|
||||
for pat in set(patterns):
|
||||
new_names = []
|
||||
for name in names:
|
||||
# Shorten names which already exceed max-length
|
||||
if len(pat.name) > max_name_length:
|
||||
shortened_name = pat.name[:max_name_length - suffix_length]
|
||||
logger.warning(f'Pattern name "{pat.name}" is too long ({len(pat.name)}/{max_name_length} chars),\n'
|
||||
if len(name) > max_name_length:
|
||||
shortened_name = name[:max_name_length - suffix_length]
|
||||
logger.warning(f'Pattern name "{name}" is too long ({len(name)}/{max_name_length} chars),\n'
|
||||
+ f' shortening to "{shortened_name}" before generating suffix')
|
||||
else:
|
||||
shortened_name = pat.name
|
||||
shortened_name = name
|
||||
|
||||
# Remove invalid characters
|
||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
|
||||
|
|
@ -533,7 +539,7 @@ def disambiguate_pattern_names(
|
|||
# Add a suffix that makes the name unique
|
||||
i = 0
|
||||
suffixed_name = sanitized_name
|
||||
while suffixed_name in used_names or suffixed_name == '':
|
||||
while suffixed_name in new_names or suffixed_name == '':
|
||||
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
||||
|
||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||
|
|
@ -542,27 +548,25 @@ def disambiguate_pattern_names(
|
|||
if sanitized_name == '':
|
||||
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
||||
elif suffixed_name != sanitized_name:
|
||||
if dup_warn_filter is None or dup_warn_filter(pat.name):
|
||||
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
|
||||
if dup_warn_filter is None or dup_warn_filter(name):
|
||||
logger.warning(f'Pattern name "{name}" ({sanitized_name}) appears multiple times;\n'
|
||||
+ f' renaming to "{suffixed_name}"')
|
||||
|
||||
# Encode into a byte-string and perform some final checks
|
||||
encoded_name = suffixed_name.encode('ASCII')
|
||||
if len(encoded_name) == 0:
|
||||
# Should never happen since zero-length names are replaced
|
||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"')
|
||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{name}"')
|
||||
if len(encoded_name) > max_name_length:
|
||||
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n'
|
||||
+ f' originally "{pat.name}"')
|
||||
+ f' originally "{name}"')
|
||||
|
||||
pat.name = suffixed_name
|
||||
used_names.append(suffixed_name)
|
||||
new_names.append(suffixed_name)
|
||||
return new_names
|
||||
|
||||
|
||||
def load_library(
|
||||
stream: BinaryIO,
|
||||
tag: str,
|
||||
is_secondary: Optional[Callable[[str], bool]] = None,
|
||||
*,
|
||||
full_load: bool = False,
|
||||
) -> Tuple[Library, Dict[str, Any]]:
|
||||
|
|
@ -574,28 +578,17 @@ def load_library(
|
|||
|
||||
Args:
|
||||
stream: Seekable stream. Position 0 should be the start of the file.
|
||||
The caller should leave the stream open while the library
|
||||
is still in use, since the library will need to access it
|
||||
in order to read the structure contents.
|
||||
tag: Unique identifier that will be used to identify this data source
|
||||
is_secondary: Function which takes a structure name and returns
|
||||
True if the structure should only be used as a subcell
|
||||
and not appear in the main Library interface.
|
||||
Default always returns False.
|
||||
The caller should leave the stream open while the library
|
||||
is still in use, since the library will need to access it
|
||||
in order to read the structure contents.
|
||||
full_load: If True, force all structures to be read immediately rather
|
||||
than as-needed. Since data is read sequentially from the file,
|
||||
this will be faster than using the resulting library's
|
||||
`precache` method.
|
||||
than as-needed. Since data is read sequentially from the file, this
|
||||
will be faster than using the resulting library's `precache` method.
|
||||
|
||||
Returns:
|
||||
Library object, allowing for deferred load of structures.
|
||||
Additional library info (dict, same format as from `read`).
|
||||
"""
|
||||
if is_secondary is None:
|
||||
def is_secondary(k: str) -> bool:
|
||||
return False
|
||||
assert(is_secondary is not None)
|
||||
|
||||
stream.seek(0)
|
||||
lib = Library()
|
||||
|
||||
|
|
@ -603,7 +596,7 @@ def load_library(
|
|||
# Full load approach (immediately load everything)
|
||||
patterns, library_info = read(stream)
|
||||
for name, pattern in patterns.items():
|
||||
lib.set_const(name, tag, pattern, secondary=is_secondary(name))
|
||||
lib[name] = lambda: pattern
|
||||
return lib, library_info
|
||||
|
||||
# Normal approach (scan and defer load)
|
||||
|
|
@ -613,19 +606,17 @@ def load_library(
|
|||
for name_bytes, pos in structs.items():
|
||||
name = name_bytes.decode('ASCII')
|
||||
|
||||
def mkstruct(pos: int = pos, name: str = name) -> Pattern:
|
||||
def mkstruct(pos: int = pos) -> Pattern:
|
||||
stream.seek(pos)
|
||||
return read_elements(stream, name, raw_mode=True)
|
||||
return read_elements(stream, raw_mode=True)
|
||||
|
||||
lib.set_value(name, tag, mkstruct, secondary=is_secondary(name))
|
||||
lib[name] = mkstruct
|
||||
|
||||
return lib, library_info
|
||||
|
||||
|
||||
def load_libraryfile(
|
||||
filename: Union[str, pathlib.Path],
|
||||
tag: str,
|
||||
is_secondary: Optional[Callable[[str], bool]] = None,
|
||||
*,
|
||||
use_mmap: bool = True,
|
||||
full_load: bool = False,
|
||||
|
|
@ -640,8 +631,6 @@ def load_libraryfile(
|
|||
|
||||
Args:
|
||||
path: filename or path to read from
|
||||
tag: Unique identifier for library, see `load_library`
|
||||
is_secondary: Function specifying subcess, see `load_library`
|
||||
use_mmap: If `True`, will attempt to memory-map the file instead
|
||||
of buffering. In the case of gzipped files, the file
|
||||
is decompressed into a python `bytes` object in memory
|
||||
|
|
@ -667,4 +656,4 @@ def load_libraryfile(
|
|||
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ)
|
||||
else:
|
||||
stream = io.BufferedReader(base_stream)
|
||||
return load_library(stream, tag, is_secondary)
|
||||
return load_library(stream, full_load=full_load)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Note that OASIS references follow the same convention as `masque`,
|
|||
Scaling, rotation, and mirroring apply to individual instances, not grid
|
||||
vectors or offsets.
|
||||
"""
|
||||
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional
|
||||
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Mapping, Optional
|
||||
import re
|
||||
import io
|
||||
import copy
|
||||
|
|
@ -22,11 +22,12 @@ import pathlib
|
|||
import gzip
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
import fatamorgana
|
||||
import fatamorgana.records as fatrec
|
||||
from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
|
||||
|
||||
from .utils import clean_pattern_vertices, is_gzipped
|
||||
from .utils import is_gzipped
|
||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
||||
from ..shapes import Polygon, Path, Circle
|
||||
from ..repetition import Grid, Arbitrary, Repetition
|
||||
|
|
@ -47,19 +48,22 @@ path_cap_map = {
|
|||
|
||||
#TODO implement more shape types?
|
||||
|
||||
def rint_cast(val: ArrayLike) -> NDArray[numpy.int64]:
|
||||
return numpy.rint(val, dtype=numpy.int64, casting='unsafe')
|
||||
|
||||
|
||||
def build(
|
||||
patterns: Union[Pattern, Sequence[Pattern]],
|
||||
library: Mapping[str, Pattern], # NOTE: Pattern here should be treated as immutable!
|
||||
units_per_micron: int,
|
||||
layer_map: Optional[Dict[str, Union[int, Tuple[int, int]]]] = None,
|
||||
*,
|
||||
modify_originals: bool = False,
|
||||
disambiguate_func: Optional[Callable[[Iterable[Pattern]], None]] = None,
|
||||
disambiguate_func: Optional[Callable[[Iterable[str]], List[str]]] = None,
|
||||
annotations: Optional[annotations_t] = None,
|
||||
) -> fatamorgana.OasisLayout:
|
||||
"""
|
||||
Convert a `Pattern` or list of patterns to an OASIS stream, writing patterns
|
||||
as OASIS cells, subpatterns as Placement records, and other shapes and labels
|
||||
mapped to equivalent record types (Polygon, Path, Circle, Text).
|
||||
Convert a collection of {name: Pattern} pairs to an OASIS stream, writing patterns
|
||||
as OASIS cells, subpatterns as Placement records, and mapping other shapes and labels
|
||||
to equivalent record types (Polygon, Path, Circle, Text).
|
||||
Other shape types may be converted to polygons if no equivalent
|
||||
record type exists (or is not implemented here yet).
|
||||
|
||||
|
|
@ -75,7 +79,7 @@ def build(
|
|||
prior to calling this function.
|
||||
|
||||
Args:
|
||||
patterns: A Pattern or list of patterns to convert.
|
||||
library: A {name: Pattern} mapping of patterns to write.
|
||||
units_per_micron: Written into the OASIS file, number of grid steps per micrometer.
|
||||
All distances are assumed to be an integer multiple of the grid step, and are stored as such.
|
||||
layer_map: Dictionary which translates layer names into layer numbers. If this argument is
|
||||
|
|
@ -86,11 +90,8 @@ def build(
|
|||
into numbers, omit this argument, and manually generate the required
|
||||
`fatamorgana.records.LayerName` entries.
|
||||
Default is an empty dict (no names provided).
|
||||
modify_originals: If `True`, the original pattern is modified as part of the writing
|
||||
process. Otherwise, a copy is made and `deepunlock()`-ed.
|
||||
Default `False`.
|
||||
disambiguate_func: Function which takes a list of patterns and alters them
|
||||
to make their names valid and unique. Default is `disambiguate_pattern_names`.
|
||||
disambiguate_func: Function which takes a list of pattern names and returns a list of names
|
||||
altered to be valid and unique. Default is `disambiguate_pattern_names`.
|
||||
annotations: dictionary of key-value pairs which are saved as library-level properties
|
||||
|
||||
Returns:
|
||||
|
|
@ -108,9 +109,6 @@ def build(
|
|||
if annotations is None:
|
||||
annotations = {}
|
||||
|
||||
if not modify_originals:
|
||||
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)]
|
||||
|
||||
# Create library
|
||||
lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None)
|
||||
lib.properties = annotations_to_properties(annotations)
|
||||
|
|
@ -119,10 +117,12 @@ def build(
|
|||
for name, layer_num in layer_map.items():
|
||||
layer, data_type = _mlayer2oas(layer_num)
|
||||
lib.layers += [
|
||||
fatrec.LayerName(nstring=name,
|
||||
layer_interval=(layer, layer),
|
||||
type_interval=(data_type, data_type),
|
||||
is_textlayer=tt)
|
||||
fatrec.LayerName(
|
||||
nstring=name,
|
||||
layer_interval=(layer, layer),
|
||||
type_interval=(data_type, data_type),
|
||||
is_textlayer=tt,
|
||||
)
|
||||
for tt in (True, False)]
|
||||
|
||||
def layer2oas(mlayer: layer_t) -> Tuple[int, int]:
|
||||
|
|
@ -132,17 +132,14 @@ def build(
|
|||
else:
|
||||
layer2oas = _mlayer2oas
|
||||
|
||||
# Get a dict of id(pattern) -> pattern
|
||||
patterns_by_id = {id(pattern): pattern for pattern in patterns}
|
||||
for pattern in patterns:
|
||||
for i, p in pattern.referenced_patterns_by_id().items():
|
||||
patterns_by_id[i] = p
|
||||
|
||||
disambiguate_func(patterns_by_id.values())
|
||||
old_names = list(library.keys())
|
||||
new_names = disambiguate_func(old_names)
|
||||
renamed_lib = {new_name: library[old_name]
|
||||
for old_name, new_name in zip(old_names, new_names)}
|
||||
|
||||
# Now create a structure for each pattern
|
||||
for pat in patterns_by_id.values():
|
||||
structure = fatamorgana.Cell(name=pat.name)
|
||||
for name, pat in renamed_lib.items():
|
||||
structure = fatamorgana.Cell(name=name)
|
||||
lib.cells.append(structure)
|
||||
|
||||
structure.properties += annotations_to_properties(pat.annotations)
|
||||
|
|
@ -229,7 +226,6 @@ def readfile(
|
|||
|
||||
def read(
|
||||
stream: io.BufferedIOBase,
|
||||
clean_vertices: bool = True,
|
||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||
"""
|
||||
Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are
|
||||
|
|
@ -243,9 +239,6 @@ def read(
|
|||
|
||||
Args:
|
||||
stream: Stream to read from.
|
||||
clean_vertices: If `True`, remove any redundant vertices when loading polygons.
|
||||
The cleaning process removes any polygons with zero area or <3 vertices.
|
||||
Default `True`.
|
||||
|
||||
Returns:
|
||||
- Dict of `pattern_name`:`Pattern`s generated from OASIS cells
|
||||
|
|
@ -264,14 +257,14 @@ def read(
|
|||
layer_map[str(layer_name.nstring)] = layer_name
|
||||
library_info['layer_map'] = layer_map
|
||||
|
||||
patterns = []
|
||||
patterns_dict = {}
|
||||
for cell in lib.cells:
|
||||
if isinstance(cell.name, int):
|
||||
cell_name = lib.cellnames[cell.name].nstring.string
|
||||
else:
|
||||
cell_name = cell.name.string
|
||||
|
||||
pat = Pattern(name=cell_name)
|
||||
pat = Pattern()
|
||||
for element in cell.geometry:
|
||||
if isinstance(element, fatrec.XElement):
|
||||
logger.warning('Skipping XElement record')
|
||||
|
|
@ -453,19 +446,7 @@ def read(
|
|||
for placement in cell.placements:
|
||||
pat.subpatterns.append(_placement_to_subpat(placement, lib))
|
||||
|
||||
if clean_vertices:
|
||||
clean_pattern_vertices(pat)
|
||||
patterns.append(pat)
|
||||
|
||||
# Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
|
||||
# according to the subpattern.identifier (which is deleted after use).
|
||||
patterns_dict = dict(((p.name, p) for p in patterns))
|
||||
for p in patterns_dict.values():
|
||||
for sp in p.subpatterns:
|
||||
ident = sp.identifier[0]
|
||||
name = ident if isinstance(ident, str) else lib.cellnames[ident].nstring.string
|
||||
sp.pattern = patterns_dict[name]
|
||||
del sp.identifier
|
||||
patterns_dict[name] = pat
|
||||
|
||||
return patterns_dict, library_info
|
||||
|
||||
|
|
@ -489,8 +470,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
|
|||
|
||||
def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> SubPattern:
|
||||
"""
|
||||
Helper function to create a SubPattern from a placment. Sets subpat.pattern to None
|
||||
and sets the instance .identifier to (struct_name,).
|
||||
Helper function to create a SubPattern from a placment. Sets subpat.target to the placemen name.
|
||||
"""
|
||||
assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition))
|
||||
xy = numpy.array((placement.x, placement.y))
|
||||
|
|
@ -502,14 +482,15 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo
|
|||
rotation = 0
|
||||
else:
|
||||
rotation = numpy.deg2rad(float(placement.angle))
|
||||
subpat = SubPattern(offset=xy,
|
||||
pattern=None,
|
||||
mirrored=(placement.flip, False),
|
||||
rotation=rotation,
|
||||
scale=float(mag),
|
||||
identifier=(name,),
|
||||
repetition=repetition_fata2masq(placement.repetition),
|
||||
annotations=annotations)
|
||||
subpat = SubPattern(
|
||||
target=name,
|
||||
offset=xy,
|
||||
mirrored=(placement.flip, False),
|
||||
rotation=rotation,
|
||||
scale=float(mag),
|
||||
repetition=repetition_fata2masq(placement.repetition),
|
||||
annotations=annotations,
|
||||
)
|
||||
return subpat
|
||||
|
||||
|
||||
|
|
@ -518,17 +499,17 @@ def _subpatterns_to_placements(
|
|||
) -> List[fatrec.Placement]:
|
||||
refs = []
|
||||
for subpat in subpatterns:
|
||||
if subpat.pattern is None:
|
||||
if subpat.target is None:
|
||||
continue
|
||||
|
||||
# Note: OASIS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
||||
frep, rep_offset = repetition_masq2fata(subpat.repetition)
|
||||
|
||||
offset = numpy.round(subpat.offset + rep_offset).astype(int)
|
||||
offset = rint_cast(subpat.offset + rep_offset)
|
||||
angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
|
||||
ref = fatrec.Placement(
|
||||
name=subpat.pattern.name,
|
||||
name=subpat.target,
|
||||
flip=mirror_across_x,
|
||||
angle=angle,
|
||||
magnification=subpat.scale,
|
||||
|
|
@ -552,46 +533,51 @@ def _shapes_to_elements(
|
|||
repetition, rep_offset = repetition_masq2fata(shape.repetition)
|
||||
properties = annotations_to_properties(shape.annotations)
|
||||
if isinstance(shape, Circle):
|
||||
offset = numpy.round(shape.offset + rep_offset).astype(int)
|
||||
radius = numpy.round(shape.radius).astype(int)
|
||||
circle = fatrec.Circle(layer=layer,
|
||||
datatype=datatype,
|
||||
radius=radius,
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
properties=properties,
|
||||
repetition=repetition)
|
||||
offset = rint_cast(shape.offset + rep_offset)
|
||||
radius = rint_cast(shape.radius)
|
||||
circle = fatrec.Circle(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
radius=radius,
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
elements.append(circle)
|
||||
elif isinstance(shape, Path):
|
||||
xy = numpy.round(shape.offset + shape.vertices[0] + rep_offset).astype(int)
|
||||
deltas = numpy.round(numpy.diff(shape.vertices, axis=0)).astype(int)
|
||||
half_width = numpy.round(shape.width / 2).astype(int)
|
||||
xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset)
|
||||
deltas = rint_cast(numpy.diff(shape.vertices, axis=0))
|
||||
half_width = rint_cast(shape.width / 2)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
|
||||
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
|
||||
path = fatrec.Path(layer=layer,
|
||||
datatype=datatype,
|
||||
point_list=deltas,
|
||||
half_width=half_width,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
extension_start=extension_start, # TODO implement multiple cap types?
|
||||
extension_end=extension_end,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
path = fatrec.Path(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
point_list=deltas,
|
||||
half_width=half_width,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
extension_start=extension_start, # TODO implement multiple cap types?
|
||||
extension_end=extension_end,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
elements.append(path)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy = numpy.round(polygon.offset + polygon.vertices[0] + rep_offset).astype(int)
|
||||
points = numpy.round(numpy.diff(polygon.vertices, axis=0)).astype(int)
|
||||
elements.append(fatrec.Polygon(layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
point_list=points,
|
||||
properties=properties,
|
||||
repetition=repetition))
|
||||
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
|
||||
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
|
||||
elements.append(fatrec.Polygon(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
point_list=points,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
return elements
|
||||
|
||||
|
||||
|
|
@ -603,29 +589,31 @@ def _labels_to_texts(
|
|||
for label in labels:
|
||||
layer, datatype = layer2oas(label.layer)
|
||||
repetition, rep_offset = repetition_masq2fata(label.repetition)
|
||||
xy = numpy.round(label.offset + rep_offset).astype(int)
|
||||
xy = rint_cast(label.offset + rep_offset)
|
||||
properties = annotations_to_properties(label.annotations)
|
||||
texts.append(fatrec.Text(layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
string=label.string,
|
||||
properties=properties,
|
||||
repetition=repetition))
|
||||
texts.append(fatrec.Text(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
string=label.string,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
return texts
|
||||
|
||||
|
||||
def disambiguate_pattern_names(
|
||||
patterns,
|
||||
names: Iterable[str],
|
||||
dup_warn_filter: Callable[[str], bool] = None, # If returns False, don't warn about this name
|
||||
) -> None:
|
||||
used_names = []
|
||||
for pat in patterns:
|
||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', pat.name)
|
||||
) -> List[str]:
|
||||
new_names = []
|
||||
for name in names:
|
||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', name)
|
||||
|
||||
i = 0
|
||||
suffixed_name = sanitized_name
|
||||
while suffixed_name in used_names or suffixed_name == '':
|
||||
while suffixed_name in new_names or suffixed_name == '':
|
||||
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
||||
|
||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||
|
|
@ -634,16 +622,16 @@ def disambiguate_pattern_names(
|
|||
if sanitized_name == '':
|
||||
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
||||
elif suffixed_name != sanitized_name:
|
||||
if dup_warn_filter is None or dup_warn_filter(pat.name):
|
||||
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
|
||||
if dup_warn_filter is None or dup_warn_filter(name):
|
||||
logger.warning(f'Pattern name "{name}" ({sanitized_name}) appears multiple times;\n'
|
||||
+ f' renaming to "{suffixed_name}"')
|
||||
|
||||
if len(suffixed_name) == 0:
|
||||
# Should never happen since zero-length names are replaced
|
||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"')
|
||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{name}"')
|
||||
|
||||
pat.name = suffixed_name
|
||||
used_names.append(suffixed_name)
|
||||
new_names.append(suffixed_name)
|
||||
return new_names
|
||||
|
||||
|
||||
def repetition_fata2masq(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Notes:
|
|||
* GDS does not support library- or structure-level annotations
|
||||
"""
|
||||
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Optional
|
||||
from typing import Sequence
|
||||
from typing import Sequence, Mapping
|
||||
import re
|
||||
import io
|
||||
import copy
|
||||
|
|
@ -59,13 +59,13 @@ def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
|||
|
||||
|
||||
def build(
|
||||
patterns: Union[Pattern, Sequence[Pattern]],
|
||||
library: Mapping[str, Pattern],
|
||||
meters_per_unit: float,
|
||||
logical_units_per_unit: float = 1,
|
||||
library_name: str = 'masque-gdsii-write',
|
||||
*,
|
||||
modify_originals: bool = False,
|
||||
disambiguate_func: Callable[[Iterable[Pattern]], None] = None,
|
||||
disambiguate_func: Callable[[Iterable[str]], List[str]] = None,
|
||||
) -> gdsii.library.Library:
|
||||
"""
|
||||
Convert a `Pattern` or list of patterns to a GDSII stream, by first calling
|
||||
|
|
@ -86,7 +86,7 @@ def build(
|
|||
prior to calling this function.
|
||||
|
||||
Args:
|
||||
patterns: A Pattern or list of patterns to convert.
|
||||
library: A {name: Pattern} mapping of patterns to write.
|
||||
meters_per_unit: Written into the GDSII file, meters per (database) length unit.
|
||||
All distances are assumed to be an integer multiple of this unit, and are stored as such.
|
||||
logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
|
||||
|
|
@ -95,27 +95,29 @@ def build(
|
|||
library_name: Library name written into the GDSII file.
|
||||
Default 'masque-gdsii-write'.
|
||||
modify_originals: If `True`, the original pattern is modified as part of the writing
|
||||
process. Otherwise, a copy is made and `deepunlock()`-ed.
|
||||
process. Otherwise, a copy is made.
|
||||
Default `False`.
|
||||
disambiguate_func: Function which takes a list of patterns and alters them
|
||||
to make their names valid and unique. Default is `disambiguate_pattern_names`, which
|
||||
attempts to adhere to the GDSII standard as well as possible.
|
||||
disambiguate_func: Function which takes a list of pattern names and returns a list of names
|
||||
altered to be valid and unique. Default is `disambiguate_pattern_names`, which
|
||||
attempts to adhere to the GDSII standard reasonably well.
|
||||
WARNING: No additional error checking is performed on the results.
|
||||
|
||||
Returns:
|
||||
`gdsii.library.Library`
|
||||
"""
|
||||
if isinstance(patterns, Pattern):
|
||||
patterns = [patterns]
|
||||
|
||||
if disambiguate_func is None:
|
||||
disambiguate_func = disambiguate_pattern_names # type: ignore
|
||||
assert(disambiguate_func is not None) # placate mypy
|
||||
disambiguate_func = disambiguate_pattern_names
|
||||
|
||||
if not modify_originals:
|
||||
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)]
|
||||
library = copy.deepcopy(library)
|
||||
|
||||
patterns = [p.wrap_repeated_shapes() for p in patterns]
|
||||
for p in library.values():
|
||||
library.add(p.wrap_repeated_shapes())
|
||||
|
||||
old_names = list(library.keys())
|
||||
new_names = disambiguate_func(old_names)
|
||||
renamed_lib = {new_name: library[old_name]
|
||||
for old_name, new_name in zip(old_names, new_names)}
|
||||
|
||||
# Create library
|
||||
lib = gdsii.library.Library(version=600,
|
||||
|
|
@ -123,17 +125,9 @@ def build(
|
|||
logical_unit=logical_units_per_unit,
|
||||
physical_unit=meters_per_unit)
|
||||
|
||||
# Get a dict of id(pattern) -> pattern
|
||||
patterns_by_id = {id(pattern): pattern for pattern in patterns}
|
||||
for pattern in patterns:
|
||||
for i, p in pattern.referenced_patterns_by_id().items():
|
||||
patterns_by_id[i] = p
|
||||
|
||||
disambiguate_func(patterns_by_id.values())
|
||||
|
||||
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
||||
for pat in patterns_by_id.values():
|
||||
structure = gdsii.structure.Structure(name=pat.name.encode('ASCII'))
|
||||
for name, pat in renamed_lib.items():
|
||||
structure = gdsii.structure.Structure(name=name.encode('ASCII'))
|
||||
lib.append(structure)
|
||||
|
||||
structure += _shapes_to_elements(pat.shapes)
|
||||
|
|
@ -144,7 +138,7 @@ def build(
|
|||
|
||||
|
||||
def write(
|
||||
patterns: Union[Pattern, Sequence[Pattern]],
|
||||
library: Mapping[str, Pattern],
|
||||
stream: io.BufferedIOBase,
|
||||
*args,
|
||||
**kwargs,
|
||||
|
|
@ -154,31 +148,31 @@ def write(
|
|||
See `masque.file.gdsii.build()` for details.
|
||||
|
||||
Args:
|
||||
patterns: A Pattern or list of patterns to write to file.
|
||||
library: A {name: Pattern} mapping of patterns to write.
|
||||
stream: Stream to write to.
|
||||
*args: passed to `masque.file.gdsii.build()`
|
||||
**kwargs: passed to `masque.file.gdsii.build()`
|
||||
"""
|
||||
lib = build(patterns, *args, **kwargs)
|
||||
lib = build(library, *args, **kwargs)
|
||||
lib.save(stream)
|
||||
return
|
||||
|
||||
def writefile(
|
||||
patterns: Union[Sequence[Pattern], Pattern],
|
||||
library: Mapping[str, Pattern],
|
||||
filename: Union[str, pathlib.Path],
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
Wrapper for `masque.file.gdsii.write()` that takes a filename or path instead of a stream.
|
||||
Wrapper for `write()` that takes a filename or path instead of a stream.
|
||||
|
||||
Will automatically compress the file if it has a .gz suffix.
|
||||
|
||||
Args:
|
||||
patterns: `Pattern` or list of patterns to save
|
||||
library: {name: Pattern} pairs to save.
|
||||
filename: Filename to save to.
|
||||
*args: passed to `masque.file.gdsii.write`
|
||||
**kwargs: passed to `masque.file.gdsii.write`
|
||||
*args: passed to `write()`
|
||||
**kwargs: passed to `write()`
|
||||
"""
|
||||
path = pathlib.Path(filename)
|
||||
if path.suffix == '.gz':
|
||||
|
|
@ -196,14 +190,14 @@ def readfile(
|
|||
**kwargs,
|
||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||
"""
|
||||
Wrapper for `masque.file.gdsii.read()` that takes a filename or path instead of a stream.
|
||||
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 `masque.file.gdsii.read`
|
||||
**kwargs: passed to `masque.file.gdsii.read`
|
||||
*args: passed to `read()`
|
||||
**kwargs: passed to `read()`
|
||||
"""
|
||||
path = pathlib.Path(filename)
|
||||
if is_gzipped(path):
|
||||
|
|
@ -251,9 +245,10 @@ def read(
|
|||
|
||||
raw_mode = True # Whether to construct shapes in raw mode (less error checking)
|
||||
|
||||
patterns = []
|
||||
patterns_dict = {}
|
||||
for structure in lib:
|
||||
pat = Pattern(name=structure.name.decode('ASCII'))
|
||||
pat = Pattern()
|
||||
name=structure.name.decode('ASCII')
|
||||
for element in structure:
|
||||
# Switch based on element type:
|
||||
if isinstance(element, gdsii.elements.Boundary):
|
||||
|
|
@ -275,15 +270,7 @@ def read(
|
|||
|
||||
if clean_vertices:
|
||||
clean_pattern_vertices(pat)
|
||||
patterns.append(pat)
|
||||
|
||||
# Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
|
||||
# according to the subpattern.identifier (which is deleted after use).
|
||||
patterns_dict = dict(((p.name, p) for p in patterns))
|
||||
for p in patterns_dict.values():
|
||||
for sp in p.subpatterns:
|
||||
sp.pattern = patterns_dict[sp.identifier[0].decode('ASCII')]
|
||||
del sp.identifier
|
||||
patterns_dict[name] = pat
|
||||
|
||||
return patterns_dict, library_info
|
||||
|
||||
|
|
@ -309,8 +296,7 @@ def _ref_to_subpat(
|
|||
gdsii.elements.ARef]
|
||||
) -> SubPattern:
|
||||
"""
|
||||
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None
|
||||
and sets the instance .identifier to (struct_name,).
|
||||
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.target to struct_name.
|
||||
|
||||
NOTE: "Absolute" means not affected by parent elements.
|
||||
That's not currently supported by masque at all (and not planned).
|
||||
|
|
@ -351,7 +337,6 @@ def _ref_to_subpat(
|
|||
mirrored=(mirror_across_x, False),
|
||||
annotations=_properties_to_annotations(element.properties),
|
||||
repetition=repetition)
|
||||
subpat.identifier = (element.struct_name,)
|
||||
return subpat
|
||||
|
||||
|
||||
|
|
@ -395,9 +380,9 @@ def _subpatterns_to_refs(
|
|||
) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]:
|
||||
refs = []
|
||||
for subpat in subpatterns:
|
||||
if subpat.pattern is None:
|
||||
if subpat.target is None:
|
||||
continue
|
||||
encoded_name = subpat.pattern.name.encode('ASCII')
|
||||
encoded_name = subpat.target.encode('ASCII')
|
||||
|
||||
# Note: GDS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
||||
|
|
@ -523,14 +508,14 @@ def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
|
|||
|
||||
|
||||
def disambiguate_pattern_names(
|
||||
patterns: Sequence[Pattern],
|
||||
names: Iterable[str],
|
||||
max_name_length: int = 32,
|
||||
suffix_length: int = 6,
|
||||
dup_warn_filter: Optional[Callable[[str], bool]] = None,
|
||||
) -> None:
|
||||
) -> List[str]:
|
||||
"""
|
||||
Args:
|
||||
patterns: List of patterns to disambiguate
|
||||
names: List of pattern names to disambiguate
|
||||
max_name_length: Names longer than this will be truncated
|
||||
suffix_length: Names which get truncated are truncated by this many extra characters. This is to
|
||||
leave room for a suffix if one is necessary.
|
||||
|
|
@ -538,15 +523,15 @@ def disambiguate_pattern_names(
|
|||
the cell name and returns `False` if the warning should be suppressed and `True` if it should
|
||||
be displayed. Default displays all warnings.
|
||||
"""
|
||||
used_names = []
|
||||
for pat in set(patterns):
|
||||
new_names = []
|
||||
for name in names:
|
||||
# Shorten names which already exceed max-length
|
||||
if len(pat.name) > max_name_length:
|
||||
shortened_name = pat.name[:max_name_length - suffix_length]
|
||||
logger.warning(f'Pattern name "{pat.name}" is too long ({len(pat.name)}/{max_name_length} chars),\n'
|
||||
if len(name) > max_name_length:
|
||||
shortened_name = name[:max_name_length - suffix_length]
|
||||
logger.warning(f'Pattern name "{name}" is too long ({len(name)}/{max_name_length} chars),\n'
|
||||
+ f' shortening to "{shortened_name}" before generating suffix')
|
||||
else:
|
||||
shortened_name = pat.name
|
||||
shortened_name = name
|
||||
|
||||
# Remove invalid characters
|
||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
|
||||
|
|
@ -554,7 +539,7 @@ def disambiguate_pattern_names(
|
|||
# Add a suffix that makes the name unique
|
||||
i = 0
|
||||
suffixed_name = sanitized_name
|
||||
while suffixed_name in used_names or suffixed_name == '':
|
||||
while suffixed_name in new_names or suffixed_name == '':
|
||||
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
||||
|
||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
||||
|
|
@ -563,18 +548,19 @@ def disambiguate_pattern_names(
|
|||
if sanitized_name == '':
|
||||
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
||||
elif suffixed_name != sanitized_name:
|
||||
if dup_warn_filter is None or dup_warn_filter(pat.name):
|
||||
logger.warning(f'Pattern name "{pat.name}" ({sanitized_name}) appears multiple times;\n'
|
||||
if dup_warn_filter is None or dup_warn_filter(name):
|
||||
logger.warning(f'Pattern name "{name}" ({sanitized_name}) appears multiple times;\n'
|
||||
+ f' renaming to "{suffixed_name}"')
|
||||
|
||||
# Encode into a byte-string and perform some final checks
|
||||
encoded_name = suffixed_name.encode('ASCII')
|
||||
if len(encoded_name) == 0:
|
||||
# Should never happen since zero-length names are replaced
|
||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.name}"')
|
||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{name}"')
|
||||
if len(encoded_name) > max_name_length:
|
||||
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n'
|
||||
+ f' originally "{pat.name}"')
|
||||
+ f' originally "{name}"')
|
||||
|
||||
new_names.append(suffixed_name)
|
||||
return new_names
|
||||
|
||||
pat.name = suffixed_name
|
||||
used_names.append(suffixed_name)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
SVG file format readers and writers
|
||||
"""
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Mapping
|
||||
import warnings
|
||||
|
||||
import numpy
|
||||
|
|
@ -13,7 +13,8 @@ from .. import Pattern
|
|||
|
||||
|
||||
def writefile(
|
||||
pattern: Pattern,
|
||||
library: Mapping[str, Pattern],
|
||||
top: str,
|
||||
filename: str,
|
||||
custom_attributes: bool = False,
|
||||
) -> None:
|
||||
|
|
@ -41,11 +42,12 @@ def writefile(
|
|||
custom_attributes: Whether to write non-standard `pattern_layer` and
|
||||
`pattern_dose` attributes to the SVG elements.
|
||||
"""
|
||||
pattern = library[top]
|
||||
|
||||
# Polygonize pattern
|
||||
pattern.polygonize()
|
||||
|
||||
bounds = pattern.get_bounds()
|
||||
bounds = pattern.get_bounds(library=library)
|
||||
if bounds is None:
|
||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
||||
|
|
@ -59,15 +61,10 @@ def writefile(
|
|||
svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string,
|
||||
debug=(not custom_attributes))
|
||||
|
||||
# Get a dict of id(pattern) -> pattern
|
||||
patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} # type: Dict[int, Optional[Pattern]]
|
||||
|
||||
# Now create a group for each row in sd_table (ie, each pattern + dose combination)
|
||||
# and add in any Boundary and Use elements
|
||||
for pat in patterns_by_id.values():
|
||||
if pat is None:
|
||||
continue
|
||||
svg_group = svg.g(id=mangle_name(pat), fill='blue', stroke='red')
|
||||
for name, pat in library.items():
|
||||
svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red')
|
||||
|
||||
for shape in pat.shapes:
|
||||
for polygon in shape.to_polygons():
|
||||
|
|
@ -81,20 +78,24 @@ def writefile(
|
|||
svg_group.add(path)
|
||||
|
||||
for subpat in pat.subpatterns:
|
||||
if subpat.pattern is None:
|
||||
if subpat.target is None:
|
||||
continue
|
||||
transform = f'scale({subpat.scale:g}) rotate({subpat.rotation:g}) translate({subpat.offset[0]:g},{subpat.offset[1]:g})'
|
||||
use = svg.use(href='#' + mangle_name(subpat.pattern), transform=transform)
|
||||
use = svg.use(href='#' + mangle_name(subpat.target), transform=transform)
|
||||
if custom_attributes:
|
||||
use['pattern_dose'] = subpat.dose
|
||||
svg_group.add(use)
|
||||
|
||||
svg.defs.add(svg_group)
|
||||
svg.add(svg.use(href='#' + mangle_name(pattern)))
|
||||
svg.add(svg.use(href='#' + mangle_name(top)))
|
||||
svg.save()
|
||||
|
||||
|
||||
def writefile_inverted(pattern: Pattern, filename: str):
|
||||
def writefile_inverted(
|
||||
library: Mapping[str, Pattern],
|
||||
top: str,
|
||||
filename: str,
|
||||
) -> None:
|
||||
"""
|
||||
Write an inverted Pattern to an SVG file, by first calling `.polygonize()` and
|
||||
`.flatten()` on it to change the shapes into polygons, then drawing a bounding
|
||||
|
|
@ -110,10 +111,12 @@ def writefile_inverted(pattern: Pattern, filename: str):
|
|||
pattern: Pattern to write to file. Modified by this function.
|
||||
filename: Filename to write to.
|
||||
"""
|
||||
pattern = library[top]
|
||||
|
||||
# Polygonize and flatten pattern
|
||||
pattern.polygonize().flatten()
|
||||
|
||||
bounds = pattern.get_bounds()
|
||||
bounds = pattern.get_bounds(library=library)
|
||||
if bounds is None:
|
||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Helper functions for file reading and writing
|
||||
"""
|
||||
from typing import Set, Tuple, List
|
||||
from typing import Set, Tuple, List, Iterable, Mapping
|
||||
import re
|
||||
import copy
|
||||
import pathlib
|
||||
|
|
@ -10,19 +10,22 @@ from .. import Pattern, PatternError
|
|||
from ..shapes import Polygon, Path
|
||||
|
||||
|
||||
def mangle_name(pattern: Pattern, dose_multiplier: float = 1.0) -> str:
|
||||
def mangle_name(name: str, dose_multiplier: float = 1.0) -> str:
|
||||
"""
|
||||
Create a name using `pattern.name`, `id(pattern)`, and the dose multiplier.
|
||||
Create a new name using `name` and the `dose_multiplier`.
|
||||
|
||||
Args:
|
||||
pattern: Pattern whose name we want to mangle.
|
||||
name: Name we want to mangle.
|
||||
dose_multiplier: Dose multiplier to mangle with.
|
||||
|
||||
Returns:
|
||||
Mangled name.
|
||||
"""
|
||||
if dose_multiplier == 1:
|
||||
full_name = name
|
||||
else:
|
||||
full_name = f'{name}_dm{dose_multiplier}'
|
||||
expression = re.compile(r'[^A-Za-z0-9_\?\$]')
|
||||
full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern))
|
||||
sanitized_name = expression.sub('_', full_name)
|
||||
return sanitized_name
|
||||
|
||||
|
|
@ -51,25 +54,30 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
|
|||
return pat
|
||||
|
||||
|
||||
def make_dose_table(patterns: List[Pattern], dose_multiplier: float = 1.0) -> Set[Tuple[int, float]]:
|
||||
def make_dose_table(
|
||||
top_names: Iterable[str],
|
||||
library: Mapping[str, Pattern],
|
||||
dose_multiplier: float = 1.0,
|
||||
) -> Set[Tuple[int, float]]:
|
||||
"""
|
||||
Create a set containing `(id(pat), written_dose)` for each pattern (including subpatterns)
|
||||
Create a set containing `(name, written_dose)` for each pattern (including subpatterns)
|
||||
|
||||
Args:
|
||||
top_names: Names of all topcells
|
||||
pattern: Source Patterns.
|
||||
dose_multiplier: Multiplier for all written_dose entries.
|
||||
|
||||
Returns:
|
||||
`{(id(subpat.pattern), written_dose), ...}`
|
||||
`{(name, written_dose), ...}`
|
||||
"""
|
||||
dose_table = {(id(pattern), dose_multiplier) for pattern in patterns}
|
||||
for pattern in patterns:
|
||||
dose_table = {(top_name, dose_multiplier) for top_name in top_names}
|
||||
for name, pattern in library.items():
|
||||
for subpat in pattern.subpatterns:
|
||||
if subpat.pattern is None:
|
||||
if subpat.target is None:
|
||||
continue
|
||||
subpat_dose_entry = (id(subpat.pattern), subpat.dose * dose_multiplier)
|
||||
subpat_dose_entry = (subpat.target, subpat.dose * dose_multiplier)
|
||||
if subpat_dose_entry not in dose_table:
|
||||
subpat_dose_table = make_dose_table([subpat.pattern], subpat.dose * dose_multiplier)
|
||||
subpat_dose_table = make_dose_table(subpat.target, library, subpat.dose * dose_multiplier)
|
||||
dose_table = dose_table.union(subpat_dose_table)
|
||||
return dose_table
|
||||
|
||||
|
|
@ -96,7 +104,7 @@ def dtype2dose(pattern: Pattern) -> Pattern:
|
|||
|
||||
|
||||
def dose2dtype(
|
||||
patterns: List[Pattern],
|
||||
library: List[Pattern],
|
||||
) -> Tuple[List[Pattern], List[float]]:
|
||||
"""
|
||||
For each shape in each pattern, set shape.layer to the tuple
|
||||
|
|
@ -119,21 +127,16 @@ def dose2dtype(
|
|||
dose_list: A list of doses, providing a mapping between datatype (int, list index)
|
||||
and dose (float, list entry).
|
||||
"""
|
||||
# Get a dict of id(pattern) -> pattern
|
||||
patterns_by_id = {id(pattern): pattern for pattern in patterns}
|
||||
for pattern in patterns:
|
||||
for i, p in pattern.referenced_patterns_by_id().items():
|
||||
patterns_by_id[i] = p
|
||||
|
||||
logger.warning('TODO: dose2dtype() needs to be tested!')
|
||||
# Get a table of (id(pat), written_dose) for each pattern and subpattern
|
||||
sd_table = make_dose_table(patterns)
|
||||
sd_table = make_dose_table(library.find_topcells(), library)
|
||||
|
||||
# Figure out all the unique doses necessary to write this pattern
|
||||
# This means going through each row in sd_table and adding the dose values needed to write
|
||||
# that subpattern at that dose level
|
||||
dose_vals = set()
|
||||
for pat_id, pat_dose in sd_table:
|
||||
pat = patterns_by_id[pat_id]
|
||||
for name, pat_dose in sd_table:
|
||||
pat = library[name]
|
||||
for shape in pat.shapes:
|
||||
dose_vals.add(shape.dose * pat_dose)
|
||||
|
||||
|
|
@ -144,21 +147,22 @@ def dose2dtype(
|
|||
|
||||
# Create a new pattern for each non-1-dose entry in the dose table
|
||||
# and update the shapes to reflect their new dose
|
||||
new_pats = {} # (id, dose) -> new_pattern mapping
|
||||
for pat_id, pat_dose in sd_table:
|
||||
new_names = {} # {(old name, dose): new name} mapping
|
||||
new_lib = {} # {new_name: new_pattern} mapping
|
||||
for name, pat_dose in sd_table:
|
||||
mangled_name = mangle_name(name, pat_dose)
|
||||
new_names[(name, pat_dose)] = mangled_name
|
||||
|
||||
old_pat = library[name]
|
||||
|
||||
if pat_dose == 1:
|
||||
new_pats[(pat_id, pat_dose)] = patterns_by_id[pat_id]
|
||||
new_lib[mangled_name] = old_pat
|
||||
continue
|
||||
|
||||
old_pat = patterns_by_id[pat_id]
|
||||
pat = old_pat.copy() # keep old subpatterns
|
||||
pat.shapes = copy.deepcopy(old_pat.shapes)
|
||||
pat.labels = copy.deepcopy(old_pat.labels)
|
||||
pat = old_pat.deepcopy()
|
||||
|
||||
encoded_name = mangle_name(pat, pat_dose)
|
||||
if len(encoded_name) == 0:
|
||||
raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name))
|
||||
pat.name = encoded_name
|
||||
raise PatternError('Zero-length name after mangle+encode, originally "{name}"'.format(pat.name))
|
||||
|
||||
for shape in pat.shapes:
|
||||
data_type = dose_vals_list.index(shape.dose * pat_dose)
|
||||
|
|
@ -169,15 +173,9 @@ def dose2dtype(
|
|||
else:
|
||||
raise PatternError(f'Invalid layer for gdsii: {shape.layer}')
|
||||
|
||||
new_pats[(pat_id, pat_dose)] = pat
|
||||
new_lib[mangled_name] = pat
|
||||
|
||||
# Go back through all the dose-specific patterns and fix up their subpattern entries
|
||||
for (pat_id, pat_dose), pat in new_pats.items():
|
||||
for subpat in pat.subpatterns:
|
||||
dose_mult = subpat.dose * pat_dose
|
||||
subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)]
|
||||
|
||||
return patterns, dose_vals_list
|
||||
return new_lib, dose_vals_list
|
||||
|
||||
|
||||
def is_gzipped(path: pathlib.Path) -> bool:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue