Add all files to repository
This commit is contained in:
commit
5bf486ac81
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
*.idea
|
19
README
Normal file
19
README
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Masque README
|
||||||
|
|
||||||
|
Masque is a Python module for designing lithography masks.
|
||||||
|
|
||||||
|
The general idea is to implement something resembling the GDSII file-format, but
|
||||||
|
with some vectorized element types (eg. circles, not just polygons), better support for
|
||||||
|
E-beam doses, and the ability to output to multiple formats.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
* python 3 (written and tested with 3.5)
|
||||||
|
* numpy
|
||||||
|
* matplotlib (optional, used for visualization functions)
|
||||||
|
* python-gdsii (optional, used for gdsii i/o)
|
||||||
|
* svgwrite (optional, used for svg output)
|
||||||
|
|
||||||
|
|
||||||
|
|
32
masque/__init__.py
Normal file
32
masque/__init__.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""
|
||||||
|
masque 2D CAD library
|
||||||
|
|
||||||
|
masque is an attempt to make a relatively small library for designing lithography
|
||||||
|
masks. The general idea is to implement something resembling the GDSII file-format, but
|
||||||
|
with some vectorized element types (eg. circles, not just polygons), better support for
|
||||||
|
E-beam doses, and the ability to output to multiple formats.
|
||||||
|
|
||||||
|
Pattern is a basic object containing a 2D lithography mask, composed of a list of Shape
|
||||||
|
objects and a list of SubPattern objects.
|
||||||
|
|
||||||
|
SubPattern provides basic support for nesting Pattern objects within each other, by adding
|
||||||
|
offset, rotation, scaling, and other such properties to a Pattern reference.
|
||||||
|
|
||||||
|
Note that the methods for these classes try to avoid copying wherever possible, so unless
|
||||||
|
otherwise noted, assume that arguments are stored by-reference.
|
||||||
|
|
||||||
|
|
||||||
|
Dependencies:
|
||||||
|
- numpy
|
||||||
|
- matplotlib [Pattern.visualize(...)]
|
||||||
|
- python-gdsii [masque.file.gdsii]
|
||||||
|
- svgwrite [masque.file.svgwrite]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .error import PatternError
|
||||||
|
from .shapes import Shape
|
||||||
|
from .subpattern import SubPattern
|
||||||
|
from .pattern import Pattern
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
9
masque/error.py
Normal file
9
masque/error.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class PatternError(Exception):
|
||||||
|
"""
|
||||||
|
Simple Exception for Pattern objects and their contents
|
||||||
|
"""
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.value)
|
3
masque/file/__init__.py
Normal file
3
masque/file/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Functions for reading from and writing to various file formats.
|
||||||
|
"""
|
171
masque/file/gdsii.py
Normal file
171
masque/file/gdsii.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
"""
|
||||||
|
GDSII file format readers and writers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gdsii.library
|
||||||
|
import gdsii.structure
|
||||||
|
import gdsii.elements
|
||||||
|
|
||||||
|
from typing import List, Any, Dict
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from .utils import mangle_name, make_dose_table
|
||||||
|
from .. import Pattern, SubPattern, PatternError
|
||||||
|
from ..shapes import Polygon
|
||||||
|
from ..utils import rotation_matrix_2d, get_bit, vector2
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
def write_dose2dtype(pattern: Pattern,
|
||||||
|
filename: str,
|
||||||
|
meters_per_unit: float):
|
||||||
|
"""
|
||||||
|
Write a Pattern to a GDSII file, by first calling .polygonize() on it
|
||||||
|
to change the shapes into polygons, and then writing patterns as GDSII
|
||||||
|
structures, polygons as boundary elements, and subpatterns as structure
|
||||||
|
references (sref).
|
||||||
|
|
||||||
|
Note that this function modifies the Pattern.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
:param pattern: A Pattern to write to file. Modified by this function.
|
||||||
|
:param filename: Filename to write to.
|
||||||
|
:param meters_per_unit: Written into the GDSII file, meters per length unit.
|
||||||
|
"""
|
||||||
|
# Create library
|
||||||
|
lib = gdsii.library.Library(version=600,
|
||||||
|
name='masque-write_dose2dtype'.encode('ASCII'),
|
||||||
|
logical_unit=1,
|
||||||
|
physical_unit=meters_per_unit)
|
||||||
|
|
||||||
|
# Polygonize pattern
|
||||||
|
pattern.polygonize()
|
||||||
|
|
||||||
|
# Get a dict of id(pattern) -> pattern
|
||||||
|
patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern}
|
||||||
|
|
||||||
|
# Get a table of (id(subpat.pattern), written_dose) for each subpattern
|
||||||
|
sd_table = make_dose_table(pattern)
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
[dose_vals.add(shape.dose * pat_dose) for shape in pat.shapes]
|
||||||
|
|
||||||
|
if len(dose_vals) > 256:
|
||||||
|
raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals)))
|
||||||
|
|
||||||
|
dose_vals_list = list(dose_vals)
|
||||||
|
|
||||||
|
# Now create a structure for each row in sd_table (ie, each pattern + dose combination)
|
||||||
|
# and add in any Boundary and SREF elements
|
||||||
|
for pat_id, pat_dose in sd_table:
|
||||||
|
pat = patterns_by_id[pat_id]
|
||||||
|
|
||||||
|
structure = gdsii.structure.Structure(name=mangle_name(pat, pat_dose).encode('ASCII'))
|
||||||
|
lib.append(structure)
|
||||||
|
|
||||||
|
# Add a Boundary element for each shape
|
||||||
|
for shape in pat.shapes:
|
||||||
|
for polygon in shape.to_polygons():
|
||||||
|
data_type = dose_vals_list.index(polygon.dose * pat_dose)
|
||||||
|
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int)
|
||||||
|
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
|
||||||
|
structure.append(gdsii.elements.Boundary(layer=polygon.layer,
|
||||||
|
data_type=data_type,
|
||||||
|
xy=xy_closed))
|
||||||
|
# Add an SREF for each subpattern entry
|
||||||
|
# strans must be set for angle and mag to take effect
|
||||||
|
for subpat in pat.subpatterns:
|
||||||
|
dose_mult = subpat.dose * pat_dose
|
||||||
|
sref = gdsii.elements.SRef(struct_name=mangle_name(subpat.pattern, dose_mult).encode('ASCII'),
|
||||||
|
xy=numpy.round([subpat.offset]).astype(int))
|
||||||
|
sref.strans = 0
|
||||||
|
sref.angle = subpat.rotation
|
||||||
|
sref.mag = subpat.scale
|
||||||
|
structure.append(sref)
|
||||||
|
|
||||||
|
with open(filename, mode='wb') as stream:
|
||||||
|
lib.save(stream)
|
||||||
|
|
||||||
|
return dose_vals_list
|
||||||
|
|
||||||
|
|
||||||
|
def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Read a gdsii file and translate it into a list of Pattern objects. GDSII structures are
|
||||||
|
translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
|
||||||
|
are translated into SubPattern objects.
|
||||||
|
|
||||||
|
:param filename: Filename specifying a GDSII file to read from.
|
||||||
|
:return: Tuple: (List of Patterns generated GDSII structures, Dict of GDSII library info)
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(filename, mode='rb') as stream:
|
||||||
|
lib = gdsii.library.Library.load(stream)
|
||||||
|
|
||||||
|
library_info = {'name': lib.name.decode('ASCII'),
|
||||||
|
'physical_unit': lib.physical_unit,
|
||||||
|
'logical_unit': lib.logical_unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
def ref_element_to_subpat(element, offset: vector2) -> SubPattern:
|
||||||
|
# Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None
|
||||||
|
# and sets the instance attribute .ref_name to the struct_name.
|
||||||
|
#
|
||||||
|
# BUG: Figure out what "absolute" means in the context of elements and if the current
|
||||||
|
# behavior is correct
|
||||||
|
# BUG: Need to check STRANS bit 0 to handle x-reflection
|
||||||
|
subpat = SubPattern(pattern=None, offset=offset)
|
||||||
|
subpat.ref_name = element.struct_name
|
||||||
|
if element.strans is not None:
|
||||||
|
if element.mag is not None:
|
||||||
|
subpat.scale = element.mag
|
||||||
|
# Bit 13 means absolute scale
|
||||||
|
if get_bit(element.strans, 13):
|
||||||
|
subpat.offset *= subpat.scale
|
||||||
|
if element.angle is not None:
|
||||||
|
subpat.rotation = element.angle
|
||||||
|
# Bit 14 means absolute rotation
|
||||||
|
if get_bit(element.strans, 14):
|
||||||
|
subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset)
|
||||||
|
return subpat
|
||||||
|
|
||||||
|
patterns = []
|
||||||
|
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):
|
||||||
|
pat.shapes.append(
|
||||||
|
Polygon(vertices=element.xy[:-1],
|
||||||
|
dose=element.data_type,
|
||||||
|
layer=element.layer))
|
||||||
|
|
||||||
|
elif isinstance(element, gdsii.elements.SRef):
|
||||||
|
pat.subpatterns.append(ref_element_to_subpat(element, element.xy))
|
||||||
|
|
||||||
|
elif isinstance(element, gdsii.elements.ARef):
|
||||||
|
for offset in element.xy:
|
||||||
|
pat.subpatterns.append(ref_element_to_subpat(element, offset))
|
||||||
|
patterns.append(pat)
|
||||||
|
|
||||||
|
# Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
|
||||||
|
# according to the subpattern.ref_name (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.ref_name.decode('ASCII')]
|
||||||
|
del sp.ref_name
|
||||||
|
|
||||||
|
return patterns_dict, library_info
|
139
masque/file/svg.py
Normal file
139
masque/file/svg.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
SVG file format readers and writers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import svgwrite
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from .utils import mangle_name
|
||||||
|
from .. import Pattern
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
def write(pattern: Pattern,
|
||||||
|
filename: str,
|
||||||
|
custom_attributes: bool=False):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
as <use> elements.
|
||||||
|
|
||||||
|
Note that this function modifies the Pattern.
|
||||||
|
|
||||||
|
If custom_attributes is True, non-standard pattern_layer and pattern_dose attributes
|
||||||
|
are written to the relevant elements.
|
||||||
|
|
||||||
|
It is often a good idea to run pattern.subpatternize() on pattern 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.
|
||||||
|
|
||||||
|
:param pattern: Pattern to write to file. Modified by this function.
|
||||||
|
:param filename: Filename to write to.
|
||||||
|
:param custom_attributes: Whether to write non-standard pattern_layer and
|
||||||
|
pattern_dose attributes to the SVG elements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Polygonize pattern
|
||||||
|
pattern.polygonize()
|
||||||
|
|
||||||
|
[bounds_min, bounds_max] = pattern.get_bounds()
|
||||||
|
|
||||||
|
viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2))
|
||||||
|
viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox)
|
||||||
|
|
||||||
|
# Create file
|
||||||
|
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}
|
||||||
|
|
||||||
|
# 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():
|
||||||
|
svg_group = svg.g(id=mangle_name(pat), fill='blue', stroke='red')
|
||||||
|
|
||||||
|
for shape in pat.shapes:
|
||||||
|
for polygon in shape.to_polygons():
|
||||||
|
path_spec = poly2path(polygon.vertices + polygon.offset)
|
||||||
|
|
||||||
|
path = svg.path(d=path_spec)
|
||||||
|
if custom_attributes:
|
||||||
|
path['pattern_layer'] = polygon.layer
|
||||||
|
path['pattern_dose'] = polygon.dose
|
||||||
|
|
||||||
|
svg_group.add(path)
|
||||||
|
|
||||||
|
for subpat in pat.subpatterns:
|
||||||
|
transform = 'scale({:g}) rotate({:g}) translate({:g},{:g})'.format(
|
||||||
|
subpat.scale, subpat.rotation, subpat.offset[0], subpat.offset[1])
|
||||||
|
use = svg.use(href='#' + mangle_name(subpat.pattern), 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.save()
|
||||||
|
|
||||||
|
|
||||||
|
def write_inverted(pattern: Pattern, filename: str):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
box and drawing the polygons with reverse vertex order inside it, all within
|
||||||
|
one <path> element.
|
||||||
|
|
||||||
|
Note that this function modifies the Pattern.
|
||||||
|
|
||||||
|
If you want pattern polygonized with non-default arguments, just call pattern.polygonize()
|
||||||
|
prior to calling this function.
|
||||||
|
|
||||||
|
:param pattern: Pattern to write to file. Modified by this function.
|
||||||
|
:param filename: Filename to write to.
|
||||||
|
"""
|
||||||
|
# Polygonize and flatten pattern
|
||||||
|
pattern.polygonize().flatten()
|
||||||
|
|
||||||
|
[bounds_min, bounds_max] = pattern.get_bounds()
|
||||||
|
|
||||||
|
viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2))
|
||||||
|
viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox)
|
||||||
|
|
||||||
|
# Create file
|
||||||
|
svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string)
|
||||||
|
|
||||||
|
# Draw bounding box
|
||||||
|
slab_edge = [[bounds_min[0] - 1, bounds_max[1] + 1],
|
||||||
|
[bounds_max[0] + 1, bounds_max[1] + 1],
|
||||||
|
[bounds_max[0] + 1, bounds_min[1] - 1],
|
||||||
|
[bounds_min[0] - 1, bounds_min[1] - 1]]
|
||||||
|
path_spec = poly2path(slab_edge)
|
||||||
|
|
||||||
|
# Draw polygons with reversed vertex order
|
||||||
|
for shape in pattern.shapes:
|
||||||
|
for polygon in shape.to_polygons():
|
||||||
|
path_spec += poly2path(polygon.vertices[::-1] + polygon.offset)
|
||||||
|
|
||||||
|
svg.add(svg.path(d=path_spec, fill='blue', stroke='red'))
|
||||||
|
svg.save()
|
||||||
|
|
||||||
|
|
||||||
|
def poly2path(vertices: numpy.ndarray) -> str:
|
||||||
|
"""
|
||||||
|
Create an SVG path string from an Nx2 list of vertices.
|
||||||
|
|
||||||
|
:param vertices: Nx2 array of vertices.
|
||||||
|
:return: SVG path-string.
|
||||||
|
"""
|
||||||
|
commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1])
|
||||||
|
for vertex in vertices[1:]:
|
||||||
|
commands += 'L{:g},{:g}'.format(vertex[0], vertex[1])
|
||||||
|
commands += ' Z '
|
||||||
|
return commands
|
41
masque/file/utils.py
Normal file
41
masque/file/utils.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Helper functions for file reading and writing
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from typing import Set, Tuple
|
||||||
|
|
||||||
|
from masque.pattern import Pattern
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str:
|
||||||
|
"""
|
||||||
|
Create a name using pattern.name, id(pattern), and the dose multiplier.
|
||||||
|
|
||||||
|
:param pattern: Pattern whose name we want to mangle.
|
||||||
|
:param dose_multiplier: Dose multiplier to mangle with.
|
||||||
|
:return: Mangled name.
|
||||||
|
"""
|
||||||
|
expression = re.compile('[^A-Za-z0-9_\?\$]')
|
||||||
|
sanitized_name = expression.sub('_', pattern.name)
|
||||||
|
full_name = '{}_{}_{}'.format(sanitized_name, dose_multiplier, id(pattern))
|
||||||
|
return full_name
|
||||||
|
|
||||||
|
|
||||||
|
def make_dose_table(pattern: Pattern, dose_multiplier: float=1.0) -> Set[Tuple[int, float]]:
|
||||||
|
"""
|
||||||
|
Create a set containing (id(subpat.pattern), written_dose) for each subpattern
|
||||||
|
|
||||||
|
:param pattern: Source Pattern.
|
||||||
|
:param dose_multiplier: Multiplier for all written_dose entries.
|
||||||
|
:return: {(id(subpat.pattern), written_dose), ...}
|
||||||
|
"""
|
||||||
|
dose_table = {(id(pattern), dose_multiplier)}
|
||||||
|
for subpat in pattern.subpatterns:
|
||||||
|
subpat_dose_entry = (id(subpat.pattern), subpat.dose * dose_multiplier)
|
||||||
|
if subpat_dose_entry not in dose_table:
|
||||||
|
subpat_dose_table = make_dose_table(subpat.pattern, subpat.dose * dose_multiplier)
|
||||||
|
dose_table = dose_table.union(subpat_dose_table)
|
||||||
|
return dose_table
|
412
masque/pattern.py
Normal file
412
masque/pattern.py
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
"""
|
||||||
|
Base object for containing a lithography mask.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Callable, Tuple, Dict, Union
|
||||||
|
import copy
|
||||||
|
import itertools
|
||||||
|
import pickle
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
# .visualize imports matplotlib and matplotlib.collections
|
||||||
|
|
||||||
|
from .subpattern import SubPattern
|
||||||
|
from .shapes import Shape, Polygon
|
||||||
|
from .utils import rotation_matrix_2d, vector2
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
class Pattern(object):
|
||||||
|
"""
|
||||||
|
2D layout consisting of some set of shapes and references to other Pattern objects
|
||||||
|
(via SubPattern). Shapes are assumed to inherit from .shapes.Shape or provide equivalent
|
||||||
|
functions.
|
||||||
|
|
||||||
|
:var shapes: List of all shapes in this Pattern. Elements in this list are assumed to inherit
|
||||||
|
from Shape or provide equivalent functions.
|
||||||
|
:var subpatterns: List of all SubPattern objects in this Pattern. Multiple SubPattern objects
|
||||||
|
may reference the same Pattern object.
|
||||||
|
:var name: An identifier for this object. Not necessarily unique.
|
||||||
|
"""
|
||||||
|
|
||||||
|
shapes = List[Shape]
|
||||||
|
subpatterns = List[SubPattern]
|
||||||
|
name = str
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
shapes: List[Shape]=(),
|
||||||
|
subpatterns: List[SubPattern]=(),
|
||||||
|
name: str='',
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Basic init; arguments get assigned to member variables.
|
||||||
|
Non-list inputs for shapes and subpatterns get converted to lists.
|
||||||
|
|
||||||
|
:param shapes: Initial shapes in the Pattern
|
||||||
|
:param subpatterns: Initial subpatterns in the Pattern
|
||||||
|
:param name: An identifier for the Pattern
|
||||||
|
"""
|
||||||
|
if isinstance(shapes, list):
|
||||||
|
self.shapes = shapes
|
||||||
|
else:
|
||||||
|
self.shapes = list(shapes)
|
||||||
|
|
||||||
|
if isinstance(subpatterns, list):
|
||||||
|
self.subpatterns = subpatterns
|
||||||
|
else:
|
||||||
|
self.subpatterns = list(subpatterns)
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def append(self, other_pattern: 'Pattern') -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Appends all shapes and subpatterns from other_pattern to self's shapes and subpatterns.
|
||||||
|
|
||||||
|
:param other_pattern: The Pattern to append
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
self.subpatterns += other_pattern.subpatterns
|
||||||
|
self.shapes += other_pattern.shapes
|
||||||
|
return self
|
||||||
|
|
||||||
|
def subset(self,
|
||||||
|
shapes_func: Callable[[Shape], bool]=None,
|
||||||
|
subpatterns_func: Callable[[SubPattern], bool]=None,
|
||||||
|
) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Returns a Pattern containing only the shapes and subpatterns for which shapes_func or
|
||||||
|
subpatterns_func returns True.
|
||||||
|
Self is _not_ altered, but shapes and subpatterns are _not_ copied.
|
||||||
|
|
||||||
|
:param shapes_func: Given a shape, returns a boolean denoting whether the shape is a member
|
||||||
|
of the subset
|
||||||
|
:param subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member
|
||||||
|
of the subset
|
||||||
|
:return: A Pattern containing all the shapes and subpatterns for which the parameter
|
||||||
|
functions return True
|
||||||
|
"""
|
||||||
|
pat = Pattern()
|
||||||
|
if shapes_func is not None:
|
||||||
|
pat.shapes = [s for s in self.shapes if shapes_func(s)]
|
||||||
|
if subpatterns_func is not None:
|
||||||
|
pat.subpatterns = [s for s in self.subpatterns if subpatterns_func(s)]
|
||||||
|
return pat
|
||||||
|
|
||||||
|
def polygonize(self,
|
||||||
|
poly_num_points: int=None,
|
||||||
|
poly_max_arclen: float=None
|
||||||
|
) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Calls .to_polygons(...) on all the shapes in this Pattern and any referenced patterns,
|
||||||
|
replacing them with the returned polygons.
|
||||||
|
Arguments are passed directly to shape.to_polygons(...).
|
||||||
|
|
||||||
|
:param poly_num_points: Number of points to use for each polygon. Can be overridden by
|
||||||
|
poly_max_arclen if that results in more points. Optional, defaults to shapes'
|
||||||
|
internal defaults.
|
||||||
|
:param poly_max_arclen: Maximum arclength which can be approximated by a single line
|
||||||
|
segment. Optional, defaults to shapes' internal defaults.
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
old_shapes = self.shapes
|
||||||
|
self.shapes = list(itertools.chain.from_iterable(
|
||||||
|
(shape.to_polygons(poly_num_points, poly_max_arclen)
|
||||||
|
for shape in old_shapes)))
|
||||||
|
for subpat in self.subpatterns:
|
||||||
|
subpat.pattern.polygonize(poly_num_points, poly_max_arclen)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def subpatternize(self,
|
||||||
|
recursive: bool=True,
|
||||||
|
norm_value: int=1e6,
|
||||||
|
exclude_types: Tuple[Shape]=(Polygon,)
|
||||||
|
) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Iterates through this Pattern and all referenced Patterns. Within each Pattern, it iterates
|
||||||
|
over all shapes, calling .normalized_form(norm_value) on them to retrieve a scale-,
|
||||||
|
offset-, dose-, and rotation-independent form. Each shape whose normalized form appears
|
||||||
|
more than once is removed and re-added using subpattern objects referencing a newly-created
|
||||||
|
Pattern containing only the normalized form of the shape.
|
||||||
|
|
||||||
|
Note that the default norm_value was chosen to give a reasonable precision when converting
|
||||||
|
to GDSII, which uses integer values for pixel coordinates.
|
||||||
|
|
||||||
|
:param recursive: Whether to call recursively on self's subpatterns. Default True.
|
||||||
|
:param norm_value: Passed to shape.normalized_form(norm_value). Default 1e6 (see function
|
||||||
|
note about GDSII)
|
||||||
|
:param exclude_types: Shape types passed in this argument are always left untouched, for
|
||||||
|
speed or convenience. Default: (Shapes.Polygon,)
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
|
||||||
|
if exclude_types is None:
|
||||||
|
exclude_types = ()
|
||||||
|
|
||||||
|
if recursive:
|
||||||
|
for subpat in self.subpatterns:
|
||||||
|
subpat.pattern.subpatternize(recursive=True,
|
||||||
|
norm_value=norm_value,
|
||||||
|
exclude_types=exclude_types)
|
||||||
|
|
||||||
|
# Create a dict which uses the label tuple from .normalized_form() as a key, and which
|
||||||
|
# stores (function_to_create_normalized_shape, [(index_in_shapes, values), ...]), where
|
||||||
|
# values are the (offset, scale, rotation, dose) values as calculated by .normalized_form()
|
||||||
|
shape_table = defaultdict(lambda: [None, list()])
|
||||||
|
for i, shape in enumerate(self.shapes):
|
||||||
|
if not any((isinstance(shape, t) for t in exclude_types)):
|
||||||
|
label, values, func = shape.normalized_form(norm_value)
|
||||||
|
shape_table[label][0] = func
|
||||||
|
shape_table[label][1].append((i, values))
|
||||||
|
|
||||||
|
# Iterate over the normalized shapes in the table. If any normalized shape occurs more than
|
||||||
|
# once, create a Pattern holding a normalized shape object, and add self.subpatterns
|
||||||
|
# entries for each occurrence in self. Also, note down that we should delete the
|
||||||
|
# self.shapes entries for which we made SubPatterns.
|
||||||
|
shapes_to_remove = []
|
||||||
|
for label in shape_table:
|
||||||
|
if len(shape_table[label][1]) > 1:
|
||||||
|
shape = shape_table[label][0]()
|
||||||
|
pat = Pattern(shapes=[shape])
|
||||||
|
|
||||||
|
for i, values in shape_table[label][1]:
|
||||||
|
(offset, scale, rotation, dose) = values
|
||||||
|
subpat = SubPattern(pattern=pat, offset=offset, scale=scale,
|
||||||
|
rotation=rotation, dose=dose)
|
||||||
|
self.subpatterns.append(subpat)
|
||||||
|
shapes_to_remove.append(i)
|
||||||
|
|
||||||
|
# Remove any shapes for which we have created subpatterns.
|
||||||
|
for i in sorted(shapes_to_remove, reverse=True):
|
||||||
|
del self.shapes[i]
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def as_polygons(self) -> List[numpy.ndarray]:
|
||||||
|
"""
|
||||||
|
Represents the pattern as a list of polygons.
|
||||||
|
|
||||||
|
Deep-copies the pattern, then calls .polygonize() and .flatten() on the copy in order to
|
||||||
|
generate the list of polygons.
|
||||||
|
|
||||||
|
:return: A list of (Ni, 2) numpy.ndarrays specifying vertices of the polygons. Each ndarray
|
||||||
|
is of the form [[x0, y0], [x1, y1],...].
|
||||||
|
"""
|
||||||
|
pat = copy.deepcopy(self).polygonize().flatten()
|
||||||
|
return [shape.vertices + shape.offset for shape in pat.shapes]
|
||||||
|
|
||||||
|
def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']:
|
||||||
|
"""
|
||||||
|
Create a dictionary of {id(pat): pat} for all Pattern objects referenced by this
|
||||||
|
Pattern (operates recursively on all referenced Patterns as well)
|
||||||
|
|
||||||
|
:return: Dictionary of {id(pat): pat} for all referenced Pattern objects
|
||||||
|
"""
|
||||||
|
ids = {}
|
||||||
|
for subpat in self.subpatterns:
|
||||||
|
if id(subpat.pattern) not in ids:
|
||||||
|
ids[id(subpat.pattern)] = subpat.pattern
|
||||||
|
ids.update(subpat.pattern.referenced_patterns_by_id())
|
||||||
|
return ids
|
||||||
|
|
||||||
|
def get_bounds(self) -> Union[numpy.ndarray, None]:
|
||||||
|
"""
|
||||||
|
Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the
|
||||||
|
extent of the Pattern's contents in each dimension.
|
||||||
|
Returns None if the Pattern is empty.
|
||||||
|
|
||||||
|
:return: [[x_min, y_min], [x_max, y_max]] or None
|
||||||
|
"""
|
||||||
|
entries = self.shapes + self.subpatterns
|
||||||
|
if not entries:
|
||||||
|
return None
|
||||||
|
|
||||||
|
init_bounds = entries[0].get_bounds()
|
||||||
|
min_bounds = init_bounds[0, :]
|
||||||
|
max_bounds = init_bounds[1, :]
|
||||||
|
for entry in entries[1:]:
|
||||||
|
bounds = entry.get_bounds()
|
||||||
|
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
|
||||||
|
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
|
||||||
|
return numpy.vstack((min_bounds, max_bounds))
|
||||||
|
|
||||||
|
def flatten(self) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Removes all subpatterns and adds equivalent shapes.
|
||||||
|
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
subpatterns = copy.deepcopy(self.subpatterns)
|
||||||
|
self.subpatterns = []
|
||||||
|
for subpat in subpatterns:
|
||||||
|
subpat.pattern.flatten()
|
||||||
|
self.shapes += subpat.as_pattern().shapes
|
||||||
|
return self
|
||||||
|
|
||||||
|
def translate_elements(self, offset: vector2) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Translates all shapes and subpatterns by the given offset.
|
||||||
|
|
||||||
|
:param offset: Offset to translate by
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
for entry in self.shapes + self.subpatterns:
|
||||||
|
entry.translate(offset)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def scale_elements(self, scale: float) -> 'Pattern':
|
||||||
|
""""
|
||||||
|
Scales all shapes and subpatterns by the given value.
|
||||||
|
|
||||||
|
:param scale: value to scale by
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
for entry in self.shapes + self.subpatterns:
|
||||||
|
entry.scale(scale)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def scale_by(self, c: float) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Scale this Pattern by the given value
|
||||||
|
(all shapes and subpatterns and their offsets are scaled)
|
||||||
|
|
||||||
|
:param c: value to scale by
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
for entry in self.shapes + self.subpatterns:
|
||||||
|
entry.offset *= c
|
||||||
|
entry.scale_by(c)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate_around(self, pivot: vector2, rotation: float) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Rotate the Pattern around the a location.
|
||||||
|
|
||||||
|
:param pivot: Location to rotate around
|
||||||
|
:param rotation: Angle to rotate by (counter-clockwise, radians)
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
pivot = numpy.array(pivot)
|
||||||
|
self.translate_elements(-pivot)
|
||||||
|
self.rotate_elements(rotation)
|
||||||
|
self.rotate_element_centers(rotation)
|
||||||
|
self.translate_elements(+pivot)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate_element_centers(self, rotation: float) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Rotate the offsets of all shapes and subpatterns around (0, 0)
|
||||||
|
|
||||||
|
:param rotation: Angle to rotate by (counter-clockwise, radians)
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
for entry in self.shapes + self.subpatterns:
|
||||||
|
entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate_elements(self, rotation: float) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Rotate each shape and subpattern around its center (offset)
|
||||||
|
|
||||||
|
:param rotation: Angle to rotate by (counter-clockwise, radians)
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
for entry in self.shapes + self.subpatterns:
|
||||||
|
entry.rotate(rotation)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def scale_element_doses(self, factor: float) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Multiply all shape and subpattern doses by a factor
|
||||||
|
|
||||||
|
:param factor: Factor to multiply doses by
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
for entry in self.shapes + self.subpatterns:
|
||||||
|
entry.dose *= factor
|
||||||
|
return self
|
||||||
|
|
||||||
|
def copy(self) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Return a copy of the Pattern, deep-copying shapes and copying subpattern entries, but not
|
||||||
|
deep-copying any referenced patterns.
|
||||||
|
|
||||||
|
:return: A copy of the current Pattern.
|
||||||
|
"""
|
||||||
|
cp = copy.copy(self)
|
||||||
|
cp.shapes = copy.deepcopy(cp.shapes)
|
||||||
|
cp.subpatterns = [copy.copy(subpat) for subpat in cp.subpatterns]
|
||||||
|
return cp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(filename: str) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Load a Pattern from a file
|
||||||
|
|
||||||
|
:param filename: Filename to load from
|
||||||
|
:return: Loaded Pattern
|
||||||
|
"""
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
tmp_dict = pickle.load(f)
|
||||||
|
|
||||||
|
pattern = Pattern()
|
||||||
|
pattern.__dict__.update(tmp_dict)
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
def save(self, filename: str) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Save the Pattern to a file
|
||||||
|
|
||||||
|
:param filename: Filename to save to
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
pickle.dump(self.__dict__, f, protocol=2)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def visualize(self,
|
||||||
|
offset: vector2=(0., 0.),
|
||||||
|
line_color: str='k',
|
||||||
|
fill_color: str='none',
|
||||||
|
overdraw: bool=False):
|
||||||
|
"""
|
||||||
|
Draw a picture of the Pattern and wait for the user to inspect it
|
||||||
|
|
||||||
|
Imports matplotlib.
|
||||||
|
|
||||||
|
:param offset: Coordinates to offset by before drawing
|
||||||
|
:param line_color: Outlines are drawn with this color (passed to matplotlib PolyCollection)
|
||||||
|
:param fill_color: Interiors are drawn with this color (passed to matplotlib PolyCollection)
|
||||||
|
:param overdraw: Whether to create a new figure or draw on a pre-existing one
|
||||||
|
"""
|
||||||
|
from matplotlib import pyplot
|
||||||
|
import matplotlib.collections
|
||||||
|
|
||||||
|
offset = numpy.array(offset, dtype=float)
|
||||||
|
|
||||||
|
if not overdraw:
|
||||||
|
figure = pyplot.figure()
|
||||||
|
pyplot.axis('equal')
|
||||||
|
else:
|
||||||
|
figure = pyplot.gcf()
|
||||||
|
|
||||||
|
axes = figure.gca()
|
||||||
|
|
||||||
|
polygons = []
|
||||||
|
for shape in self.shapes:
|
||||||
|
polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()]
|
||||||
|
|
||||||
|
mpl_poly_collection = matplotlib.collections.PolyCollection(polygons,
|
||||||
|
facecolors=fill_color,
|
||||||
|
edgecolors=line_color)
|
||||||
|
axes.add_collection(mpl_poly_collection)
|
||||||
|
pyplot.axis('equal')
|
||||||
|
|
||||||
|
for subpat in self.subpatterns:
|
||||||
|
subpat.as_pattern().visualize(offset=offset, overdraw=True,
|
||||||
|
line_color=line_color, fill_color=fill_color)
|
||||||
|
|
||||||
|
if not overdraw:
|
||||||
|
pyplot.show()
|
14
masque/shapes/__init__.py
Normal file
14
masque/shapes/__init__.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Shapes for use with the Pattern class, as well as the Shape abstract class from
|
||||||
|
which they are derived.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
||||||
|
|
||||||
|
from .polygon import Polygon
|
||||||
|
from .circle import Circle
|
||||||
|
from .ellipse import Ellipse
|
||||||
|
from .arc import Arc
|
||||||
|
|
||||||
|
|
||||||
|
|
273
masque/shapes/arc.py
Normal file
273
masque/shapes/arc.py
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
from typing import List
|
||||||
|
import math
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
||||||
|
from .. import PatternError
|
||||||
|
from ..utils import is_scalar, vector2
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
class Arc(Shape):
|
||||||
|
"""
|
||||||
|
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
|
||||||
|
center. It has a position, two radii, a start and stop angle, a rotation, and a width.
|
||||||
|
|
||||||
|
The radii define an ellipse; the ring is formed with radii +/- width/2.
|
||||||
|
The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
|
||||||
|
The start and stop angle are measure counterclockwise from the first (x) radius.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_radii = None # type: numpy.ndarray
|
||||||
|
_angles = None # type: numpy.ndarray
|
||||||
|
_width = 1.0 # type: float
|
||||||
|
_rotation = 0.0 # type: float
|
||||||
|
|
||||||
|
# Defaults for to_polygons
|
||||||
|
poly_num_points = DEFAULT_POLY_NUM_POINTS # type: int
|
||||||
|
poly_max_arclen = None # type: float
|
||||||
|
|
||||||
|
# radius properties
|
||||||
|
@property
|
||||||
|
def radii(self) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
Return the radii [rx, ry]
|
||||||
|
|
||||||
|
:return: [rx, ry]
|
||||||
|
"""
|
||||||
|
return self.radii
|
||||||
|
|
||||||
|
@radii.setter
|
||||||
|
def radii(self, val: vector2):
|
||||||
|
val = numpy.array(val, dtype=float).flatten()
|
||||||
|
if not val.size == 2:
|
||||||
|
raise PatternError('Radii must have length 2')
|
||||||
|
if not val.min() >= 0:
|
||||||
|
raise PatternError('Radii must be non-negative')
|
||||||
|
self.radii = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def radius_x(self) -> float:
|
||||||
|
return self.radii[0]
|
||||||
|
|
||||||
|
@radius_x.setter
|
||||||
|
def radius_x(self, val: float):
|
||||||
|
if not val >= 0:
|
||||||
|
raise PatternError('Radius must be non-negative')
|
||||||
|
self.radii[0] = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def radius_y(self) -> float:
|
||||||
|
return self.radii[1]
|
||||||
|
|
||||||
|
@radius_y.setter
|
||||||
|
def radius_y(self, val: float):
|
||||||
|
if not val >= 0:
|
||||||
|
raise PatternError('Radius must be non-negative')
|
||||||
|
self.radii[1] = val
|
||||||
|
|
||||||
|
# arc start/stop angle properties
|
||||||
|
@property
|
||||||
|
def angles(self) -> vector2:
|
||||||
|
"""
|
||||||
|
Return the start and stop angles [a_start, a_stop].
|
||||||
|
Angles are measured from x-axis after rotation, and are stored mod 2*pi
|
||||||
|
|
||||||
|
:return: [a_start, a_stop]
|
||||||
|
"""
|
||||||
|
return self._angles
|
||||||
|
|
||||||
|
@angles.setter
|
||||||
|
def angles(self, val: vector2):
|
||||||
|
val = numpy.array(val, dtype=float).flatten()
|
||||||
|
if not val.size == 2:
|
||||||
|
raise PatternError('Angles must have length 2')
|
||||||
|
angles = val % (2 * pi)
|
||||||
|
if angles[0] > pi:
|
||||||
|
self.rotation += pi
|
||||||
|
angles -= pi
|
||||||
|
self._angles = angles
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start_angle(self) -> float:
|
||||||
|
return self.angles[0]
|
||||||
|
|
||||||
|
@start_angle.setter
|
||||||
|
def start_angle(self, val: float):
|
||||||
|
self.angles[0] = val % (2 * pi)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stop_angle(self) -> float:
|
||||||
|
return self.angles[1]
|
||||||
|
|
||||||
|
@stop_angle.setter
|
||||||
|
def stop_angle(self, val: float):
|
||||||
|
self.angles[1] = val % (2 * pi)
|
||||||
|
|
||||||
|
# Rotation property
|
||||||
|
@property
|
||||||
|
def rotation(self) -> float:
|
||||||
|
"""
|
||||||
|
Rotation of radius_x from x_axis, counterclockwise, in radians. Stored mod 2*pi
|
||||||
|
|
||||||
|
:return: rotation counterclockwise in radians
|
||||||
|
"""
|
||||||
|
return self._rotation
|
||||||
|
|
||||||
|
@rotation.setter
|
||||||
|
def rotation(self, val: float):
|
||||||
|
if not is_scalar(val):
|
||||||
|
raise PatternError('Rotation must be a scalar')
|
||||||
|
self._rotation = val % (2 * pi)
|
||||||
|
|
||||||
|
# Width
|
||||||
|
@property
|
||||||
|
def width(self) -> float:
|
||||||
|
"""
|
||||||
|
Width of the arc (difference between inner and outer radii)
|
||||||
|
|
||||||
|
:return: width
|
||||||
|
"""
|
||||||
|
return self._width
|
||||||
|
|
||||||
|
@width.setter
|
||||||
|
def width(self, val: float):
|
||||||
|
if not is_scalar(val):
|
||||||
|
raise PatternError('Width must be a scalar')
|
||||||
|
if not val > 0:
|
||||||
|
raise PatternError('Width must be positive')
|
||||||
|
self._width = val
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
radii: vector2,
|
||||||
|
angles: vector2,
|
||||||
|
rotation: float=0,
|
||||||
|
poly_num_points: int=DEFAULT_POLY_NUM_POINTS,
|
||||||
|
poly_max_arclen: float=None,
|
||||||
|
offset: vector2=(0.0, 0.0),
|
||||||
|
layer: int=0,
|
||||||
|
dose: float=1.0):
|
||||||
|
self.offset = offset
|
||||||
|
self.layer = layer
|
||||||
|
self.dose = dose
|
||||||
|
self.radii = radii
|
||||||
|
self.angles = angles
|
||||||
|
self.rotation = rotation
|
||||||
|
self.poly_num_points = poly_num_points
|
||||||
|
self.poly_max_arclen = poly_max_arclen
|
||||||
|
|
||||||
|
def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]:
|
||||||
|
if poly_num_points is None:
|
||||||
|
poly_num_points = self.poly_num_points
|
||||||
|
if poly_max_arclen is None:
|
||||||
|
poly_max_arclen = self.poly_max_arclen
|
||||||
|
|
||||||
|
if (poly_num_points is None) and (poly_max_arclen is None):
|
||||||
|
raise PatternError('Max number of points and arclength left unspecified' +
|
||||||
|
' (default was also overridden)')
|
||||||
|
|
||||||
|
rxy = self.radii
|
||||||
|
ang = self.angles
|
||||||
|
|
||||||
|
# Approximate perimeter
|
||||||
|
# Ramanujan, S., "Modular Equations and Approximations to ,"
|
||||||
|
# Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372
|
||||||
|
h = ((rxy[1] - rxy[0]) / rxy.sum()) ** 2
|
||||||
|
ellipse_perimeter = pi * rxy.sum() * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h)))
|
||||||
|
perimeter = abs(ang[0] - ang[1]) / (2 * pi) * ellipse_perimeter
|
||||||
|
|
||||||
|
n = []
|
||||||
|
if poly_num_points is not None:
|
||||||
|
n += [poly_num_points]
|
||||||
|
if poly_max_arclen is not None:
|
||||||
|
n += [perimeter / poly_max_arclen]
|
||||||
|
thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False)
|
||||||
|
|
||||||
|
sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas))
|
||||||
|
wh = self.width / 2.0
|
||||||
|
|
||||||
|
xs1 = (rxy[0] + wh) * cos_th - (rxy[1] + wh) * sin_th
|
||||||
|
ys1 = (rxy[0] + wh) * cos_th - (rxy[1] + wh) * sin_th
|
||||||
|
xs2 = (rxy[0] - wh) * cos_th - (rxy[1] - wh) * sin_th
|
||||||
|
ys2 = (rxy[0] - wh) * cos_th - (rxy[1] - wh) * sin_th
|
||||||
|
|
||||||
|
xs = numpy.hstack((xs1, xs2[::-1]))
|
||||||
|
ys = numpy.hstack((ys1, ys2[::-1]))
|
||||||
|
xys = numpy.vstack((xs, ys)).T
|
||||||
|
|
||||||
|
poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset)
|
||||||
|
poly.rotate(self.rotation)
|
||||||
|
return [poly]
|
||||||
|
|
||||||
|
def get_bounds(self) -> numpy.ndarray:
|
||||||
|
a = self.angles - 0.5 * pi
|
||||||
|
|
||||||
|
mins = []
|
||||||
|
maxs = []
|
||||||
|
for sgn in (+1, -1):
|
||||||
|
wh = sgn * self.width/2
|
||||||
|
rx = self.radius_x + wh
|
||||||
|
ry = self.radius_y + wh
|
||||||
|
|
||||||
|
sin_r = numpy.sin(self.rotation)
|
||||||
|
cos_r = numpy.cos(self.rotation)
|
||||||
|
tan_r = numpy.tan(self.rotation)
|
||||||
|
sin_a = numpy.sin(a)
|
||||||
|
cos_a = numpy.cos(a)
|
||||||
|
|
||||||
|
xpt = numpy.arctan(-ry / rx * tan_r)
|
||||||
|
ypt = numpy.arctan(+ry / rx / tan_r)
|
||||||
|
xnt = numpy.arcsin(numpy.sin(xpt - pi))
|
||||||
|
ynt = numpy.arcsin(numpy.sin(ypt - pi))
|
||||||
|
|
||||||
|
xr = numpy.sqrt((rx * cos_r) ** 2 + (ry * sin_r) ** 2)
|
||||||
|
yr = numpy.sqrt((rx * sin_r) ** 2 + (ry * cos_r) ** 2)
|
||||||
|
|
||||||
|
xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a)
|
||||||
|
yn, yp = sorted(rx * sin_r * cos_a - ry * cos_r * sin_a)
|
||||||
|
|
||||||
|
if min(a) < xpt < max(a):
|
||||||
|
xp = xr
|
||||||
|
|
||||||
|
if min(a) < xnt < max(a):
|
||||||
|
xn = -xr
|
||||||
|
|
||||||
|
if min(a) < ypt < max(a):
|
||||||
|
yp = yr
|
||||||
|
|
||||||
|
if min(a) < ynt < max(a):
|
||||||
|
yn = -yr
|
||||||
|
|
||||||
|
mins.append([xn, yn])
|
||||||
|
maxs.append([xp, yp])
|
||||||
|
return numpy.vstack((numpy.min(mins, axis=0) + self.offset,
|
||||||
|
numpy.max(maxs, axis=0) + self.offset))
|
||||||
|
|
||||||
|
def rotate(self, theta: float) -> 'Arc':
|
||||||
|
self.rotation += theta
|
||||||
|
return self
|
||||||
|
|
||||||
|
def scale_by(self, c: float) -> 'Arc':
|
||||||
|
self.radii *= c
|
||||||
|
self.width *= c
|
||||||
|
return self
|
||||||
|
|
||||||
|
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||||
|
if self.radius_x < self.radius_y:
|
||||||
|
radii = self.radii / self.radius_x
|
||||||
|
scale = self.radius_x
|
||||||
|
rotation = self.rotation
|
||||||
|
angles = self.angles
|
||||||
|
else: # rotate by 90 degrees and swap radii
|
||||||
|
radii = self.radii[::-1] / self.radius_y
|
||||||
|
scale = self.radius_y
|
||||||
|
rotation = self.rotation + pi / 2
|
||||||
|
angles = self.angles - pi / 2
|
||||||
|
return (type(self), radii, angles, self.layer), \
|
||||||
|
(self.offset, scale/norm_value, rotation, self.dose), \
|
||||||
|
lambda: Arc(radii=radii*norm_value, angles=angles, layer=self.layer)
|
||||||
|
|
||||||
|
|
95
masque/shapes/circle.py
Normal file
95
masque/shapes/circle.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from typing import List
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
||||||
|
from .. import PatternError
|
||||||
|
from ..utils import is_scalar, vector2
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
class Circle(Shape):
|
||||||
|
"""
|
||||||
|
A circle, which has a position and radius.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_radius = None # type: float
|
||||||
|
|
||||||
|
# Defaults for to_polygons
|
||||||
|
poly_num_points = DEFAULT_POLY_NUM_POINTS # type: int
|
||||||
|
poly_max_arclen = None # type: float
|
||||||
|
|
||||||
|
# radius property
|
||||||
|
@property
|
||||||
|
def radius(self) -> float:
|
||||||
|
"""
|
||||||
|
Circle's radius (float, >= 0)
|
||||||
|
|
||||||
|
:return: radius
|
||||||
|
"""
|
||||||
|
return self._radius
|
||||||
|
|
||||||
|
@radius.setter
|
||||||
|
def radius(self, val: float):
|
||||||
|
if not is_scalar(val):
|
||||||
|
raise PatternError('Radius must be a scalar')
|
||||||
|
if not val >= 0:
|
||||||
|
raise PatternError('Radius must be non-negative')
|
||||||
|
self._radius = val
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
radius: float,
|
||||||
|
poly_num_points: int=DEFAULT_POLY_NUM_POINTS,
|
||||||
|
poly_max_arclen: float=None,
|
||||||
|
offset: vector2=(0.0, 0.0),
|
||||||
|
layer: int=0,
|
||||||
|
dose: float=1.0):
|
||||||
|
self.offset = numpy.array(offset, dtype=float)
|
||||||
|
self.layer = layer
|
||||||
|
self.dose = dose
|
||||||
|
self.radius = radius
|
||||||
|
self.poly_num_points = poly_num_points
|
||||||
|
self.poly_max_arclen = poly_max_arclen
|
||||||
|
|
||||||
|
def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]:
|
||||||
|
if poly_num_points is None:
|
||||||
|
poly_num_points = self.poly_num_points
|
||||||
|
if poly_max_arclen is None:
|
||||||
|
poly_max_arclen = self.poly_max_arclen
|
||||||
|
|
||||||
|
if (poly_num_points is None) and (poly_max_arclen is None):
|
||||||
|
raise PatternError('Number of points and arclength left '
|
||||||
|
'unspecified (default was also overridden)')
|
||||||
|
|
||||||
|
n = []
|
||||||
|
if poly_num_points is not None:
|
||||||
|
n += [poly_num_points]
|
||||||
|
if poly_max_arclen is not None:
|
||||||
|
n += [2 * pi * self.radius / poly_max_arclen]
|
||||||
|
thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False)
|
||||||
|
xs = numpy.cos(thetas) * self.radius
|
||||||
|
ys = numpy.sin(thetas) * self.radius
|
||||||
|
xys = numpy.vstack((xs, ys)).T
|
||||||
|
|
||||||
|
return [Polygon(xys, offset=self.offset, dose=self.dose, layer=self.layer)]
|
||||||
|
|
||||||
|
def get_bounds(self) -> numpy.ndarray:
|
||||||
|
return numpy.vstack((self.offset - self.radius,
|
||||||
|
self.offset + self.radius))
|
||||||
|
|
||||||
|
def rotate(self, theta: float) -> 'Circle':
|
||||||
|
return self
|
||||||
|
|
||||||
|
def scale_by(self, c: float) -> 'Circle':
|
||||||
|
self.radius *= c
|
||||||
|
return self
|
||||||
|
|
||||||
|
def normalized_form(self, norm_value) -> normalized_shape_tuple:
|
||||||
|
rotation = 0.0
|
||||||
|
magnitude = self.radius / norm_value
|
||||||
|
return (type(self), self.layer), \
|
||||||
|
(self.offset, magnitude, rotation, self.dose), \
|
||||||
|
lambda: Circle(radius=norm_value, layer=self.layer)
|
||||||
|
|
161
masque/shapes/ellipse.py
Normal file
161
masque/shapes/ellipse.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
from typing import List
|
||||||
|
import math
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
|
||||||
|
from .. import PatternError
|
||||||
|
from ..utils import is_scalar, rotation_matrix_2d, vector2
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
class Ellipse(Shape):
|
||||||
|
"""
|
||||||
|
An ellipse, which has a position, two radii, and a rotation.
|
||||||
|
The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_radii = None # type: numpy.ndarray
|
||||||
|
_rotation = 0.0 # type: float
|
||||||
|
|
||||||
|
# Defaults for to_polygons
|
||||||
|
poly_num_points = DEFAULT_POLY_NUM_POINTS # type: int
|
||||||
|
poly_max_arclen = None # type: float
|
||||||
|
|
||||||
|
# radius properties
|
||||||
|
@property
|
||||||
|
def radii(self) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
Return the radii [rx, ry]
|
||||||
|
|
||||||
|
:return: [rx, ry]
|
||||||
|
"""
|
||||||
|
return self.radii
|
||||||
|
|
||||||
|
@radii.setter
|
||||||
|
def radii(self, val: vector2):
|
||||||
|
val = numpy.array(val).flatten()
|
||||||
|
if not val.size == 2:
|
||||||
|
raise PatternError('Radii must have length 2')
|
||||||
|
if not val.min() >= 0:
|
||||||
|
raise PatternError('Radii must be non-negative')
|
||||||
|
self.radii = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def radius_x(self) -> float:
|
||||||
|
return self.radii[0]
|
||||||
|
|
||||||
|
@radius_x.setter
|
||||||
|
def radius_x(self, val: float):
|
||||||
|
if not val >= 0:
|
||||||
|
raise PatternError('Radius must be non-negative')
|
||||||
|
self.radii[0] = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def radius_y(self) -> float:
|
||||||
|
return self.radii[1]
|
||||||
|
|
||||||
|
@radius_y.setter
|
||||||
|
def radius_y(self, val: float):
|
||||||
|
if not val >= 0:
|
||||||
|
raise PatternError('Radius must be non-negative')
|
||||||
|
self.radii[1] = val
|
||||||
|
|
||||||
|
# Rotation property
|
||||||
|
@property
|
||||||
|
def rotation(self) -> float:
|
||||||
|
"""
|
||||||
|
Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise
|
||||||
|
is positive)
|
||||||
|
|
||||||
|
:return: counterclockwise rotation in radians
|
||||||
|
"""
|
||||||
|
return self._rotation
|
||||||
|
|
||||||
|
@rotation.setter
|
||||||
|
def rotation(self, val: float):
|
||||||
|
if not is_scalar(val):
|
||||||
|
raise PatternError('Rotation must be a scalar')
|
||||||
|
self._rotation = val % pi
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
radii: vector2,
|
||||||
|
rotation: float=0,
|
||||||
|
poly_num_points: int=DEFAULT_POLY_NUM_POINTS,
|
||||||
|
poly_max_arclen: float=None,
|
||||||
|
offset: vector2=(0.0, 0.0),
|
||||||
|
layer: int=0,
|
||||||
|
dose: float=1.0):
|
||||||
|
self.offset = offset
|
||||||
|
self.layer = layer
|
||||||
|
self.dose = dose
|
||||||
|
self.radii = radii
|
||||||
|
self.rotation = rotation
|
||||||
|
self.poly_num_points = poly_num_points
|
||||||
|
self.poly_max_arclen = poly_max_arclen
|
||||||
|
|
||||||
|
def to_polygons(self,
|
||||||
|
poly_num_points: int=None,
|
||||||
|
poly_max_arclen: float=None
|
||||||
|
) -> List[Polygon]:
|
||||||
|
if poly_num_points is None:
|
||||||
|
poly_num_points = self.poly_num_points
|
||||||
|
if poly_max_arclen is None:
|
||||||
|
poly_max_arclen = self.poly_max_arclen
|
||||||
|
|
||||||
|
if (poly_num_points is None) and (poly_max_arclen is None):
|
||||||
|
raise PatternError('Number of points and arclength left unspecified'
|
||||||
|
' (default was also overridden)')
|
||||||
|
|
||||||
|
rxy = self.radii
|
||||||
|
|
||||||
|
# Approximate perimeter
|
||||||
|
# Ramanujan, S., "Modular Equations and Approximations to ,"
|
||||||
|
# Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372
|
||||||
|
h = ((rxy[1] - rxy[0]) / rxy.sum()) ** 2
|
||||||
|
perimeter = pi * rxy.sum() * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h)))
|
||||||
|
|
||||||
|
n = []
|
||||||
|
if poly_num_points is not None:
|
||||||
|
n += [poly_num_points]
|
||||||
|
if poly_max_arclen is not None:
|
||||||
|
n += [perimeter / poly_max_arclen]
|
||||||
|
thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False)
|
||||||
|
|
||||||
|
sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas))
|
||||||
|
xs = rxy[0] * cos_th - rxy[1] * sin_th
|
||||||
|
ys = rxy[0] * sin_th - rxy[1] * cos_th
|
||||||
|
xys = numpy.vstack((xs, ys)).T
|
||||||
|
|
||||||
|
poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset)
|
||||||
|
poly.rotate(self.rotation)
|
||||||
|
return [poly]
|
||||||
|
|
||||||
|
def get_bounds(self) -> numpy.ndarray:
|
||||||
|
rot_radii = numpy.dot(rotation_matrix_2d(self.rotation), self.radii)
|
||||||
|
return numpy.vstack((self.offset - rot_radii[0],
|
||||||
|
self.offset + rot_radii[1]))
|
||||||
|
|
||||||
|
def rotate(self, theta: float) -> 'Ellipse':
|
||||||
|
self.rotation += theta
|
||||||
|
return self
|
||||||
|
|
||||||
|
def scale_by(self, c: float) -> 'Ellipse':
|
||||||
|
self.radii *= c
|
||||||
|
return self
|
||||||
|
|
||||||
|
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||||
|
if self.radius_x < self.radius_y:
|
||||||
|
radii = self.radii / self.radius_x
|
||||||
|
scale = self.radius_x
|
||||||
|
angle = self.rotation
|
||||||
|
else:
|
||||||
|
radii = self.radii[::-1] / self.radius_y
|
||||||
|
scale = self.radius_y
|
||||||
|
angle = (self.rotation + pi / 2) % pi
|
||||||
|
return (type(self), radii, self.layer), \
|
||||||
|
(self.offset, scale/norm_value, angle, self.dose), \
|
||||||
|
lambda: Ellipse(radii=radii*norm_value, layer=self.layer)
|
||||||
|
|
174
masque/shapes/polygon.py
Normal file
174
masque/shapes/polygon.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
from typing import List
|
||||||
|
import copy
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
from . import Shape, normalized_shape_tuple
|
||||||
|
from .. import PatternError
|
||||||
|
from ..utils import is_scalar, rotation_matrix_2d, vector2
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
class Polygon(Shape):
|
||||||
|
"""
|
||||||
|
A polygon, consisting of a bunch of vertices (Nx2 ndarray) along with an offset.
|
||||||
|
|
||||||
|
A normalized_form(...) is available, but can be quite slow with lots of vertices.
|
||||||
|
"""
|
||||||
|
_vertices = None # type: numpy.ndarray
|
||||||
|
|
||||||
|
# vertices property
|
||||||
|
@property
|
||||||
|
def vertices(self) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
Vertices of the polygon (Nx2 ndarray: [[x0, y0], [x1, y1], ...]
|
||||||
|
|
||||||
|
:return: vertices
|
||||||
|
"""
|
||||||
|
return self._vertices
|
||||||
|
|
||||||
|
@vertices.setter
|
||||||
|
def vertices(self, val: numpy.ndarray):
|
||||||
|
val = numpy.array(val, dtype=float)
|
||||||
|
if val.shape[1] != 2:
|
||||||
|
raise PatternError('Vertices must be an Nx2 array')
|
||||||
|
if val.shape[0] < 3:
|
||||||
|
raise PatternError('Must have at least 3 vertices (Nx2, N>3)')
|
||||||
|
self._vertices = val
|
||||||
|
|
||||||
|
# xs property
|
||||||
|
@property
|
||||||
|
def xs(self) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
All x vertices in a 1D ndarray
|
||||||
|
"""
|
||||||
|
return self.vertices[:, 0]
|
||||||
|
|
||||||
|
@xs.setter
|
||||||
|
def xs(self, val: numpy.ndarray):
|
||||||
|
val = numpy.array(val, dtype=float).flatten()
|
||||||
|
if val.size != self.vertices.shape[0]:
|
||||||
|
raise PatternError('Wrong number of vertices')
|
||||||
|
self.vertices[:, 0] = val
|
||||||
|
|
||||||
|
# ys property
|
||||||
|
@property
|
||||||
|
def ys(self) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
All y vertices in a 1D ndarray
|
||||||
|
"""
|
||||||
|
return self.vertices[:, 1]
|
||||||
|
|
||||||
|
@ys.setter
|
||||||
|
def ys(self, val: numpy.ndarray):
|
||||||
|
val = numpy.array(val, dtype=float).flatten()
|
||||||
|
if val.size != self.vertices.shape[0]:
|
||||||
|
raise PatternError('Wrong number of vertices')
|
||||||
|
self.vertices[:, 1] = val
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
vertices: numpy.ndarray,
|
||||||
|
offset: vector2=(0.0, 0.0),
|
||||||
|
layer: int=0,
|
||||||
|
dose: float=1.0):
|
||||||
|
self.offset = offset
|
||||||
|
self.layer = layer
|
||||||
|
self.dose = dose
|
||||||
|
self.vertices = vertices
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def square(side_length: float,
|
||||||
|
rotation: float=0.0,
|
||||||
|
offset: vector2=(0.0, 0.0),
|
||||||
|
layer: int=0,
|
||||||
|
dose: float=1.0
|
||||||
|
) -> 'Polygon':
|
||||||
|
"""
|
||||||
|
Draw a square given side_length
|
||||||
|
|
||||||
|
:param side_length: Length of one side
|
||||||
|
:param rotation: Rotation counterclockwise, in radians
|
||||||
|
:param offset: Offset, default (0, 0)
|
||||||
|
:param layer: Layer, default 0
|
||||||
|
:param dose: Dose, default 1.0
|
||||||
|
:return: A Polygon object containing the requested square
|
||||||
|
"""
|
||||||
|
norm_square = numpy.array([[-1, -1],
|
||||||
|
[-1, +1],
|
||||||
|
[+1, +1],
|
||||||
|
[+1, -1]], dtype=float)
|
||||||
|
vertices = 0.5 * side_length * norm_square
|
||||||
|
poly = Polygon(vertices, offset, layer, dose)
|
||||||
|
poly.rotate(rotation)
|
||||||
|
return poly
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rectangle(lx: float,
|
||||||
|
ly: float,
|
||||||
|
rotation: float=0,
|
||||||
|
offset: vector2=(0.0, 0.0),
|
||||||
|
layer: int=0,
|
||||||
|
dose: float=1.0
|
||||||
|
) -> 'Polygon':
|
||||||
|
"""
|
||||||
|
Draw a rectangle with side lengths lx and ly
|
||||||
|
|
||||||
|
:param lx: Length along x (before rotation)
|
||||||
|
:param ly: Length along y (before rotation)
|
||||||
|
:param rotation: Rotation counterclockwise, in radians
|
||||||
|
:param offset: Offset, default (0, 0)
|
||||||
|
:param layer: Layer, default 0
|
||||||
|
:param dose: Dose, default 1.0
|
||||||
|
:return: A Polygon object containing the requested rectangle
|
||||||
|
"""
|
||||||
|
vertices = 0.5 * numpy.array([[-lx, -ly],
|
||||||
|
[-lx, +ly],
|
||||||
|
[+lx, +ly],
|
||||||
|
[+lx, -ly]], dtype=float)
|
||||||
|
poly = Polygon(vertices, offset, layer, dose)
|
||||||
|
poly.rotate(rotation)
|
||||||
|
return poly
|
||||||
|
|
||||||
|
def to_polygons(self,
|
||||||
|
_poly_num_points: int=None,
|
||||||
|
_poly_max_arclen: float=None,
|
||||||
|
) -> List['Polygon']:
|
||||||
|
return [copy.deepcopy(self)]
|
||||||
|
|
||||||
|
def get_bounds(self) -> numpy.ndarray:
|
||||||
|
return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0),
|
||||||
|
self.offset + numpy.max(self.vertices, axis=0)))
|
||||||
|
|
||||||
|
def rotate(self, theta: float) -> 'Polygon':
|
||||||
|
self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T
|
||||||
|
return self
|
||||||
|
|
||||||
|
def scale_by(self, c: float) -> 'Polygon':
|
||||||
|
self.vertices *= c
|
||||||
|
return self
|
||||||
|
|
||||||
|
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||||
|
# Note: this function is going to be pretty slow for many-vertexed polygons, relative to
|
||||||
|
# other shapes
|
||||||
|
offset = self.vertices.mean(axis=0) + self.offset
|
||||||
|
zeroed_vertices = self.vertices - offset
|
||||||
|
|
||||||
|
scale = zeroed_vertices.std()
|
||||||
|
normed_vertices = zeroed_vertices / scale
|
||||||
|
|
||||||
|
_, _, vertex_axis = numpy.linalg.svd(zeroed_vertices)
|
||||||
|
rotation = numpy.arctan2(vertex_axis[0][1], vertex_axis[0][0]) % (2 * pi)
|
||||||
|
rotated_vertices = numpy.vstack([numpy.dot(rotation_matrix_2d(-rotation), v)
|
||||||
|
for v in normed_vertices])
|
||||||
|
|
||||||
|
# Reorder the vertices so that the one with lowest x, then y, comes first.
|
||||||
|
x_min = rotated_vertices[:, 0].argmin()
|
||||||
|
if not is_scalar(x_min):
|
||||||
|
y_min = rotated_vertices[x_min, 1].argmin()
|
||||||
|
x_min = x_min[y_min]
|
||||||
|
reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
|
||||||
|
|
||||||
|
return (type(self), reordered_vertices.data.tobytes(), self.layer), \
|
||||||
|
(offset, scale/norm_value, rotation, self.dose), \
|
||||||
|
lambda: Polygon(reordered_vertices*norm_value, layer=self.layer)
|
182
masque/shapes/shape.py
Normal file
182
masque/shapes/shape.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
from typing import List, Tuple, Callable
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from .. import PatternError
|
||||||
|
from ..utils import is_scalar, rotation_matrix_2d, vector2
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
# Type definitions
|
||||||
|
normalized_shape_tuple = Tuple[Tuple,
|
||||||
|
Tuple[numpy.ndarray, float, float, float],
|
||||||
|
Callable[[], 'Shape']]
|
||||||
|
|
||||||
|
# ## Module-wide defaults
|
||||||
|
# Default number of points per polygon for shapes
|
||||||
|
DEFAULT_POLY_NUM_POINTS = 24
|
||||||
|
|
||||||
|
|
||||||
|
class Shape(object, metaclass=ABCMeta):
|
||||||
|
"""
|
||||||
|
Abstract class specifying functions common to all shapes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# [x_offset, y_offset]
|
||||||
|
_offset = numpy.array([0.0, 0.0]) # type: numpy.ndarray
|
||||||
|
|
||||||
|
# Layer (integer >= 0)
|
||||||
|
_layer = 0 # type: int
|
||||||
|
|
||||||
|
# Dose
|
||||||
|
_dose = 1.0 # type: float
|
||||||
|
|
||||||
|
# --- Abstract methods
|
||||||
|
@abstractmethod
|
||||||
|
def to_polygons(self, num_vertices: int, max_arclen: float) -> List['Polygon']:
|
||||||
|
"""
|
||||||
|
Returns a list of polygons which approximate the shape.
|
||||||
|
|
||||||
|
:param num_vertices: Number of points to use for each polygon. Can be overridden by
|
||||||
|
max_arclen if that results in more points. Optional, defaults to shapes'
|
||||||
|
internal defaults.
|
||||||
|
:param max_arclen: Maximum arclength which can be approximated by a single line
|
||||||
|
segment. Optional, defaults to shapes' internal defaults.
|
||||||
|
:return: List of polygons equivalent to the shape
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_bounds(self) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
Returns [[x_min, y_min], [x_max, y_max]] which specify a minimal bounding box for the shape.
|
||||||
|
|
||||||
|
:return: [[x_min, y_min], [x_max, y_max]]
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def rotate(self, theta: float) -> 'Shape':
|
||||||
|
"""
|
||||||
|
Rotate the shape around its center (0, 0), ignoring its offset.
|
||||||
|
|
||||||
|
:param theta: Angle to rotate by (counterclockwise, radians)
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def scale_by(self, c: float) -> 'Shape':
|
||||||
|
"""
|
||||||
|
Scale the shape's size (eg. radius, for a circle) by a constant factor.
|
||||||
|
|
||||||
|
:param c: Factor to scale by
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def normalized_form(self, norm_value: int) -> normalized_shape_tuple:
|
||||||
|
"""
|
||||||
|
Writes the shape in a standardized notation, with offset, scale, rotation, and dose
|
||||||
|
information separated out from the remaining values.
|
||||||
|
|
||||||
|
:param norm_value: This value is used to normalize lengths intrinsic to teh shape;
|
||||||
|
eg. for a circle, the returned magnitude value will be (radius / norm_value), and
|
||||||
|
the returned callable will create a Circle(radius=norm_value, ...). This is useful
|
||||||
|
when you find it important for quantities to remain in a certain range, eg. for
|
||||||
|
GDSII where vertex locations are stored as integers.
|
||||||
|
:return: The returned information takes the form of a 3-element tuple,
|
||||||
|
(intrinsic, extrinsic, constructor). These are further broken down as:
|
||||||
|
extrinsic: ([x_offset, y_offset], scale, rotation, dose)
|
||||||
|
intrinsic: A tuple of basic types containing all information about the instance that
|
||||||
|
is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self).
|
||||||
|
constructor: A callable (no arguments) which returns an instance of type(self) with
|
||||||
|
internal state equivalent to 'intrinsic'.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ---- Non-abstract properties
|
||||||
|
# offset property
|
||||||
|
@property
|
||||||
|
def offset(self) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
[x, y] offset
|
||||||
|
|
||||||
|
:return: [x_offset, y_offset]
|
||||||
|
"""
|
||||||
|
return self._offset
|
||||||
|
|
||||||
|
@offset.setter
|
||||||
|
def offset(self, val: vector2):
|
||||||
|
if not isinstance(val, numpy.ndarray):
|
||||||
|
val = numpy.array(val, dtype=float)
|
||||||
|
|
||||||
|
if val.size != 2:
|
||||||
|
raise PatternError('Offset must be convertible to size-2 ndarray')
|
||||||
|
self._offset = val.flatten()
|
||||||
|
|
||||||
|
# layer property
|
||||||
|
@property
|
||||||
|
def layer(self) -> int:
|
||||||
|
"""
|
||||||
|
Layer number (int, >=0)
|
||||||
|
|
||||||
|
:return: Layer
|
||||||
|
"""
|
||||||
|
return self._layer
|
||||||
|
|
||||||
|
@layer.setter
|
||||||
|
def layer(self, val: int):
|
||||||
|
if not isinstance(val, int):
|
||||||
|
raise PatternError('Layer must be an integer')
|
||||||
|
if not val >= 0:
|
||||||
|
raise PatternError('Layer must be non-negative')
|
||||||
|
self._layer = val
|
||||||
|
|
||||||
|
# dose property
|
||||||
|
@property
|
||||||
|
def dose(self) -> float:
|
||||||
|
"""
|
||||||
|
Dose (float >= 0)
|
||||||
|
|
||||||
|
:return: Dose value
|
||||||
|
"""
|
||||||
|
return self._dose
|
||||||
|
|
||||||
|
@dose.setter
|
||||||
|
def dose(self, val: float):
|
||||||
|
if not is_scalar(val):
|
||||||
|
raise PatternError('Dose must be a scalar')
|
||||||
|
if not val >= 0:
|
||||||
|
raise PatternError('Dose must be non-negative')
|
||||||
|
self._dose = val
|
||||||
|
|
||||||
|
# ---- Non-abstract methods
|
||||||
|
def translate(self, offset: vector2) -> 'Shape':
|
||||||
|
"""
|
||||||
|
Translate the shape by the given offset
|
||||||
|
|
||||||
|
:param offset: [x_offset, y,offset]
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
self.offset += offset
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate_around(self, pivot: vector2, rotation: float) -> 'Shape':
|
||||||
|
"""
|
||||||
|
Rotate the shape around a point.
|
||||||
|
|
||||||
|
:param pivot: Point (x, y) to rotate around
|
||||||
|
:param rotation: Angle to rotate by (counterclockwise, radians)
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
pivot = numpy.array(pivot, dtype=float)
|
||||||
|
self.translate(-pivot)
|
||||||
|
self.rotate(rotation)
|
||||||
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||||
|
self.translate(+pivot)
|
||||||
|
return self
|
||||||
|
|
57
masque/shapes/text.py
Normal file
57
masque/shapes/text.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
#
|
||||||
|
# class Text(Shape):
|
||||||
|
# _string = ''
|
||||||
|
# _height = 1.0
|
||||||
|
# _rotation = 0.0
|
||||||
|
# font_path = ''
|
||||||
|
#
|
||||||
|
# # vertices property
|
||||||
|
# @property
|
||||||
|
# def string(self):
|
||||||
|
# return self._string
|
||||||
|
#
|
||||||
|
# @string.setter
|
||||||
|
# def string(self, val):
|
||||||
|
# self._string = val
|
||||||
|
#
|
||||||
|
# # Rotation property
|
||||||
|
# @property
|
||||||
|
# def rotation(self):
|
||||||
|
# return self._rotation
|
||||||
|
#
|
||||||
|
# @rotation.setter
|
||||||
|
# def rotation(self, val):
|
||||||
|
# if not is_scalar(val):
|
||||||
|
# raise PatternError('Rotation must be a scalar')
|
||||||
|
# self._rotation = val % (2 * pi)
|
||||||
|
#
|
||||||
|
# # Height property
|
||||||
|
# @property
|
||||||
|
# def height(self):
|
||||||
|
# return self._height
|
||||||
|
#
|
||||||
|
# @height.setter
|
||||||
|
# def height(self, val):
|
||||||
|
# if not is_scalar(val):
|
||||||
|
# raise PatternError('Height must be a scalar')
|
||||||
|
# self._height = val
|
||||||
|
#
|
||||||
|
# def __init__(self, text, height, font_path, rotation=0.0, offset=(0.0, 0.0), layer=0, dose=1.0):
|
||||||
|
# self.offset = offset
|
||||||
|
# self.layer = layer
|
||||||
|
# self.dose = dose
|
||||||
|
# self.text = text
|
||||||
|
# self.height = height
|
||||||
|
# self.rotation = rotation
|
||||||
|
# self.font_path = font_path
|
||||||
|
#
|
||||||
|
# def to_polygon(self, _poly_num_points=None, _poly_max_arclen=None):
|
||||||
|
#
|
||||||
|
# return copy.deepcopy(self)
|
||||||
|
#
|
||||||
|
# def rotate(self, theta):
|
||||||
|
# self.rotation += theta
|
||||||
|
#
|
||||||
|
# def scale_by(self, c):
|
||||||
|
# self.height *= c
|
159
masque/subpattern.py
Normal file
159
masque/subpattern.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
"""
|
||||||
|
SubPattern provides basic support for nesting Pattern objects within each other, by adding
|
||||||
|
offset, rotation, scaling, and other such properties to the reference.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
from .error import PatternError
|
||||||
|
from .utils import is_scalar, rotation_matrix_2d, vector2
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
|
||||||
|
class SubPattern(object):
|
||||||
|
"""
|
||||||
|
SubPattern provides basic support for nesting Pattern objects within each other, by adding
|
||||||
|
offset, rotation, scaling, and associated methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pattern = None # type: Pattern
|
||||||
|
_offset = (0.0, 0.0) # type: numpy.ndarray
|
||||||
|
_rotation = 0.0 # type: float
|
||||||
|
_dose = 1.0 # type: float
|
||||||
|
_scale = 1.0 # type: float
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
pattern: 'Pattern',
|
||||||
|
offset: vector2=(0.0, 0.0),
|
||||||
|
rotation: float=0.0,
|
||||||
|
dose: float=1.0,
|
||||||
|
scale: float=1.0):
|
||||||
|
self.pattern = pattern
|
||||||
|
self.offset = offset
|
||||||
|
self.rotation = rotation
|
||||||
|
self.dose = dose
|
||||||
|
self.scale = scale
|
||||||
|
|
||||||
|
# offset property
|
||||||
|
@property
|
||||||
|
def offset(self) -> numpy.ndarray:
|
||||||
|
return self._offset
|
||||||
|
|
||||||
|
@offset.setter
|
||||||
|
def offset(self, val: vector2):
|
||||||
|
if not isinstance(val, numpy.ndarray):
|
||||||
|
val = numpy.array(val, dtype=float)
|
||||||
|
|
||||||
|
if val.size != 2:
|
||||||
|
raise PatternError('Offset must be convertible to size-2 ndarray')
|
||||||
|
self._offset = val.flatten()
|
||||||
|
|
||||||
|
# dose property
|
||||||
|
@property
|
||||||
|
def dose(self) -> float:
|
||||||
|
return self._dose
|
||||||
|
|
||||||
|
@dose.setter
|
||||||
|
def dose(self, val: float):
|
||||||
|
if not is_scalar(val):
|
||||||
|
raise PatternError('Dose must be a scalar')
|
||||||
|
if not val >= 0:
|
||||||
|
raise PatternError('Dose must be non-negative')
|
||||||
|
self._dose = val
|
||||||
|
|
||||||
|
# scale property
|
||||||
|
@property
|
||||||
|
def scale(self) -> float:
|
||||||
|
return self._scale
|
||||||
|
|
||||||
|
@scale.setter
|
||||||
|
def scale(self, val: float):
|
||||||
|
if not is_scalar(val):
|
||||||
|
raise PatternError('Scale must be a scalar')
|
||||||
|
if not val > 0:
|
||||||
|
raise PatternError('Scale must be positive')
|
||||||
|
self._scale = val
|
||||||
|
|
||||||
|
# Rotation property [ccw]
|
||||||
|
@property
|
||||||
|
def rotation(self) -> float:
|
||||||
|
return self._rotation
|
||||||
|
|
||||||
|
@rotation.setter
|
||||||
|
def rotation(self, val: float):
|
||||||
|
if not is_scalar(val):
|
||||||
|
raise PatternError('Rotation must be a scalar')
|
||||||
|
self._rotation = val % (2 * pi)
|
||||||
|
|
||||||
|
def as_pattern(self) -> 'Pattern':
|
||||||
|
"""
|
||||||
|
Returns a copy of self.pattern which has been scaled, rotated, etc. according to this
|
||||||
|
SubPattern's properties.
|
||||||
|
:return: Copy of self.pattern that has been altered to reflect the SubPattern's properties.
|
||||||
|
"""
|
||||||
|
pattern = self.pattern.copy()
|
||||||
|
pattern.scale_by(self.scale)
|
||||||
|
pattern.rotate_around((0.0, 0.0), self.rotation)
|
||||||
|
pattern.translate_elements(self.offset)
|
||||||
|
pattern.scale_element_doses(self.dose)
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
def translate(self, offset: vector2) -> 'SubPattern':
|
||||||
|
"""
|
||||||
|
Translate by the given offset
|
||||||
|
|
||||||
|
:param offset: Translate by this offset
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
self.offset += offset
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate_around(self, pivot: vector2, rotation: float) -> 'SubPattern':
|
||||||
|
"""
|
||||||
|
Rotate around a point
|
||||||
|
|
||||||
|
:param pivot: Point to rotate around
|
||||||
|
:param rotation: Angle to rotate by (counterclockwise, radians)
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
pivot = numpy.array(pivot, dtype=float)
|
||||||
|
self.translate(-pivot)
|
||||||
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||||
|
self.rotate(rotation)
|
||||||
|
self.translate(+pivot)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate(self, rotation: float) -> 'SubPattern':
|
||||||
|
"""
|
||||||
|
Rotate around (0, 0)
|
||||||
|
|
||||||
|
:param rotation: Angle to rotate by (counterclockwise, radians)
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
self.rotation += rotation
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_bounds(self) -> numpy.ndarray or None:
|
||||||
|
"""
|
||||||
|
Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the
|
||||||
|
extent of the SubPattern in each dimension.
|
||||||
|
Returns None if the contained Pattern is empty.
|
||||||
|
|
||||||
|
:return: [[x_min, y_min], [x_max, y_max]] or None
|
||||||
|
"""
|
||||||
|
return self.as_pattern().get_bounds()
|
||||||
|
|
||||||
|
def scale_by(self, c: float) -> 'SubPattern':
|
||||||
|
"""
|
||||||
|
Scale the subpattern by a factor
|
||||||
|
|
||||||
|
:param c: scaling factor
|
||||||
|
"""
|
||||||
|
self.scale *= c
|
||||||
|
return self
|
||||||
|
|
41
masque/utils.py
Normal file
41
masque/utils.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Various helper functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any, Union, Tuple
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
# Type definitions
|
||||||
|
vector2 = Union[numpy.ndarray, Tuple[float, float]]
|
||||||
|
|
||||||
|
|
||||||
|
def is_scalar(var: Any) -> bool:
|
||||||
|
"""
|
||||||
|
Alias for 'not hasattr(var, "__len__")'
|
||||||
|
|
||||||
|
:param var: Checks if var has a length.
|
||||||
|
"""
|
||||||
|
return not hasattr(var, "__len__")
|
||||||
|
|
||||||
|
|
||||||
|
def get_bit(bit_string: Any, bit_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Returns true iff bit number 'bit_id' from the right of 'bitstring' is 1
|
||||||
|
|
||||||
|
:param bit_string: st
|
||||||
|
:param bit_id:
|
||||||
|
:return: value of the requested bit (bool)
|
||||||
|
"""
|
||||||
|
return bit_string & (1 << bit_id) != 0
|
||||||
|
|
||||||
|
|
||||||
|
def rotation_matrix_2d(theta: float) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
2D rotation matrix for rotating counterclockwise around the origin.
|
||||||
|
|
||||||
|
:param theta: Angle to rotate, in radians
|
||||||
|
:return: rotation matrix
|
||||||
|
"""
|
||||||
|
return numpy.array([[numpy.cos(theta), -numpy.sin(theta)],
|
||||||
|
[numpy.sin(theta), +numpy.cos(theta)]])
|
13
setup.py
Normal file
13
setup.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
setup(name='masque',
|
||||||
|
version='0.1',
|
||||||
|
description='Lithography mask library',
|
||||||
|
author='Jan Petykiewicz',
|
||||||
|
author_email='anewusername@gmail.com',
|
||||||
|
url='https://mpxd.net/gogs/jan/masque',
|
||||||
|
packages=['masque'],
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user