|
|
|
@ -18,8 +18,8 @@ Notes:
|
|
|
|
|
* GDS does not support library- or structure-level annotations |
|
|
|
|
* Creation/modification/access times are set to 1900-01-01 for reproducibility. |
|
|
|
|
""" |
|
|
|
|
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional |
|
|
|
|
from typing import Sequence, Mapping, BinaryIO |
|
|
|
|
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Optional |
|
|
|
|
from typing import Sequence, BinaryIO |
|
|
|
|
import re |
|
|
|
|
import io |
|
|
|
|
import mmap |
|
|
|
@ -29,29 +29,27 @@ import struct
|
|
|
|
|
import logging |
|
|
|
|
import pathlib |
|
|
|
|
import gzip |
|
|
|
|
from itertools import chain |
|
|
|
|
|
|
|
|
|
import numpy # type: ignore |
|
|
|
|
import klamath |
|
|
|
|
from klamath import records |
|
|
|
|
|
|
|
|
|
from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose, is_gzipped |
|
|
|
|
from .utils import is_gzipped |
|
|
|
|
from .. import Pattern, SubPattern, PatternError, Label, Shape |
|
|
|
|
from ..shapes import Polygon, Path |
|
|
|
|
from ..repetition import Grid |
|
|
|
|
from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t |
|
|
|
|
from ..utils import remove_colinear_vertices, normalize_mirror, annotations_t |
|
|
|
|
from ..utils import layer_t, normalize_mirror, annotations_t |
|
|
|
|
from ..library import Library |
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
path_cap_map = { |
|
|
|
|
0: Path.Cap.Flush, |
|
|
|
|
1: Path.Cap.Circle, |
|
|
|
|
2: Path.Cap.Square, |
|
|
|
|
4: Path.Cap.SquareCustom, |
|
|
|
|
} |
|
|
|
|
0: Path.Cap.Flush, |
|
|
|
|
1: Path.Cap.Circle, |
|
|
|
|
2: Path.Cap.Square, |
|
|
|
|
4: Path.Cap.SquareCustom, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write(patterns: Union[Pattern, Sequence[Pattern]], |
|
|
|
@ -144,15 +142,15 @@ def writefile(patterns: Union[Sequence[Pattern], Pattern],
|
|
|
|
|
**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 |
|
|
|
|
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': |
|
|
|
@ -169,14 +167,14 @@ def readfile(filename: Union[str, pathlib.Path],
|
|
|
|
|
**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): |
|
|
|
@ -185,7 +183,7 @@ def readfile(filename: Union[str, pathlib.Path],
|
|
|
|
|
open_func = open |
|
|
|
|
|
|
|
|
|
with io.BufferedReader(open_func(path, mode='rb')) as stream: |
|
|
|
|
results = read(stream)#, *args, **kwargs) |
|
|
|
|
results = read(stream, *args, **kwargs) |
|
|
|
|
return results |
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -216,7 +214,7 @@ def read(stream: BinaryIO,
|
|
|
|
|
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')) |
|
|
|
|
pat = read_elements(stream, name=name.decode('ASCII'), raw_mode=raw_mode) |
|
|
|
|
patterns.append(pat) |
|
|
|
|
found_struct = records.BGNSTR.skip_past(stream) |
|
|
|
|
|
|
|
|
@ -368,10 +366,10 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]
|
|
|
|
|
|
|
|
|
|
if isinstance(rep, Grid): |
|
|
|
|
xy = numpy.array(subpat.offset) + [ |
|
|
|
|
[0, 0], |
|
|
|
|
rep.a_vector * rep.a_count, |
|
|
|
|
rep.b_vector * rep.b_count, |
|
|
|
|
] |
|
|
|
|
[0, 0], |
|
|
|
|
rep.a_vector * rep.a_count, |
|
|
|
|
rep.b_vector * rep.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)), |
|
|
|
@ -412,7 +410,7 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
|
|
|
|
|
for key, vals in annotations.items(): |
|
|
|
|
try: |
|
|
|
|
i = int(key) |
|
|
|
|
except: |
|
|
|
|
except ValueError: |
|
|
|
|
raise PatternError(f'Annotation key {key} is not convertable to an integer') |
|
|
|
|
if not (0 < i < 126): |
|
|
|
|
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])') |
|
|
|
@ -439,7 +437,7 @@ def _shapes_to_elements(shapes: List[Shape],
|
|
|
|
|
if isinstance(shape, Path) and not polygonize_paths: |
|
|
|
|
xy = numpy.round(shape.vertices + shape.offset).astype(int) |
|
|
|
|
width = numpy.round(shape.width).astype(int) |
|
|
|
|
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) #reverse lookup |
|
|
|
|
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup |
|
|
|
|
|
|
|
|
|
extension: Tuple[int, int] |
|
|
|
|
if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None: |
|
|
|
@ -455,13 +453,13 @@ def _shapes_to_elements(shapes: List[Shape],
|
|
|
|
|
properties=properties) |
|
|
|
|
elements.append(path) |
|
|
|
|
elif isinstance(shape, Polygon): |
|
|
|
|
polygon = shape |
|
|
|
|
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) |
|
|
|
|
xy_closed = numpy.vstack((xy_open, xy_open[0, :])) |
|
|
|
|
boundary = klamath.elements.Boundary(layer=(layer, data_type), |
|
|
|
|
xy=xy_closed, |
|
|
|
|
properties=properties) |
|
|
|
|
elements.append(boundary) |
|
|
|
|
polygon = shape |
|
|
|
|
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) |
|
|
|
|
xy_closed = numpy.vstack((xy_open, xy_open[0, :])) |
|
|
|
|
boundary = klamath.elements.Boundary(layer=(layer, data_type), |
|
|
|
|
xy=xy_closed, |
|
|
|
|
properties=properties) |
|
|
|
|
elements.append(boundary) |
|
|
|
|
else: |
|
|
|
|
for polygon in shape.to_polygons(): |
|
|
|
|
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) |
|
|
|
@ -483,7 +481,7 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]:
|
|
|
|
|
xy=xy, |
|
|
|
|
string=label.string.encode('ASCII'), |
|
|
|
|
properties=properties, |
|
|
|
|
presentation=0, #TODO maybe set some of these? |
|
|
|
|
presentation=0, # TODO maybe set some of these? |
|
|
|
|
angle_deg=0, |
|
|
|
|
invert_y=False, |
|
|
|
|
width=0, |
|
|
|
@ -496,7 +494,7 @@ def _labels_to_texts(labels: List[Label]) -> List[klamath.elements.Text]:
|
|
|
|
|
def disambiguate_pattern_names(patterns: Sequence[Pattern], |
|
|
|
|
max_name_length: int = 32, |
|
|
|
|
suffix_length: int = 6, |
|
|
|
|
dup_warn_filter: Optional[Callable[[str,], bool]] = None, |
|
|
|
|
dup_warn_filter: Optional[Callable[[str], bool]] = None, |
|
|
|
|
): |
|
|
|
|
""" |
|
|
|
|
Args: |
|
|
|
@ -513,13 +511,13 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
|
|
|
|
|
# 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' + |
|
|
|
|
f' shortening to "{shortened_name}" before generating suffix') |
|
|
|
|
logger.warning(f'Pattern name "{pat.name}" is too long ({len(pat.name)}/{max_name_length} chars),\n' |
|
|
|
|
+ f' shortening to "{shortened_name}" before generating suffix') |
|
|
|
|
else: |
|
|
|
|
shortened_name = pat.name |
|
|
|
|
|
|
|
|
|
# Remove invalid characters |
|
|
|
|
sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', shortened_name) |
|
|
|
|
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name) |
|
|
|
|
|
|
|
|
|
# Add a suffix that makes the name unique |
|
|
|
|
i = 0 |
|
|
|
@ -534,8 +532,8 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
|
|
|
|
|
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' + |
|
|
|
|
f' renaming to "{suffixed_name}"') |
|
|
|
|
logger.warning(f'Pattern name "{pat.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') |
|
|
|
@ -543,8 +541,8 @@ def disambiguate_pattern_names(patterns: Sequence[Pattern],
|
|
|
|
|
# Should never happen since zero-length names are replaced |
|
|
|
|
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{pat.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}"') |
|
|
|
|
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n' |
|
|
|
|
+ f' originally "{pat.name}"') |
|
|
|
|
|
|
|
|
|
pat.name = suffixed_name |
|
|
|
|
used_names.append(suffixed_name) |
|
|
|
@ -576,7 +574,8 @@ def load_library(stream: BinaryIO,
|
|
|
|
|
Additional library info (dict, same format as from `read`). |
|
|
|
|
""" |
|
|
|
|
if is_secondary is None: |
|
|
|
|
is_secondary = lambda k: False |
|
|
|
|
def is_secondary(k: str): |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
stream.seek(0) |
|
|
|
|
library_info = _read_header(stream) |
|
|
|
@ -592,7 +591,7 @@ def load_library(stream: BinaryIO,
|
|
|
|
|
|
|
|
|
|
lib.set_value(name, tag, mkstruct, secondary=is_secondary(name)) |
|
|
|
|
|
|
|
|
|
return lib |
|
|
|
|
return lib, library_info |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_libraryfile(filename: Union[str, pathlib.Path], |
|
|
|
|