WIP: make libraries and names first-class!

This commit is contained in:
jan 2022-07-07 11:27:29 -07:00
commit 7aaf73cb37
34 changed files with 1780 additions and 1812 deletions

View file

@ -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

View file

@ -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)

View file

@ -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(

View file

@ -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)

View file

@ -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')

View file

@ -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: