wip
This commit is contained in:
parent
d9ae8dd6e3
commit
9efb6f0eeb
18 changed files with 712 additions and 1113 deletions
|
|
@ -13,7 +13,7 @@ import gzip
|
|||
import numpy
|
||||
import ezdxf # type: ignore
|
||||
|
||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
||||
from .. import Pattern, Ref, PatternError, Label, Shape
|
||||
from ..shapes import Polygon, Path
|
||||
from ..repetition import Grid
|
||||
from ..utils import rotation_matrix_2d, layer_t
|
||||
|
|
@ -39,7 +39,7 @@ def write(
|
|||
"""
|
||||
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
||||
into polygons, and then writing patterns as DXF `Block`s, polygons as `LWPolyline`s,
|
||||
and subpatterns as `Insert`s.
|
||||
and refs as `Insert`s.
|
||||
|
||||
The top level pattern's name is not written to the DXF file. Nested patterns keep their
|
||||
names.
|
||||
|
|
@ -49,7 +49,7 @@ def write(
|
|||
tuple: (1, 2) -> '1.2'
|
||||
str: '1.2' -> '1.2' (no change)
|
||||
|
||||
It is often a good idea to run `pattern.subpatternize()` prior to calling this function,
|
||||
It is often a good idea to run `pattern.dedup()` prior to calling this function,
|
||||
especially if calling `.polygonize()` will result in very many vertices.
|
||||
|
||||
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
|
||||
|
|
@ -86,7 +86,7 @@ def write(
|
|||
msp = lib.modelspace()
|
||||
_shapes_to_elements(msp, pattern.shapes)
|
||||
_labels_to_texts(msp, pattern.labels)
|
||||
_subpatterns_to_refs(msp, pattern.subpatterns)
|
||||
_mrefs_to_drefs(msp, pattern.refs)
|
||||
|
||||
# Now create a block for each referenced pattern, and add in any shapes
|
||||
for name, pat in library.items():
|
||||
|
|
@ -95,7 +95,7 @@ def write(
|
|||
|
||||
_shapes_to_elements(block, pat.shapes)
|
||||
_labels_to_texts(block, pat.labels)
|
||||
_subpatterns_to_refs(block, pat.subpatterns)
|
||||
_mrefs_to_drefs(block, pat.refs)
|
||||
|
||||
lib.write(stream)
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ def read(
|
|||
"""
|
||||
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
|
||||
are translated into `SubPattern` objects.
|
||||
are translated into `Ref` objects.
|
||||
|
||||
If an object has no layer it is set to this module's `DEFAULT_LAYER` ("DEFAULT").
|
||||
|
||||
|
|
@ -268,57 +268,57 @@ def _read_block(block, clean_vertices: bool) -> Tuple[str, Pattern]:
|
|||
b_vector=(0, attr['row_spacing']),
|
||||
a_count=attr['column_count'],
|
||||
b_count=attr['row_count'])
|
||||
pat.subpatterns.append(SubPattern(**args))
|
||||
pat.ref(**args)
|
||||
else:
|
||||
logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')
|
||||
return name, pat
|
||||
|
||||
|
||||
def _subpatterns_to_refs(
|
||||
def _mrefs_to_drefs(
|
||||
block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],
|
||||
subpatterns: List[SubPattern],
|
||||
refs: List[Ref],
|
||||
) -> None:
|
||||
for subpat in subpatterns:
|
||||
if subpat.target is None:
|
||||
for ref in refs:
|
||||
if ref.target is None:
|
||||
continue
|
||||
encoded_name = subpat.target
|
||||
encoded_name = ref.target
|
||||
|
||||
rotation = (subpat.rotation * 180 / numpy.pi) % 360
|
||||
rotation = (ref.rotation * 180 / numpy.pi) % 360
|
||||
attribs = {
|
||||
'xscale': subpat.scale * (-1 if subpat.mirrored[1] else 1),
|
||||
'yscale': subpat.scale * (-1 if subpat.mirrored[0] else 1),
|
||||
'xscale': ref.scale * (-1 if ref.mirrored[1] else 1),
|
||||
'yscale': ref.scale * (-1 if ref.mirrored[0] else 1),
|
||||
'rotation': rotation,
|
||||
}
|
||||
|
||||
rep = subpat.repetition
|
||||
rep = ref.repetition
|
||||
if rep is None:
|
||||
block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
|
||||
block.add_blockref(encoded_name, ref.offset, dxfattribs=attribs)
|
||||
elif isinstance(rep, Grid):
|
||||
a = rep.a_vector
|
||||
b = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||
rotated_a = rotation_matrix_2d(-subpat.rotation) @ a
|
||||
rotated_b = rotation_matrix_2d(-subpat.rotation) @ b
|
||||
rotated_a = rotation_matrix_2d(-ref.rotation) @ a
|
||||
rotated_b = rotation_matrix_2d(-ref.rotation) @ b
|
||||
if rotated_a[1] == 0 and rotated_b[0] == 0:
|
||||
attribs['column_count'] = rep.a_count
|
||||
attribs['row_count'] = rep.b_count
|
||||
attribs['column_spacing'] = rotated_a[0]
|
||||
attribs['row_spacing'] = rotated_b[1]
|
||||
block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
|
||||
block.add_blockref(encoded_name, ref.offset, dxfattribs=attribs)
|
||||
elif rotated_a[0] == 0 and rotated_b[1] == 0:
|
||||
attribs['column_count'] = rep.b_count
|
||||
attribs['row_count'] = rep.a_count
|
||||
attribs['column_spacing'] = rotated_b[0]
|
||||
attribs['row_spacing'] = rotated_a[1]
|
||||
block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
|
||||
block.add_blockref(encoded_name, ref.offset, dxfattribs=attribs)
|
||||
else:
|
||||
#NOTE: We could still do non-manhattan (but still orthogonal) grids by getting
|
||||
# creative with counter-rotated nested patterns, but probably not worth it.
|
||||
# Instead, just break appart the grid into individual elements:
|
||||
for dd in rep.displacements:
|
||||
block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs)
|
||||
block.add_blockref(encoded_name, ref.offset + dd, dxfattribs=attribs)
|
||||
else:
|
||||
for dd in rep.displacements:
|
||||
block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs)
|
||||
block.add_blockref(encoded_name, ref.offset + dd, dxfattribs=attribs)
|
||||
|
||||
|
||||
def _shapes_to_elements(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import klamath
|
|||
from klamath import records
|
||||
|
||||
from .utils import is_gzipped
|
||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
||||
from .. import Pattern, Ref, PatternError, Label, Shape
|
||||
from ..shapes import Polygon, Path
|
||||
from ..repetition import Grid
|
||||
from ..utils import layer_t, normalize_mirror, annotations_t
|
||||
|
|
@ -70,7 +70,7 @@ def write(
|
|||
"""
|
||||
Convert a library to a GDSII stream, mapping data as follows:
|
||||
Pattern -> GDSII structure
|
||||
SubPattern -> GDSII SREF or AREF
|
||||
Ref -> GDSII SREF or AREF
|
||||
Path -> GSDII path
|
||||
Shape (other than path) -> GDSII boundary/ies
|
||||
Label -> GDSII text
|
||||
|
|
@ -82,7 +82,7 @@ def write(
|
|||
datatype is chosen to be `shape.layer[1]` if available,
|
||||
otherwise `0`
|
||||
|
||||
It is often a good idea to run `pattern.subpatternize()` prior to calling this function,
|
||||
It is often a good idea to run `pattern.dedup()` prior to calling this function,
|
||||
especially if calling `.polygonize()` will result in very many vertices.
|
||||
|
||||
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
|
||||
|
|
@ -107,7 +107,7 @@ def write(
|
|||
# TODO check all hierarchy present
|
||||
|
||||
if not modify_originals:
|
||||
library = library.deepcopy() #TODO figure out best approach e.g. if lazy
|
||||
library = copy.deepcopy(library) #TODO figure out best approach e.g. if lazy
|
||||
|
||||
if not isinstance(library, MutableLibrary):
|
||||
if isinstance(library, dict):
|
||||
|
|
@ -130,7 +130,7 @@ def write(
|
|||
elements: List[klamath.elements.Element] = []
|
||||
elements += _shapes_to_elements(pat.shapes)
|
||||
elements += _labels_to_texts(pat.labels)
|
||||
elements += _subpatterns_to_refs(pat.subpatterns)
|
||||
elements += _mrefs_to_grefs(pat.refs)
|
||||
|
||||
klamath.library.write_struct(stream, name=name.encode('ASCII'), elements=elements)
|
||||
records.ENDLIB.write(stream, None)
|
||||
|
|
@ -196,7 +196,7 @@ def read(
|
|||
"""
|
||||
Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
|
||||
translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
|
||||
are translated into SubPattern objects.
|
||||
are translated into Ref objects.
|
||||
|
||||
Additional library info is returned in a dict, containing:
|
||||
'name': name of the library
|
||||
|
|
@ -273,7 +273,7 @@ def read_elements(
|
|||
)
|
||||
pat.labels.append(label)
|
||||
elif isinstance(element, klamath.elements.Reference):
|
||||
pat.subpatterns.append(_ref_to_subpat(element))
|
||||
pat.refs.append(_gref_to_mref(element))
|
||||
return pat
|
||||
|
||||
|
||||
|
|
@ -293,9 +293,9 @@ def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]:
|
|||
return layer, data_type
|
||||
|
||||
|
||||
def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
||||
def _gref_to_mref(ref: klamath.library.Reference) -> Ref:
|
||||
"""
|
||||
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.target to struct_name.
|
||||
Helper function to create a Ref from an SREF or AREF. Sets ref.target to struct_name.
|
||||
"""
|
||||
xy = ref.xy.astype(float)
|
||||
offset = xy[0]
|
||||
|
|
@ -307,7 +307,7 @@ 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(
|
||||
ref = Ref(
|
||||
target=ref.struct_name.decode('ASCII'),
|
||||
offset=offset,
|
||||
rotation=numpy.deg2rad(ref.angle_deg),
|
||||
|
|
@ -316,7 +316,7 @@ def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
|||
annotations=_properties_to_annotations(ref.properties),
|
||||
repetition=repetition,
|
||||
)
|
||||
return subpat
|
||||
return ref
|
||||
|
||||
|
||||
def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
||||
|
|
@ -349,45 +349,45 @@ def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) ->
|
|||
)
|
||||
|
||||
|
||||
def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.Reference]:
|
||||
def _mrefs_to_grefs(refs: List[Ref]) -> List[klamath.library.Reference]:
|
||||
refs = []
|
||||
for subpat in subpatterns:
|
||||
if subpat.target is None:
|
||||
for ref in refs:
|
||||
if ref.target is None:
|
||||
continue
|
||||
encoded_name = subpat.target.encode('ASCII')
|
||||
encoded_name = ref.target.encode('ASCII')
|
||||
|
||||
# Note: GDS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
||||
rep = subpat.repetition
|
||||
angle_deg = numpy.rad2deg(subpat.rotation + extra_angle) % 360
|
||||
properties = _annotations_to_properties(subpat.annotations, 512)
|
||||
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||
rep = ref.repetition
|
||||
angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||
properties = _annotations_to_properties(ref.annotations, 512)
|
||||
|
||||
if isinstance(rep, Grid):
|
||||
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||
b_count = rep.b_count if rep.b_count is not None else 1
|
||||
xy: NDArray[numpy.float64] = numpy.array(subpat.offset) + [
|
||||
[0, 0],
|
||||
xy = numpy.array(ref.offset) + numpy.array([
|
||||
[0.0, 0.0],
|
||||
rep.a_vector * rep.a_count,
|
||||
b_vector * b_count,
|
||||
]
|
||||
])
|
||||
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,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
refs.append(aref)
|
||||
elif rep is None:
|
||||
ref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([subpat.offset]),
|
||||
xy=rint_cast([ref.offset]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=subpat.scale,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
refs.append(ref)
|
||||
|
|
@ -395,11 +395,11 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.
|
|||
new_srefs = [
|
||||
klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([subpat.offset + dd]),
|
||||
xy=rint_cast([ref.offset + dd]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=subpat.scale,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
for dd in rep.displacements]
|
||||
|
|
@ -636,6 +636,7 @@ def load_libraryfile(
|
|||
Additional library info (dict, same format as from `read`).
|
||||
"""
|
||||
path = pathlib.Path(filename)
|
||||
stream: BinaryIO
|
||||
if is_gzipped(path):
|
||||
if mmap:
|
||||
logger.info('Asked to mmap a gzipped file, reading into memory instead...')
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
# FOr backwards compatibility
|
||||
from .gdsii import *
|
||||
|
|
@ -28,7 +28,7 @@ import fatamorgana.records as fatrec
|
|||
from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
|
||||
|
||||
from .utils import is_gzipped
|
||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
||||
from .. import Pattern, Ref, PatternError, Label, Shape
|
||||
from ..library import WrapLibrary, MutableLibrary
|
||||
from ..shapes import Polygon, Path, Circle
|
||||
from ..repetition import Grid, Arbitrary, Repetition
|
||||
|
|
@ -62,7 +62,7 @@ def build(
|
|||
) -> fatamorgana.OasisLayout:
|
||||
"""
|
||||
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
|
||||
as OASIS cells, refs 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).
|
||||
|
|
@ -148,7 +148,7 @@ def build(
|
|||
|
||||
structure.geometry += _shapes_to_elements(pat.shapes, layer2oas)
|
||||
structure.geometry += _labels_to_texts(pat.labels, layer2oas)
|
||||
structure.placements += _subpatterns_to_placements(pat.subpatterns)
|
||||
structure.placements += _refs_to_placements(pat.refs)
|
||||
|
||||
return lib
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ def read(
|
|||
"""
|
||||
Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are
|
||||
translated into Pattern objects; Polygons are translated into polygons, and Placements
|
||||
are translated into SubPattern objects.
|
||||
are translated into Ref objects.
|
||||
|
||||
Additional library info is returned in a dict, containing:
|
||||
'units_per_micrometer': number of database units per micrometer (all values are in database units)
|
||||
|
|
@ -456,7 +456,7 @@ def read(
|
|||
continue
|
||||
|
||||
for placement in cell.placements:
|
||||
pat.subpatterns.append(_placement_to_subpat(placement, lib))
|
||||
pat.refs.append(_placement_to_ref(placement, lib))
|
||||
|
||||
patterns_dict[cell_name] = pat
|
||||
|
||||
|
|
@ -480,9 +480,9 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
|
|||
return layer, data_type
|
||||
|
||||
|
||||
def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> SubPattern:
|
||||
def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> Ref:
|
||||
"""
|
||||
Helper function to create a SubPattern from a placment. Sets subpat.target to the placement name.
|
||||
Helper function to create a Ref from a placment. Sets ref.target to the placement name.
|
||||
"""
|
||||
assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition))
|
||||
xy = numpy.array((placement.x, placement.y))
|
||||
|
|
@ -494,7 +494,7 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo
|
|||
rotation = 0
|
||||
else:
|
||||
rotation = numpy.deg2rad(float(placement.angle))
|
||||
subpat = SubPattern(
|
||||
ref = Ref(
|
||||
target=name,
|
||||
offset=xy,
|
||||
mirrored=(placement.flip, False),
|
||||
|
|
@ -503,29 +503,29 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo
|
|||
repetition=repetition_fata2masq(placement.repetition),
|
||||
annotations=annotations,
|
||||
)
|
||||
return subpat
|
||||
return ref
|
||||
|
||||
|
||||
def _subpatterns_to_placements(
|
||||
subpatterns: List[SubPattern],
|
||||
def _refs_to_placements(
|
||||
refs: List[Ref],
|
||||
) -> List[fatrec.Placement]:
|
||||
refs = []
|
||||
for subpat in subpatterns:
|
||||
if subpat.target is None:
|
||||
for ref in refs:
|
||||
if ref.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)
|
||||
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||
frep, rep_offset = repetition_masq2fata(ref.repetition)
|
||||
|
||||
offset = rint_cast(subpat.offset + rep_offset)
|
||||
angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
|
||||
offset = rint_cast(ref.offset + rep_offset)
|
||||
angle = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||
ref = fatrec.Placement(
|
||||
name=subpat.target,
|
||||
name=ref.target,
|
||||
flip=mirror_across_x,
|
||||
angle=angle,
|
||||
magnification=subpat.scale,
|
||||
properties=annotations_to_properties(subpat.annotations),
|
||||
magnification=ref.scale,
|
||||
properties=annotations_to_properties(ref.annotations),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
repetition=frep,
|
||||
|
|
|
|||
|
|
@ -1,560 +0,0 @@
|
|||
"""
|
||||
GDSII file format readers and writers using python-gdsii
|
||||
|
||||
Note that GDSII references follow the same convention as `masque`,
|
||||
with this order of operations:
|
||||
1. Mirroring
|
||||
2. Rotation
|
||||
3. Scaling
|
||||
4. Offset and array expansion (no mirroring/rotation/scaling applied to offsets)
|
||||
|
||||
Scaling, rotation, and mirroring apply to individual instances, not grid
|
||||
vectors or offsets.
|
||||
|
||||
Notes:
|
||||
* absolute positioning is not supported
|
||||
* PLEX is not supported
|
||||
* ELFLAGS are not supported
|
||||
* GDS does not support library- or structure-level annotations
|
||||
"""
|
||||
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Optional
|
||||
from typing import Sequence, Mapping
|
||||
import re
|
||||
import io
|
||||
import copy
|
||||
import base64
|
||||
import struct
|
||||
import logging
|
||||
import pathlib
|
||||
import gzip
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
# python-gdsii
|
||||
import gdsii.library #type: ignore
|
||||
import gdsii.structure #type: ignore
|
||||
import gdsii.elements #type: ignore
|
||||
|
||||
from .utils import clean_pattern_vertices, is_gzipped
|
||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
||||
from ..shapes import Polygon, Path
|
||||
from ..repetition import Grid
|
||||
from ..utils import get_bit, set_bit, layer_t, normalize_mirror, annotations_t
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
path_cap_map = {
|
||||
None: Path.Cap.Flush,
|
||||
0: Path.Cap.Flush,
|
||||
1: Path.Cap.Circle,
|
||||
2: Path.Cap.Square,
|
||||
4: Path.Cap.SquareCustom,
|
||||
}
|
||||
|
||||
|
||||
def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
||||
return numpy.rint(val, dtype=numpy.int32, casting='unsafe')
|
||||
|
||||
|
||||
def build(
|
||||
library: Mapping[str, Pattern],
|
||||
meters_per_unit: float,
|
||||
logical_units_per_unit: float = 1,
|
||||
library_name: str = 'masque-gdsii-write',
|
||||
*,
|
||||
modify_originals: bool = False,
|
||||
) -> gdsii.library.Library:
|
||||
"""
|
||||
Convert a `Pattern` or list of patterns to a GDSII stream, by first calling
|
||||
`.polygonize()` to change the shapes into polygons, and then writing patterns
|
||||
as GDSII structures, polygons as boundary elements, and subpatterns as structure
|
||||
references (sref).
|
||||
|
||||
For each shape,
|
||||
layer is chosen to be equal to `shape.layer` if it is an int,
|
||||
or `shape.layer[0]` if it is a tuple
|
||||
datatype is chosen to be `shape.layer[1]` if available,
|
||||
otherwise `0`
|
||||
|
||||
It is often a good idea to run `pattern.subpatternize()` prior to calling this function,
|
||||
especially if calling `.polygonize()` will result in very many vertices.
|
||||
|
||||
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
|
||||
prior to calling this function.
|
||||
|
||||
Args:
|
||||
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
|
||||
"logical" unit which is different from the "database" unit, for display purposes.
|
||||
Default `1`.
|
||||
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.
|
||||
Default `False`.
|
||||
|
||||
Returns:
|
||||
`gdsii.library.Library`
|
||||
"""
|
||||
# TODO check name errors
|
||||
bad_keys = check_valid_names(library.keys())
|
||||
|
||||
# TODO check all hierarchy present
|
||||
|
||||
|
||||
if not modify_originals:
|
||||
library = library.deepcopy() #TODO figure out best approach e.g. if lazy
|
||||
|
||||
library.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,
|
||||
name=library_name.encode('ASCII'),
|
||||
logical_unit=logical_units_per_unit,
|
||||
physical_unit=meters_per_unit)
|
||||
|
||||
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
||||
for name, pat in renamed_lib.items():
|
||||
structure = gdsii.structure.Structure(name=name.encode('ASCII'))
|
||||
lib.append(structure)
|
||||
|
||||
structure += _shapes_to_elements(pat.shapes)
|
||||
structure += _labels_to_texts(pat.labels)
|
||||
structure += _subpatterns_to_refs(pat.subpatterns)
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
def write(
|
||||
library: Mapping[str, Pattern],
|
||||
stream: io.BufferedIOBase,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
Write a `Pattern` or list of patterns to a GDSII file.
|
||||
See `masque.file.gdsii.build()` for details.
|
||||
|
||||
Args:
|
||||
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(library, *args, **kwargs)
|
||||
lib.save(stream)
|
||||
return
|
||||
|
||||
def writefile(
|
||||
library: Mapping[str, Pattern],
|
||||
filename: Union[str, pathlib.Path],
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
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:
|
||||
library: {name: Pattern} pairs to save.
|
||||
filename: Filename to save to.
|
||||
*args: passed to `write()`
|
||||
**kwargs: passed to `write()`
|
||||
"""
|
||||
path = pathlib.Path(filename)
|
||||
if path.suffix == '.gz':
|
||||
open_func: Callable = gzip.open
|
||||
else:
|
||||
open_func = open
|
||||
|
||||
with io.BufferedWriter(open_func(path, mode='wb')) as stream:
|
||||
write(library, stream, *args, **kwargs)
|
||||
|
||||
|
||||
def readfile(
|
||||
filename: Union[str, pathlib.Path],
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||
"""
|
||||
Wrapper for `read()` that takes a filename or path instead of a stream.
|
||||
|
||||
Will automatically decompress gzipped files.
|
||||
|
||||
Args:
|
||||
filename: Filename to save to.
|
||||
*args: passed to `read()`
|
||||
**kwargs: passed to `read()`
|
||||
"""
|
||||
path = pathlib.Path(filename)
|
||||
if is_gzipped(path):
|
||||
open_func: Callable = gzip.open
|
||||
else:
|
||||
open_func = open
|
||||
|
||||
with io.BufferedReader(open_func(path, mode='rb')) as stream:
|
||||
results = read(stream, *args, **kwargs)
|
||||
return results
|
||||
|
||||
|
||||
def read(
|
||||
stream: io.BufferedIOBase,
|
||||
clean_vertices: bool = True,
|
||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||
"""
|
||||
Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
|
||||
translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
|
||||
are translated into SubPattern objects.
|
||||
|
||||
Additional library info is returned in a dict, containing:
|
||||
'name': name of the library
|
||||
'meters_per_unit': number of meters per database unit (all values are in database units)
|
||||
'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns)
|
||||
per database unit
|
||||
|
||||
Args:
|
||||
stream: Stream to read from.
|
||||
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:Patterns generated from GDSII structures
|
||||
- Dict of GDSII library info
|
||||
"""
|
||||
|
||||
lib = gdsii.library.Library.load(stream)
|
||||
|
||||
library_info = {'name': lib.name.decode('ASCII'),
|
||||
'meters_per_unit': lib.physical_unit,
|
||||
'logical_units_per_unit': lib.logical_unit,
|
||||
}
|
||||
|
||||
raw_mode = True # Whether to construct shapes in raw mode (less error checking)
|
||||
|
||||
patterns_dict = {}
|
||||
for structure in lib:
|
||||
pat = Pattern()
|
||||
name = structure.name.decode('ASCII')
|
||||
for element in structure:
|
||||
# Switch based on element type:
|
||||
if isinstance(element, gdsii.elements.Boundary):
|
||||
poly = _boundary_to_polygon(element, raw_mode)
|
||||
pat.shapes.append(poly)
|
||||
|
||||
if isinstance(element, gdsii.elements.Path):
|
||||
path = _gpath_to_mpath(element, raw_mode)
|
||||
pat.shapes.append(path)
|
||||
|
||||
elif isinstance(element, gdsii.elements.Text):
|
||||
label = Label(
|
||||
offset=element.xy.astype(float),
|
||||
layer=(element.layer, element.text_type),
|
||||
string=element.string.decode('ASCII'),
|
||||
)
|
||||
pat.labels.append(label)
|
||||
|
||||
elif isinstance(element, (gdsii.elements.SRef, gdsii.elements.ARef)):
|
||||
pat.subpatterns.append(_ref_to_subpat(element))
|
||||
|
||||
if clean_vertices:
|
||||
clean_pattern_vertices(pat)
|
||||
patterns_dict[name] = pat
|
||||
|
||||
return patterns_dict, library_info
|
||||
|
||||
|
||||
def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]:
|
||||
""" Helper to turn a layer tuple-or-int into a layer and datatype"""
|
||||
if isinstance(mlayer, int):
|
||||
layer = mlayer
|
||||
data_type = 0
|
||||
elif isinstance(mlayer, tuple):
|
||||
layer = mlayer[0]
|
||||
if len(mlayer) > 1:
|
||||
data_type = mlayer[1]
|
||||
else:
|
||||
data_type = 0
|
||||
else:
|
||||
raise PatternError(f'Invalid layer for gdsii: {mlayer}. Note that gdsii layers cannot be strings.')
|
||||
return layer, data_type
|
||||
|
||||
|
||||
def _ref_to_subpat(
|
||||
element: Union[gdsii.elements.SRef,
|
||||
gdsii.elements.ARef]
|
||||
) -> SubPattern:
|
||||
"""
|
||||
Helper function to create a SubPattern from an SREF or AREF. Sets `subpat.target` to `element.struct_name`.
|
||||
|
||||
NOTE: "Absolute" means not affected by parent elements.
|
||||
That's not currently supported by masque at all (and not planned).
|
||||
"""
|
||||
rotation = 0.0
|
||||
offset = numpy.array(element.xy[0], dtype=float)
|
||||
scale = 1.0
|
||||
mirror_across_x = False
|
||||
repetition = None
|
||||
|
||||
if element.strans is not None:
|
||||
if element.mag is not None:
|
||||
scale = element.mag
|
||||
# Bit 13 means absolute scale
|
||||
if get_bit(element.strans, 15 - 13):
|
||||
raise PatternError('Absolute scale is not implemented in masque!')
|
||||
if element.angle is not None:
|
||||
rotation = numpy.deg2rad(element.angle)
|
||||
# Bit 14 means absolute rotation
|
||||
if get_bit(element.strans, 15 - 14):
|
||||
raise PatternError('Absolute rotation is not implemented in masque!')
|
||||
# Bit 0 means mirror x-axis
|
||||
if get_bit(element.strans, 15 - 0):
|
||||
mirror_across_x = True
|
||||
|
||||
if isinstance(element, gdsii.elements.ARef):
|
||||
a_count = element.cols
|
||||
b_count = element.rows
|
||||
a_vector = (element.xy[1] - offset) / a_count
|
||||
b_vector = (element.xy[2] - offset) / b_count
|
||||
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
|
||||
a_count=a_count, b_count=b_count)
|
||||
|
||||
subpat = SubPattern(
|
||||
target=element.struct_name,
|
||||
offset=offset,
|
||||
rotation=rotation,
|
||||
scale=scale,
|
||||
mirrored=(mirror_across_x, False),
|
||||
annotations=_properties_to_annotations(element.properties),
|
||||
repetition=repetition,
|
||||
)
|
||||
return subpat
|
||||
|
||||
|
||||
def _gpath_to_mpath(element: gdsii.elements.Path, raw_mode: bool) -> Path:
|
||||
if element.path_type in path_cap_map:
|
||||
cap = path_cap_map[element.path_type]
|
||||
else:
|
||||
raise PatternError(f'Unrecognized path type: {element.path_type}')
|
||||
|
||||
args = {
|
||||
'vertices': element.xy.astype(float),
|
||||
'layer': (element.layer, element.data_type),
|
||||
'width': element.width if element.width is not None else 0.0,
|
||||
'cap': cap,
|
||||
'offset': numpy.zeros(2),
|
||||
'annotations': _properties_to_annotations(element.properties),
|
||||
'raw': raw_mode,
|
||||
}
|
||||
|
||||
if cap == Path.Cap.SquareCustom:
|
||||
args['cap_extensions'] = numpy.zeros(2)
|
||||
if element.bgn_extn is not None:
|
||||
args['cap_extensions'][0] = element.bgn_extn
|
||||
if element.end_extn is not None:
|
||||
args['cap_extensions'][1] = element.end_extn
|
||||
|
||||
return Path(**args)
|
||||
|
||||
|
||||
def _boundary_to_polygon(element: gdsii.elements.Boundary, raw_mode: bool) -> Polygon:
|
||||
args = {'vertices': element.xy[:-1].astype(float),
|
||||
'layer': (element.layer, element.data_type),
|
||||
'offset': numpy.zeros(2),
|
||||
'annotations': _properties_to_annotations(element.properties),
|
||||
'raw': raw_mode,
|
||||
}
|
||||
return Polygon(**args)
|
||||
|
||||
|
||||
def _subpatterns_to_refs(
|
||||
subpatterns: List[SubPattern],
|
||||
) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]:
|
||||
refs = []
|
||||
for subpat in subpatterns:
|
||||
if subpat.target is None:
|
||||
continue
|
||||
encoded_name = subpat.target.encode('ASCII')
|
||||
|
||||
# Note: GDS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
||||
rep = subpat.repetition
|
||||
|
||||
new_refs: List[Union[gdsii.elements.SRef, gdsii.elements.ARef]]
|
||||
ref: Union[gdsii.elements.SRef, gdsii.elements.ARef]
|
||||
if isinstance(rep, Grid):
|
||||
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||
b_count = rep.b_count if rep.b_count is not None else 1
|
||||
xy: NDArray[numpy.float64] = numpy.array(subpat.offset) + [
|
||||
[0, 0],
|
||||
rep.a_vector * rep.a_count,
|
||||
b_vector * b_count,
|
||||
]
|
||||
ref = gdsii.elements.ARef(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast(xy),
|
||||
cols=rint_cast(rep.a_count),
|
||||
rows=rint_cast(rep.b_count),
|
||||
)
|
||||
new_refs = [ref]
|
||||
elif rep is None:
|
||||
ref = gdsii.elements.SRef(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([subpat.offset]),
|
||||
)
|
||||
new_refs = [ref]
|
||||
else:
|
||||
new_refs = [gdsii.elements.SRef(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([subpat.offset + dd]),
|
||||
)
|
||||
for dd in rep.displacements]
|
||||
|
||||
for ref in new_refs:
|
||||
ref.angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
|
||||
# strans must be non-None for angle and mag to take effect
|
||||
ref.strans = set_bit(0, 15 - 0, mirror_across_x)
|
||||
ref.mag = subpat.scale
|
||||
ref.properties = _annotations_to_properties(subpat.annotations, 512)
|
||||
|
||||
refs += new_refs
|
||||
return refs
|
||||
|
||||
|
||||
def _properties_to_annotations(properties: List[Tuple[int, bytes]]) -> annotations_t:
|
||||
return {str(k): [v.decode()] for k, v in properties}
|
||||
|
||||
|
||||
def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> List[Tuple[int, bytes]]:
|
||||
cum_len = 0
|
||||
props = []
|
||||
for key, vals in annotations.items():
|
||||
try:
|
||||
i = int(key)
|
||||
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])')
|
||||
|
||||
val_strings = ' '.join(str(val) for val in vals)
|
||||
b = val_strings.encode()
|
||||
if len(b) > 126:
|
||||
raise PatternError(f'Annotation value {b!r} is longer than 126 characters!')
|
||||
cum_len += numpy.ceil(len(b) / 2) * 2 + 2
|
||||
if cum_len > max_len:
|
||||
raise PatternError(f'Sum of annotation data will be longer than {max_len} bytes! Generated bytes were {b!r}')
|
||||
props.append((i, b))
|
||||
return props
|
||||
|
||||
|
||||
def _shapes_to_elements(
|
||||
shapes: List[Shape],
|
||||
polygonize_paths: bool = False,
|
||||
) -> List[Union[gdsii.elements.Boundary, gdsii.elements.Path]]:
|
||||
elements: List[Union[gdsii.elements.Boundary, gdsii.elements.Path]] = []
|
||||
# Add a Boundary element for each shape, and Path elements if necessary
|
||||
for shape in shapes:
|
||||
layer, data_type = _mlayer2gds(shape.layer)
|
||||
properties = _annotations_to_properties(shape.annotations, 128)
|
||||
if isinstance(shape, Path) and not polygonize_paths:
|
||||
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
|
||||
path = gdsii.elements.Path(layer=layer,
|
||||
data_type=data_type,
|
||||
xy=xy)
|
||||
path.path_type = path_type
|
||||
path.width = width
|
||||
path.properties = properties
|
||||
elements.append(path)
|
||||
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 = gdsii.elements.Boundary(
|
||||
layer=layer,
|
||||
data_type=data_type,
|
||||
xy=xy_closed,
|
||||
)
|
||||
boundary.properties = properties
|
||||
elements.append(boundary)
|
||||
return elements
|
||||
|
||||
|
||||
def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
|
||||
texts = []
|
||||
for label in labels:
|
||||
properties = _annotations_to_properties(label.annotations, 128)
|
||||
layer, text_type = _mlayer2gds(label.layer)
|
||||
xy = rint_cast([label.offset])
|
||||
text = gdsii.elements.Text(
|
||||
layer=layer,
|
||||
text_type=text_type,
|
||||
xy=xy,
|
||||
string=label.string.encode('ASCII'),
|
||||
)
|
||||
text.properties = properties
|
||||
texts.append(text)
|
||||
return texts
|
||||
|
||||
|
||||
def disambiguate_pattern_names(
|
||||
names: Iterable[str],
|
||||
max_name_length: int = 32,
|
||||
suffix_length: int = 6,
|
||||
) -> 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:
|
||||
# Shorten names which already exceed max-length
|
||||
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 = name
|
||||
|
||||
# Remove invalid characters
|
||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
|
||||
|
||||
# Add a suffix that makes the name unique
|
||||
i = 0
|
||||
suffixed_name = sanitized_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')
|
||||
i += 1
|
||||
|
||||
if sanitized_name == '':
|
||||
logger.warning(f'Empty pattern name saved as "{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 "{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 "{name}"')
|
||||
|
||||
new_names.append(suffixed_name)
|
||||
return new_names
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ def writefile(
|
|||
"""
|
||||
Write a Pattern to an SVG file, by first calling .polygonize() on it
|
||||
to change the shapes into polygons, and then writing patterns as SVG
|
||||
groups (<g>, inside <defs>), polygons as paths (<path>), and subpatterns
|
||||
groups (<g>, inside <defs>), polygons as paths (<path>), and refs
|
||||
as <use> elements.
|
||||
|
||||
Note that this function modifies the Pattern.
|
||||
|
|
@ -29,7 +29,7 @@ def writefile(
|
|||
If `custom_attributes` is `True`, a non-standard `pattern_layer` attribute
|
||||
is written to the relevant elements.
|
||||
|
||||
It is often a good idea to run `pattern.subpatternize()` on pattern prior to
|
||||
It is often a good idea to run `pattern.dedup()` on pattern prior to
|
||||
calling this function, especially if calling `.polygonize()` will result in very
|
||||
many vertices.
|
||||
|
||||
|
|
@ -75,11 +75,11 @@ def writefile(
|
|||
|
||||
svg_group.add(path)
|
||||
|
||||
for subpat in pat.subpatterns:
|
||||
if subpat.target is None:
|
||||
for ref in pat.refs:
|
||||
if ref.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.target), transform=transform)
|
||||
transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})'
|
||||
use = svg.use(href='#' + mangle_name(ref.target), transform=transform)
|
||||
svg_group.add(use)
|
||||
|
||||
svg.defs.add(svg_group)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue