Add basic support for OASIS and update setup/docs for OASIS and DXF support
This commit is contained in:
parent
6e957d761a
commit
f204d917c9
@ -13,17 +13,19 @@ E-beam doses, and the ability to output to multiple formats.
|
||||
## Installation
|
||||
|
||||
Requirements:
|
||||
* python >= 3.5 (written and tested with 3.6)
|
||||
* python >= 3.7 (written and tested with 3.8)
|
||||
* numpy
|
||||
* matplotlib (optional, used for `visualization` functions and `text`)
|
||||
* python-gdsii (optional, used for `gdsii` i/o)
|
||||
* ezdxf (optional, used for `dxf` i/o)
|
||||
* fatamorgana (optional, used for `oasis` i/o)
|
||||
* svgwrite (optional, used for `svg` output)
|
||||
* freetype (optional, used for `text`)
|
||||
|
||||
|
||||
Install with pip:
|
||||
```bash
|
||||
pip3 install 'masque[visualization,gdsii,svg,text]'
|
||||
pip3 install 'masque[visualization,gdsii,oasis,dxf,svg,text]'
|
||||
```
|
||||
|
||||
Alternatively, install from git
|
||||
@ -36,4 +38,5 @@ pip3 install git+https://mpxd.net/code/jan/masque.git@release
|
||||
* Polygon de-embedding
|
||||
* Construct from bitmap
|
||||
* Boolean operations on polygons (using pyclipper)
|
||||
* Output to OASIS (using fatamorgana)
|
||||
* Implement shape/cell properties
|
||||
* Implement OASIS-style repetitions for shapes
|
||||
|
612
masque/file/oasis.py
Normal file
612
masque/file/oasis.py
Normal file
@ -0,0 +1,612 @@
|
||||
"""
|
||||
OASIS file format readers and writers
|
||||
|
||||
Note that OASIS 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.
|
||||
"""
|
||||
from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional
|
||||
import re
|
||||
import io
|
||||
import copy
|
||||
import base64
|
||||
import struct
|
||||
import logging
|
||||
import pathlib
|
||||
import gzip
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
||||
import fatamorgana
|
||||
import fatamorgana.records as fatrec
|
||||
from fatamorgana.basic import PathExtensionScheme
|
||||
|
||||
from .utils import mangle_name, make_dose_table
|
||||
from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t
|
||||
from ..shapes import Polygon, Path, Circle
|
||||
from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
|
||||
from ..utils import remove_colinear_vertices, normalize_mirror
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
logger.warning('OASIS support is experimental and mostly untested!')
|
||||
|
||||
|
||||
path_cap_map = {
|
||||
PathExtensionScheme.Flush: Path.Cap.Flush,
|
||||
PathExtensionScheme.HalfWidth: Path.Cap.Square,
|
||||
PathExtensionScheme.Arbitrary: Path.Cap.SquareCustom,
|
||||
}
|
||||
|
||||
#TODO implement properties
|
||||
#TODO implement more shape types?
|
||||
|
||||
def build(patterns: Union[Pattern, List[Pattern]],
|
||||
units_per_micron: int,
|
||||
layer_map: Dict[str, Union[int, Tuple[int, int]]] = None,
|
||||
modify_originals: bool = False,
|
||||
disambiguate_func: Callable[[Iterable[Pattern]], None] = 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).
|
||||
Other shape types may be converted to polygons if no equivalent
|
||||
record type exists (or is not implemented here yet).
|
||||
|
||||
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`
|
||||
If a layer map is provided, layer strings will be converted
|
||||
automatically, and layer names will be written to the file.
|
||||
|
||||
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
|
||||
prior to calling this function.
|
||||
|
||||
Args:
|
||||
patterns: A Pattern or list of patterns to convert.
|
||||
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
|
||||
provided, input shapes and labels are allowed to have layer names instead of numbers.
|
||||
It is assumed that geometry and text share the same layer names, and each name is
|
||||
assigned only to a single layer (not a range).
|
||||
If more fine-grained control is needed, manually pre-processing shapes' layer names
|
||||
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`.
|
||||
|
||||
Returns:
|
||||
`fatamorgana.OasisLayout`
|
||||
"""
|
||||
if isinstance(patterns, Pattern):
|
||||
patterns = [patterns]
|
||||
|
||||
if layer_map is None:
|
||||
layer_map = {}
|
||||
|
||||
if disambiguate_func is None:
|
||||
disambiguate_func = disambiguate_pattern_names
|
||||
|
||||
if not modify_originals:
|
||||
patterns = [p.deepunlock() for p in copy.deepcopy(patterns)]
|
||||
|
||||
# Create library
|
||||
lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None)
|
||||
|
||||
if layer_map:
|
||||
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)
|
||||
for tt in (True, False)]
|
||||
|
||||
def layer2oas(mlayer: layer_t) -> Tuple[int, int]:
|
||||
layer_num = layer_map[mlayer] if isinstance(mlayer, str) else mlayer
|
||||
return _mlayer2oas(layer_num)
|
||||
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())
|
||||
|
||||
# Now create a structure for each pattern
|
||||
for pat in patterns_by_id.values():
|
||||
structure = fatamorgana.Cell(name=pat.name)
|
||||
lib.cells.append(structure)
|
||||
|
||||
structure.geometry += _shapes_to_elements(pat.shapes, layer2oas)
|
||||
structure.geometry += _labels_to_texts(pat.labels, layer2oas)
|
||||
structure.placements += _subpatterns_to_refs(pat.subpatterns)
|
||||
|
||||
return lib
|
||||
|
||||
|
||||
def write(patterns: Union[List[Pattern], Pattern],
|
||||
stream: io.BufferedIOBase,
|
||||
*args,
|
||||
**kwargs):
|
||||
"""
|
||||
Write a `Pattern` or list of patterns to a OASIS file. See `oasis.build()`
|
||||
for details.
|
||||
|
||||
Args:
|
||||
patterns: A Pattern or list of patterns to write to file.
|
||||
stream: Stream to write to.
|
||||
*args: passed to `oasis.build()`
|
||||
**kwargs: passed to `oasis.build()`
|
||||
"""
|
||||
lib = build(patterns, *args, **kwargs)
|
||||
lib.write(stream)
|
||||
|
||||
|
||||
def writefile(patterns: Union[List[Pattern], Pattern],
|
||||
filename: Union[str, pathlib.Path],
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Wrapper for `oasis.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 `oasis.write`
|
||||
**kwargs: passed to `oasis.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:
|
||||
results = write(patterns, stream, *args, **kwargs)
|
||||
return results
|
||||
|
||||
|
||||
def readfile(filename: Union[str, pathlib.Path],
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
||||
"""
|
||||
Wrapper for `oasis.read()` that takes a filename or path instead of a stream.
|
||||
|
||||
Will automatically decompress files with a .gz suffix.
|
||||
|
||||
Args:
|
||||
filename: Filename to save to.
|
||||
*args: passed to `oasis.read`
|
||||
**kwargs: passed to `oasis.read`
|
||||
"""
|
||||
path = pathlib.Path(filename)
|
||||
if path.suffix == '.gz':
|
||||
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 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 or GridRepetition 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)
|
||||
|
||||
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
|
||||
- Dict of OASIS library info
|
||||
"""
|
||||
|
||||
lib = fatamorgana.OasisLayout.read(stream)
|
||||
|
||||
library_info: Dict[str, Any] = {
|
||||
'units_per_micrometer': lib.unit,
|
||||
}
|
||||
|
||||
layer_map = {}
|
||||
for layer_name in lib.layers:
|
||||
layer_map[str(layer_name.nstring)] = layer_name
|
||||
library_info['layer_map'] = layer_map
|
||||
|
||||
patterns = []
|
||||
for cell in lib.cells:
|
||||
if isinstance(cell.name, int):
|
||||
cell_name = lib.cellnames[cell.name].string
|
||||
else:
|
||||
cell_name = cell.name.string
|
||||
|
||||
pat = Pattern(name=cell_name)
|
||||
for element in cell.geometry:
|
||||
if isinstance(element, fatrec.XElement):
|
||||
logger.warning('Skipping XElement record')
|
||||
continue
|
||||
|
||||
if element.repetition is not None:
|
||||
# note XELEMENT has no repetition
|
||||
raise PatternError('masque OASIS reader does not implement repetitions for shapes yet')
|
||||
|
||||
# Switch based on element type:
|
||||
if isinstance(element, fatrec.Polygon):
|
||||
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0)
|
||||
poly = Polygon(vertices=vertices,
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy())
|
||||
|
||||
if clean_vertices:
|
||||
try:
|
||||
poly.clean_vertices()
|
||||
except PatternError:
|
||||
continue
|
||||
|
||||
pat.shapes.append(poly)
|
||||
|
||||
elif isinstance(element, fatrec.Path):
|
||||
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0)
|
||||
|
||||
cap_start = path_cap_map[element.get_extension_start()[0]]
|
||||
cap_end = path_cap_map[element.get_extension_end()[0]]
|
||||
if cap_start != cap_end:
|
||||
raise Exception('masque does not support multiple cap types on a single path.') #TODO handle multiple cap types
|
||||
cap = cap_start
|
||||
|
||||
path_args: Dict[str, Any] = {}
|
||||
if cap == Path.Cap.SquareCustom:
|
||||
path_args['cap_extensions'] = numpy.array((element.get_extension_start()[1],
|
||||
element.get_extension_end()[1]))
|
||||
path = Path(vertices=vertices,
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
width=element.get_half_width() * 2,
|
||||
cap=cap,
|
||||
**path_args)
|
||||
|
||||
if clean_vertices:
|
||||
try:
|
||||
path.clean_vertices()
|
||||
except PatternError as err:
|
||||
continue
|
||||
|
||||
pat.shapes.append(path)
|
||||
|
||||
elif isinstance(element, fatrec.Rectangle):
|
||||
width = element.get_width()
|
||||
height = element.get_height()
|
||||
rect = Polygon(layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height),
|
||||
)
|
||||
pat.shapes.append(rect)
|
||||
|
||||
elif isinstance(element, fatrec.Trapezoid):
|
||||
vertices = numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (element.get_width(), element.get_height())
|
||||
a = element.get_delta_a()
|
||||
b = element.get_delta_b()
|
||||
if element.get_is_vertical():
|
||||
if a > 0:
|
||||
vertices[0, 1] += a
|
||||
else:
|
||||
vertices[3, 1] += a
|
||||
|
||||
if b > 0:
|
||||
vertices[2, 1] -= b
|
||||
else:
|
||||
vertices[1, 1] -= b
|
||||
else:
|
||||
if a > 0:
|
||||
vertices[1, 0] += a
|
||||
else:
|
||||
vertices[0, 0] += a
|
||||
|
||||
if b > 0:
|
||||
vertices[3, 0] -= b
|
||||
else:
|
||||
vertices[2, 0] -= b
|
||||
|
||||
trapz = Polygon(layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
vertices=vertices,
|
||||
)
|
||||
pat.shapes.append(trapz)
|
||||
|
||||
elif isinstance(element, fatrec.CTrapezoid):
|
||||
cttype = element.get_ctrapezoid_type()
|
||||
height = element.get_height()
|
||||
width = element.get_width()
|
||||
|
||||
vertices = numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height)
|
||||
|
||||
if cttype in (0, 4, 7):
|
||||
vertices[2, 0] -= height
|
||||
if cttype in (1, 5, 6):
|
||||
vertices[3, 0] -= height
|
||||
if cttype in (2, 4, 6):
|
||||
vertices[1, 0] += height
|
||||
if cttype in (3, 5, 7):
|
||||
vertices[0, 0] += height
|
||||
|
||||
if cttype in (8, 12, 15):
|
||||
vertices[2, 0] -= width
|
||||
if cttype in (9, 13, 14):
|
||||
vertices[1, 0] -= width
|
||||
if cttype in (10, 12, 14):
|
||||
vertices[3, 0] += width
|
||||
if cttype in (11, 13, 15):
|
||||
vertices[0, 0] += width
|
||||
|
||||
if cttype == 16:
|
||||
vertices = vertices[[0, 1, 3], :]
|
||||
elif cttype == 17:
|
||||
vertices = vertices[[0, 1, 2], :]
|
||||
elif cttype == 18:
|
||||
vertices = vertices[[0, 2, 3], :]
|
||||
elif cttype == 19:
|
||||
vertices = vertices[[1, 2, 3], :]
|
||||
elif cttype == 20:
|
||||
vertices = vertices[[0, 1, 3], :]
|
||||
vertices[1, 0] += height
|
||||
elif cttype == 21:
|
||||
vertices = vertices[[0, 1, 2], :]
|
||||
vertices[0, 0] += height
|
||||
elif cttype == 22:
|
||||
vertices = vertices[[0, 1, 3], :]
|
||||
vertices[3, 1] += width
|
||||
elif cttype == 23:
|
||||
vertices = vertices[[0, 2, 3], :]
|
||||
vertices[0, 1] += width
|
||||
|
||||
ctrapz = Polygon(layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
vertices=vertices,
|
||||
)
|
||||
pat.shapes.append(ctrapz)
|
||||
|
||||
elif isinstance(element, fatrec.Circle):
|
||||
circle = Circle(layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
radius=float(element.get_radius()))
|
||||
pat.shapes.append(circle)
|
||||
|
||||
elif isinstance(element, fatrec.Text):
|
||||
label = Label(layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
string=str(element.get_string()))
|
||||
pat.labels.append(label)
|
||||
|
||||
else:
|
||||
logger.warning(f'Skipping record {element} (unimplemented)')
|
||||
continue
|
||||
|
||||
for placement in cell.placements:
|
||||
pat.subpatterns += _placement_to_subpats(placement)
|
||||
|
||||
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].string
|
||||
sp.pattern = patterns_dict[name]
|
||||
del sp.identifier
|
||||
|
||||
return patterns_dict, library_info
|
||||
|
||||
|
||||
def _mlayer2oas(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 OASIS: {layer}. Note that OASIS layers cannot be '
|
||||
'strings unless a layer map is provided.')
|
||||
return layer, data_type
|
||||
|
||||
|
||||
def _placement_to_subpats(placement: fatrec.Placement) -> List[subpattern_t]:
|
||||
"""
|
||||
Helper function to create a SubPattern from a placment. Sets subpat.pattern to None
|
||||
and sets the instance .identifier to (struct_name,).
|
||||
"""
|
||||
xy = numpy.array((placement.x, placement.y))
|
||||
mag = placement.magnification if placement.magnification is not None else 1
|
||||
pname = placement.get_name()
|
||||
name = pname if isinstance(pname, int) else pname.string
|
||||
args: Dict[str, Any] = {
|
||||
'pattern': None,
|
||||
'mirrored': (placement.flip, False),
|
||||
'rotation': float(placement.angle * pi/180),
|
||||
'scale': mag,
|
||||
'identifier': (name,),
|
||||
}
|
||||
|
||||
subpats: List[subpattern_t]
|
||||
rep = placement.repetition
|
||||
if isinstance(rep, fatamorgana.GridRepetition):
|
||||
subpat = GridRepetition(a_vector=rep.a_vector,
|
||||
b_vector=rep.b_vector,
|
||||
a_count=rep.a_count,
|
||||
b_count=rep.b_count,
|
||||
offset=xy,
|
||||
**args)
|
||||
subpats = [subpat]
|
||||
elif isinstance(rep, fatamorgana.ArbitraryRepetition):
|
||||
subpats = []
|
||||
for rep_offset in numpy.cumsum(numpy.column_stack((rep.x_displacements,
|
||||
rep.y_displacements))):
|
||||
subpats.append(SubPattern(offset=xy + rep_offset, **args))
|
||||
elif rep is None:
|
||||
subpats = [SubPattern(offset=xy, **args)]
|
||||
return subpats
|
||||
|
||||
|
||||
def _subpatterns_to_refs(subpatterns: List[subpattern_t]
|
||||
) -> List[fatrec.Placement]:
|
||||
refs = []
|
||||
for subpat in subpatterns:
|
||||
if subpat.pattern is None:
|
||||
continue
|
||||
|
||||
# Note: OASIS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
||||
xy = numpy.round(subpat.offset).astype(int)
|
||||
args: Dict[str, Any] = {
|
||||
'x': xy[0],
|
||||
'y': xy[1],
|
||||
}
|
||||
|
||||
if isinstance(subpat, GridRepetition):
|
||||
args['repetition'] = fatamorgana.GridRepetition(
|
||||
a_vector=numpy.round(subpat.a_vector).astype(int),
|
||||
b_vector=numpy.round(subpat.b_vector).astype(int),
|
||||
a_count=numpy.round(subpat.a_count).astype(int),
|
||||
b_count=numpy.round(subpat.b_count).astype(int))
|
||||
|
||||
angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360
|
||||
ref = fatrec.Placement(
|
||||
name=subpat.pattern.name,
|
||||
flip=mirror_across_x,
|
||||
angle=angle,
|
||||
magnification=subpat.scale,
|
||||
**args)
|
||||
|
||||
refs.append(ref)
|
||||
return refs
|
||||
|
||||
|
||||
def _shapes_to_elements(shapes: List[Shape],
|
||||
layer2oas: Callable[[layer_t], Tuple[int, int]],
|
||||
) -> List[Union[fatrec.Polygon, fatrec.Path, fatrec.Circle]]:
|
||||
# Add a Polygon record for each shape, and Path elements if necessary
|
||||
elements: List[Union[fatrec.Polygon, fatrec.Path, fatrec.Circle]] = []
|
||||
for shape in shapes:
|
||||
layer, datatype = layer2oas(shape.layer)
|
||||
if isinstance(shape, Circle):
|
||||
offset = numpy.round(shape.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])
|
||||
elements.append(circle)
|
||||
elif isinstance(shape, Path):
|
||||
xy = numpy.round(shape.offset + shape.vertices[0]).astype(int)
|
||||
deltas = numpy.round(numpy.diff(shape.vertices, axis=0)).astype(int)
|
||||
half_width = numpy.round(shape.width / 2).astype(int)
|
||||
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,
|
||||
)
|
||||
elements.append(path)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy = numpy.round(polygon.offset + polygon.vertices[0]).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))
|
||||
return elements
|
||||
|
||||
|
||||
def _labels_to_texts(labels: List[Label],
|
||||
layer2oas: Callable[[layer_t], Tuple[int, int]],
|
||||
) -> List[fatrec.Text]:
|
||||
texts = []
|
||||
for label in labels:
|
||||
layer, datatype = layer2oas(label.layer)
|
||||
xy = numpy.round(label.offset).astype(int)
|
||||
texts.append(fatrec.Text(layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
string=label.string))
|
||||
return texts
|
||||
|
||||
|
||||
def disambiguate_pattern_names(patterns,
|
||||
dup_warn_filter: Callable[[str,], bool] = None, # If returns False, don't warn about this name
|
||||
):
|
||||
used_names = []
|
||||
for pat in patterns:
|
||||
sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name)
|
||||
|
||||
i = 0
|
||||
suffixed_name = sanitized_name
|
||||
while suffixed_name in used_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('Empty pattern name saved as "{}"'.format(suffixed_name))
|
||||
elif suffixed_name != sanitized_name:
|
||||
if dup_warn_filter is None or dup_warn_filter(pat.name):
|
||||
logger.warning('Pattern name "{}" ({}) appears multiple times;\n renaming to "{}"'.format(
|
||||
pat.name, sanitized_name, suffixed_name))
|
||||
|
||||
if len(suffixed_name) == 0:
|
||||
# Should never happen since zero-length names are replaced
|
||||
raise PatternError('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name))
|
||||
|
||||
pat.name = suffixed_name
|
||||
used_names.append(suffixed_name)
|
4
setup.py
4
setup.py
@ -26,9 +26,11 @@ setup(name='masque',
|
||||
'numpy',
|
||||
],
|
||||
extras_require={
|
||||
'visualization': ['matplotlib'],
|
||||
'gdsii': ['python-gdsii'],
|
||||
'oasis': ['fatamorgana>=0.7'],
|
||||
'dxf': ['ezdxf'],
|
||||
'svg': ['svgwrite'],
|
||||
'visualization': ['matplotlib'],
|
||||
'text': ['freetype-py', 'matplotlib'],
|
||||
},
|
||||
classifiers=[
|
||||
|
Loading…
Reference in New Issue
Block a user