snapshot 2020-05-18 04:34:55.303040
This commit is contained in:
		
						commit
						e08c754b35
					
				
							
								
								
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,6 +1,19 @@
 | 
				
			|||||||
*.pyc
 | 
					*.pyc
 | 
				
			||||||
__pycache__
 | 
					__pycache__
 | 
				
			||||||
 | 
					
 | 
				
			||||||
*.idea
 | 
					*.idea
 | 
				
			||||||
 | 
					
 | 
				
			||||||
build/
 | 
					build/
 | 
				
			||||||
dist/
 | 
					dist/
 | 
				
			||||||
*.egg-info/
 | 
					*.egg-info/
 | 
				
			||||||
 | 
					.mypy_cache/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*.swp
 | 
				
			||||||
 | 
					*.swo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*.gds
 | 
				
			||||||
 | 
					*.gds.gz
 | 
				
			||||||
 | 
					*.svg
 | 
				
			||||||
 | 
					*.oas
 | 
				
			||||||
 | 
					*.dxf
 | 
				
			||||||
 | 
					*.dxf.gz
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,3 @@
 | 
				
			|||||||
include README.md
 | 
					include README.md
 | 
				
			||||||
include LICENSE.md
 | 
					include LICENSE.md
 | 
				
			||||||
 | 
					include masque/VERSION
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							@ -15,15 +15,15 @@ E-beam doses, and the ability to output to multiple formats.
 | 
				
			|||||||
Requirements:
 | 
					Requirements:
 | 
				
			||||||
* python >= 3.5 (written and tested with 3.6)
 | 
					* python >= 3.5 (written and tested with 3.6)
 | 
				
			||||||
* numpy
 | 
					* numpy
 | 
				
			||||||
* matplotlib (optional, used for visualization functions and text)
 | 
					* matplotlib (optional, used for `visualization` functions and `text`)
 | 
				
			||||||
* python-gdsii (optional, used for gdsii i/o)
 | 
					* python-gdsii (optional, used for `gdsii` i/o)
 | 
				
			||||||
* svgwrite (optional, used for svg output)
 | 
					* svgwrite (optional, used for `svg` output)
 | 
				
			||||||
* freetype (optional, used for text)
 | 
					* freetype (optional, used for `text`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Install with pip:
 | 
					Install with pip:
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
pip3 install masque
 | 
					pip3 install 'masque[visualization,gdsii,svg,text]'
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Alternatively, install from git
 | 
					Alternatively, install from git
 | 
				
			||||||
@ -33,11 +33,7 @@ pip3 install git+https://mpxd.net/code/jan/masque.git@release
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## TODO
 | 
					## TODO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Mirroring
 | 
					 | 
				
			||||||
* Polygon de-embedding
 | 
					* Polygon de-embedding
 | 
				
			||||||
 | 
					 | 
				
			||||||
### Maybe
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* Construct from bitmap
 | 
					* Construct from bitmap
 | 
				
			||||||
* Boolean operations on polygons (using pyclipper)
 | 
					* Boolean operations on polygons (using pyclipper)
 | 
				
			||||||
* Output to OASIS (using fatamorgana)
 | 
					* Output to OASIS (using fatamorgana)
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import numpy
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import masque
 | 
					import masque
 | 
				
			||||||
import masque.file.gdsii
 | 
					import masque.file.gdsii
 | 
				
			||||||
 | 
					import masque.file.dxf
 | 
				
			||||||
from masque import shapes
 | 
					from masque import shapes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -13,20 +14,22 @@ def main():
 | 
				
			|||||||
        pat.shapes.append(shapes.Arc(
 | 
					        pat.shapes.append(shapes.Arc(
 | 
				
			||||||
            radii=(rmin, rmin),
 | 
					            radii=(rmin, rmin),
 | 
				
			||||||
            width=0.1,
 | 
					            width=0.1,
 | 
				
			||||||
            angles=(-numpy.pi/4, numpy.pi/4)
 | 
					            angles=(-numpy.pi/4, numpy.pi/4),
 | 
				
			||||||
 | 
					            layer=(0, 0),
 | 
				
			||||||
        ))
 | 
					        ))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pat.labels.append(masque.Label(string='grating centerline', offset=(1, 0), layer=(1, 2)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pat.scale_by(1000)
 | 
					    pat.scale_by(1000)
 | 
				
			||||||
#    pat.visualize()
 | 
					#    pat.visualize()
 | 
				
			||||||
    pat2 = masque.Pattern(name='p2')
 | 
					    pat2 = pat.copy()
 | 
				
			||||||
    pat2.name = 'ellip_grating'
 | 
					    pat2.name = 'grating2'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pat2.subpatterns += [
 | 
					    masque.file.gdsii.writefile((pat, pat2), 'out.gds.gz', 1e-9, 1e-3)
 | 
				
			||||||
        masque.SubPattern(pattern=pat, offset=(20e3, 0)),
 | 
					 | 
				
			||||||
        masque.SubPattern(pattern=pat, offset=(0, 20e3)),
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    masque.file.gdsii.write_dose2dtype((pat, pat2, pat2.copy(), pat2.copy()), 'out.gds', 1e-9, 1e-3)
 | 
					    masque.file.dxf.writefile(pat, 'out.dxf.gz')
 | 
				
			||||||
 | 
					    dxf, info = masque.file.dxf.readfile('out.dxf.gz')
 | 
				
			||||||
 | 
					    masque.file.dxf.writefile(dxf, 'reout.dxf.gz')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										96
									
								
								examples/test_rep.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								examples/test_rep.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					import numpy
 | 
				
			||||||
 | 
					from numpy import pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import masque
 | 
				
			||||||
 | 
					import masque.file.gdsii
 | 
				
			||||||
 | 
					import masque.file.dxf
 | 
				
			||||||
 | 
					from masque import shapes, Pattern, SubPattern, GridRepetition
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from pprint import pprint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    pat = masque.Pattern(name='ellip_grating')
 | 
				
			||||||
 | 
					    for rmin in numpy.arange(10, 15, 0.5):
 | 
				
			||||||
 | 
					        pat.shapes.append(shapes.Arc(
 | 
				
			||||||
 | 
					            radii=(rmin, rmin),
 | 
				
			||||||
 | 
					            width=0.1,
 | 
				
			||||||
 | 
					            angles=(0*-numpy.pi/4, numpy.pi/4)
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pat.scale_by(1000)
 | 
				
			||||||
 | 
					    pat.visualize()
 | 
				
			||||||
 | 
					    pat2 = pat.copy()
 | 
				
			||||||
 | 
					    pat2.name = 'grating2'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pat3 = Pattern('sref_test')
 | 
				
			||||||
 | 
					    pat3.subpatterns = [
 | 
				
			||||||
 | 
					        SubPattern(pat, offset=(1e5, 3e5)),
 | 
				
			||||||
 | 
					        SubPattern(pat, offset=(2e5, 3e5), rotation=pi/3),
 | 
				
			||||||
 | 
					        SubPattern(pat, offset=(3e5, 3e5), rotation=pi/2),
 | 
				
			||||||
 | 
					        SubPattern(pat, offset=(4e5, 3e5), rotation=pi),
 | 
				
			||||||
 | 
					        SubPattern(pat, offset=(5e5, 3e5), rotation=3*pi/2),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, False), offset=(1e5, 4e5)),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(False, True), offset=(1e5, 5e5)),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, True), offset=(1e5, 6e5)),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi),
 | 
				
			||||||
 | 
					        SubPattern(pat, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pprint(pat3)
 | 
				
			||||||
 | 
					    pprint(pat3.subpatterns)
 | 
				
			||||||
 | 
					    pprint(pat.shapes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    args = {
 | 
				
			||||||
 | 
					            'pattern': pat,
 | 
				
			||||||
 | 
					            'a_vector': [1e4, 0],
 | 
				
			||||||
 | 
					            'b_vector': [0, 1.5e4],
 | 
				
			||||||
 | 
					            'a_count': 3,
 | 
				
			||||||
 | 
					            'b_count': 2,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					    pat4 = Pattern('aref_test')
 | 
				
			||||||
 | 
					    pat4.subpatterns = [
 | 
				
			||||||
 | 
					        GridRepetition(**args, offset=(1e5, 3e5)),
 | 
				
			||||||
 | 
					        GridRepetition(**args, offset=(2e5, 3e5), rotation=pi/3),
 | 
				
			||||||
 | 
					        GridRepetition(**args, offset=(3e5, 3e5), rotation=pi/2),
 | 
				
			||||||
 | 
					        GridRepetition(**args, offset=(4e5, 3e5), rotation=pi),
 | 
				
			||||||
 | 
					        GridRepetition(**args, offset=(5e5, 3e5), rotation=3*pi/2),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, False), offset=(1e5, 4e5)),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(False, True), offset=(1e5, 5e5)),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, True), offset=(1e5, 6e5)),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi),
 | 
				
			||||||
 | 
					        GridRepetition(**args, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    masque.file.gdsii.writefile((pat, pat2, pat3, pat4), 'rep.gds.gz', 1e-9, 1e-3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    cells = list(masque.file.gdsii.readfile('rep.gds.gz')[0].values())
 | 
				
			||||||
 | 
					    masque.file.gdsii.writefile(cells, 'rerep.gds.gz', 1e-9, 1e-3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    masque.file.dxf.writefile(pat4, 'rep.dxf.gz')
 | 
				
			||||||
 | 
					    dxf, info = masque.file.dxf.readfile('rep.dxf.gz')
 | 
				
			||||||
 | 
					    masque.file.dxf.writefile(dxf, 'rerep.dxf.gz')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
							
								
								
									
										1
									
								
								masque/VERSION
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								masque/VERSION
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					1.3
 | 
				
			||||||
@ -6,31 +6,38 @@
 | 
				
			|||||||
  with some vectorized element types (eg. circles, not just polygons), better support for
 | 
					  with some vectorized element types (eg. circles, not just polygons), better support for
 | 
				
			||||||
  E-beam doses, and the ability to output to multiple formats.
 | 
					  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
 | 
					 `Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape`
 | 
				
			||||||
  objects and a list of SubPattern objects.
 | 
					  objects, a list of `Label` objects, and a list of references to other `Patterns` (using
 | 
				
			||||||
 | 
					  `SubPattern` and `GridRepetition`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 SubPattern provides basic support for nesting Pattern objects within each other, by adding
 | 
					 `SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding
 | 
				
			||||||
  offset, rotation, scaling, and other such properties to a Pattern reference.
 | 
					  offset, rotation, scaling, and other such properties to a Pattern reference.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 `GridRepetition` provides support for nesting regular arrays of `Pattern` objects.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Note that the methods for these classes try to avoid copying wherever possible, so unless
 | 
					 Note that the methods for these classes try to avoid copying wherever possible, so unless
 | 
				
			||||||
  otherwise noted, assume that arguments are stored by-reference.
 | 
					  otherwise noted, assume that arguments are stored by-reference.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 Dependencies:
 | 
					 Dependencies:
 | 
				
			||||||
    - numpy
 | 
					    - `numpy`
 | 
				
			||||||
    - matplotlib    [Pattern.visualize(...)]
 | 
					    - `matplotlib`    [Pattern.visualize(...)]
 | 
				
			||||||
    - python-gdsii  [masque.file.gdsii]
 | 
					    - `python-gdsii`  [masque.file.gdsii]
 | 
				
			||||||
    - svgwrite      [masque.file.svg]
 | 
					    - `svgwrite`      [masque.file.svg]
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .error import PatternError
 | 
					import pathlib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .error import PatternError, PatternLockedError
 | 
				
			||||||
from .shapes import Shape
 | 
					from .shapes import Shape
 | 
				
			||||||
from .label import Label
 | 
					from .label import Label
 | 
				
			||||||
from .subpattern import SubPattern
 | 
					from .subpattern import SubPattern, subpattern_t
 | 
				
			||||||
from .repetition import GridRepetition
 | 
					from .repetition import GridRepetition
 | 
				
			||||||
from .pattern import Pattern
 | 
					from .pattern import Pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					__author__ = 'Jan Petykiewicz'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
version = '0.5'
 | 
					with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f:
 | 
				
			||||||
 | 
					    __version__ = f.read().strip()
 | 
				
			||||||
 | 
					version = __version__
 | 
				
			||||||
 | 
				
			|||||||
@ -7,3 +7,11 @@ class PatternError(Exception):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return repr(self.value)
 | 
					        return repr(self.value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PatternLockedError(PatternError):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Exception raised when trying to modify a locked pattern
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        PatternError.__init__(self, 'Tried to modify a locked Pattern, subpattern, or shape')
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										382
									
								
								masque/file/dxf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								masque/file/dxf.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,382 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					DXF file format readers and writers
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					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 ezdxf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .utils import mangle_name, make_dose_table
 | 
				
			||||||
 | 
					from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t
 | 
				
			||||||
 | 
					from ..shapes import Polygon, Path
 | 
				
			||||||
 | 
					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('DXF support is experimental and only slightly tested!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DEFAULT_LAYER = 'DEFAULT'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def write(pattern: Pattern,
 | 
				
			||||||
 | 
					          stream: io.TextIOBase,
 | 
				
			||||||
 | 
					          modify_originals: bool = False,
 | 
				
			||||||
 | 
					          dxf_version='AC1024',
 | 
				
			||||||
 | 
					          disambiguate_func: Callable[[Iterable[Pattern]], None] = None):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
 | 
				
			||||||
 | 
					     into polygons, and then writing patterns as DXF `Block`s, polygons as `LWPolyline`s,
 | 
				
			||||||
 | 
					     and subpatterns as `Insert`s.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The top level pattern's name is not written to the DXF file. Nested patterns keep their
 | 
				
			||||||
 | 
					     names.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Layer numbers are translated as follows:
 | 
				
			||||||
 | 
					        int: 1 -> '1'
 | 
				
			||||||
 | 
					        tuple: (1, 2) -> '1.2'
 | 
				
			||||||
 | 
					        str: '1.2' -> '1.2' (no change)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    It is often a good idea to run `pattern.subpatternize()` prior to calling this function,
 | 
				
			||||||
 | 
					     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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Only `GridRepetition` objects with manhattan basis vectors are preserved as arrays. Since DXF
 | 
				
			||||||
 | 
					     rotations apply to basis vectors while `masque`'s rotations do not, the basis vectors of an
 | 
				
			||||||
 | 
					     array with rotated instances must be manhattan _after_ having a compensating rotation applied.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        patterns: A Pattern or list of patterns to write to the stream.
 | 
				
			||||||
 | 
					        stream: Stream object to write to.
 | 
				
			||||||
 | 
					        modify_original: If `True`, the original pattern is modified as part of the writing
 | 
				
			||||||
 | 
					            process. Otherwise, a copy is made and `deepunlock()`-ed.
 | 
				
			||||||
 | 
					            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`.
 | 
				
			||||||
 | 
					            WARNING: No additional error checking is performed on the results.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    #TODO consider supporting DXF arcs?
 | 
				
			||||||
 | 
					    if disambiguate_func is None:
 | 
				
			||||||
 | 
					        disambiguate_func = disambiguate_pattern_names
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not modify_originals:
 | 
				
			||||||
 | 
					        pattern = pattern.deepcopy().deepunlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Get a dict of id(pattern) -> pattern
 | 
				
			||||||
 | 
					    patterns_by_id = pattern.referenced_patterns_by_id()
 | 
				
			||||||
 | 
					    disambiguate_func(patterns_by_id.values())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create library
 | 
				
			||||||
 | 
					    lib = ezdxf.new(dxf_version, setup=True)
 | 
				
			||||||
 | 
					    msp = lib.modelspace()
 | 
				
			||||||
 | 
					    _shapes_to_elements(msp, pattern.shapes)
 | 
				
			||||||
 | 
					    _labels_to_texts(msp, pattern.labels)
 | 
				
			||||||
 | 
					    _subpatterns_to_refs(msp, pattern.subpatterns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Now create a block for each referenced pattern, and add in any shapes
 | 
				
			||||||
 | 
					    for pat in patterns_by_id.values():
 | 
				
			||||||
 | 
					        assert(pat is not None)
 | 
				
			||||||
 | 
					        block = lib.blocks.new(name=pat.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _shapes_to_elements(block, pat.shapes)
 | 
				
			||||||
 | 
					        _labels_to_texts(block, pat.labels)
 | 
				
			||||||
 | 
					        _subpatterns_to_refs(block, pat.subpatterns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lib.write(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def writefile(pattern: Pattern,
 | 
				
			||||||
 | 
					              filename: Union[str, pathlib.Path],
 | 
				
			||||||
 | 
					              *args,
 | 
				
			||||||
 | 
					              **kwargs,
 | 
				
			||||||
 | 
					              ):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Wrapper for `dxf.write()` that takes a filename or path instead of a stream.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Will automatically compress the file if it has a .gz suffix.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        pattern: `Pattern` to save
 | 
				
			||||||
 | 
					        filename: Filename to save to.
 | 
				
			||||||
 | 
					        *args: passed to `dxf.write`
 | 
				
			||||||
 | 
					        **kwargs: passed to `dxf.write`
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    path = pathlib.Path(filename)
 | 
				
			||||||
 | 
					    if path.suffix == '.gz':
 | 
				
			||||||
 | 
					        open_func: Callable = gzip.open
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        open_func = open
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with open_func(path, mode='wt') as stream:
 | 
				
			||||||
 | 
					        results = write(pattern, stream, *args, **kwargs)
 | 
				
			||||||
 | 
					    return results
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def readfile(filename: Union[str, pathlib.Path],
 | 
				
			||||||
 | 
					             *args,
 | 
				
			||||||
 | 
					             **kwargs,
 | 
				
			||||||
 | 
					             ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Wrapper for `dxf.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 `dxf.read`
 | 
				
			||||||
 | 
					        **kwargs: passed to `dxf.read`
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    path = pathlib.Path(filename)
 | 
				
			||||||
 | 
					    if path.suffix == '.gz':
 | 
				
			||||||
 | 
					        open_func: Callable = gzip.open
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        open_func = open
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with open_func(path, mode='rt') as stream:
 | 
				
			||||||
 | 
					        results = read(stream, *args, **kwargs)
 | 
				
			||||||
 | 
					    return results
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read(stream: io.TextIOBase,
 | 
				
			||||||
 | 
					         clean_vertices: bool = True,
 | 
				
			||||||
 | 
					         ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Read a dxf file and translate it into a dict of `Pattern` objects. DXF `Block`s are
 | 
				
			||||||
 | 
					     translated into `Pattern` objects; `LWPolyline`s are translated into polygons, and `Insert`s
 | 
				
			||||||
 | 
					     are translated into `SubPattern` objects.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If an object has no layer it is set to this module's `DEFAULT_LAYER` ("DEFAULT").
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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:
 | 
				
			||||||
 | 
					        - Top level pattern
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    lib = ezdxf.read(stream)
 | 
				
			||||||
 | 
					    msp = lib.modelspace()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pat = _read_block(msp, clean_vertices)
 | 
				
			||||||
 | 
					    patterns = [pat] + [_read_block(bb, clean_vertices) for bb in lib.blocks if bb.name != '*Model_Space']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
 | 
				
			||||||
 | 
					    #  according to the subpattern.identifier (which is deleted after use).
 | 
				
			||||||
 | 
					    patterns_dict = dict(((p.name, p) for p in patterns))
 | 
				
			||||||
 | 
					    for p in patterns_dict.values():
 | 
				
			||||||
 | 
					        for sp in p.subpatterns:
 | 
				
			||||||
 | 
					            sp.pattern = patterns_dict[sp.identifier[0]]
 | 
				
			||||||
 | 
					            del sp.identifier
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    library_info = {
 | 
				
			||||||
 | 
					        'layers': [ll.dxfattribs() for ll in lib.layers]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return pat, library_info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _read_block(block, clean_vertices):
 | 
				
			||||||
 | 
					    pat = Pattern(block.name)
 | 
				
			||||||
 | 
					    for element in block:
 | 
				
			||||||
 | 
					        eltype = element.dxftype()
 | 
				
			||||||
 | 
					        if eltype in ('POLYLINE', 'LWPOLYLINE'):
 | 
				
			||||||
 | 
					            if eltype == 'LWPOLYLINE':
 | 
				
			||||||
 | 
					                points = numpy.array(element.lwpoints)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                points = numpy.array(element.points)
 | 
				
			||||||
 | 
					            attr = element.dxfattribs()
 | 
				
			||||||
 | 
					            args = {'layer': attr.get('layer', DEFAULT_LAYER),
 | 
				
			||||||
 | 
					                   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if points.shape[1] == 2:
 | 
				
			||||||
 | 
					                shape = Polygon(**args)
 | 
				
			||||||
 | 
					            elif points.shape[1] > 2:
 | 
				
			||||||
 | 
					                if (points[0, 2] != points[:, 2]).any():
 | 
				
			||||||
 | 
					                    raise PatternError('PolyLine has non-constant width (not yet representable in masque!)')
 | 
				
			||||||
 | 
					                elif points.shape[1] == 4 and (points[:, 3] != 0).any():
 | 
				
			||||||
 | 
					                    raise PatternError('LWPolyLine has bulge (not yet representable in masque!)')
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    width = points[0, 2]
 | 
				
			||||||
 | 
					                    if width == 0:
 | 
				
			||||||
 | 
					                        width = attr.get('const_width', 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if width == 0 and numpy.array_equal(points[0], points[-1]):
 | 
				
			||||||
 | 
					                        shape = Polygon(**args, vertices=points[:-1, :2])
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        shape = Path(**args, width=width, vertices=points[:, :2])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if clean_vertices:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    shape.clean_vertices()
 | 
				
			||||||
 | 
					                except PatternError:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            pat.shapes.append(shape)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif eltype in ('TEXT',):
 | 
				
			||||||
 | 
					            args = {'offset': element.get_pos()[1][:2],
 | 
				
			||||||
 | 
					                    'layer': element.dxfattribs().get('layer', DEFAULT_LAYER),
 | 
				
			||||||
 | 
					                   }
 | 
				
			||||||
 | 
					            string = element.dxfattribs().get('text', '')
 | 
				
			||||||
 | 
					            height = element.dxfattribs().get('height', 0)
 | 
				
			||||||
 | 
					            if height != 0:
 | 
				
			||||||
 | 
					                logger.warning('Interpreting DXF TEXT as a label despite nonzero height. '
 | 
				
			||||||
 | 
					                               'This could be changed in the future by setting a font path in the masque DXF code.')
 | 
				
			||||||
 | 
					            pat.labels.append(Label(string=string, **args))
 | 
				
			||||||
 | 
					#            else:
 | 
				
			||||||
 | 
					#                pat.shapes.append(Text(string=string, height=height, font_path=????))
 | 
				
			||||||
 | 
					        elif eltype in ('INSERT',):
 | 
				
			||||||
 | 
					            attr = element.dxfattribs()
 | 
				
			||||||
 | 
					            xscale = attr.get('xscale', 1)
 | 
				
			||||||
 | 
					            yscale = attr.get('yscale', 1)
 | 
				
			||||||
 | 
					            if abs(xscale) != abs(yscale):
 | 
				
			||||||
 | 
					                logger.warning('Masque does not support per-axis scaling; using x-scaling only!')
 | 
				
			||||||
 | 
					            scale = abs(xscale)
 | 
				
			||||||
 | 
					            mirrored = (yscale < 0, xscale < 0)
 | 
				
			||||||
 | 
					            rotation = attr.get('rotation', 0) * pi/180
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            offset = attr.get('insert', (0, 0, 0))[:2]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            args = {
 | 
				
			||||||
 | 
					                'offset': offset,
 | 
				
			||||||
 | 
					                'scale': scale,
 | 
				
			||||||
 | 
					                'mirrored': mirrored,
 | 
				
			||||||
 | 
					                'rotation': rotation,
 | 
				
			||||||
 | 
					                'pattern': None,
 | 
				
			||||||
 | 
					                'identifier': (attr.get('name', None),),
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if 'column_count' in attr:
 | 
				
			||||||
 | 
					                args['a_vector'] = (attr['column_spacing'], 0)
 | 
				
			||||||
 | 
					                args['b_vector'] = (0, attr['row_spacing'])
 | 
				
			||||||
 | 
					                args['a_count'] = attr['column_count']
 | 
				
			||||||
 | 
					                args['b_count'] = attr['row_count']
 | 
				
			||||||
 | 
					                pat.subpatterns.append(GridRepetition(**args))
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                pat.subpatterns.append(SubPattern(**args))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')
 | 
				
			||||||
 | 
					    return pat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],
 | 
				
			||||||
 | 
					                         subpatterns: List[subpattern_t]):
 | 
				
			||||||
 | 
					    for subpat in subpatterns:
 | 
				
			||||||
 | 
					        if subpat.pattern is None:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        encoded_name = subpat.pattern.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        rotation = (subpat.rotation * 180 / numpy.pi) % 360
 | 
				
			||||||
 | 
					        attribs = {
 | 
				
			||||||
 | 
					            'xscale': subpat.scale * (-1 if subpat.mirrored[1] else 1),
 | 
				
			||||||
 | 
					            'yscale': subpat.scale * (-1 if subpat.mirrored[0] else 1),
 | 
				
			||||||
 | 
					            'rotation': rotation,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if isinstance(subpat, GridRepetition):
 | 
				
			||||||
 | 
					            a = subpat.a_vector
 | 
				
			||||||
 | 
					            b = subpat.b_vector if subpat.b_vector is not None else numpy.zeros(2)
 | 
				
			||||||
 | 
					            rotated_a = rotation_matrix_2d(-subpat.rotation) @ a
 | 
				
			||||||
 | 
					            rotated_b = rotation_matrix_2d(-subpat.rotation) @ b
 | 
				
			||||||
 | 
					            if rotated_a[1] == 0 and rotated_b[0] == 0:
 | 
				
			||||||
 | 
					                attribs['column_count'] = subpat.a_count
 | 
				
			||||||
 | 
					                attribs['row_count'] = subpat.b_count
 | 
				
			||||||
 | 
					                attribs['column_spacing'] = rotated_a[0]
 | 
				
			||||||
 | 
					                attribs['row_spacing'] = rotated_b[1]
 | 
				
			||||||
 | 
					                block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
 | 
				
			||||||
 | 
					            elif rotated_a[0] == 0 and rotated_b[1] == 0:
 | 
				
			||||||
 | 
					                attribs['column_count'] = subpat.b_count
 | 
				
			||||||
 | 
					                attribs['row_count'] = subpat.a_count
 | 
				
			||||||
 | 
					                attribs['column_spacing'] = rotated_b[0]
 | 
				
			||||||
 | 
					                attribs['row_spacing'] = rotated_a[1]
 | 
				
			||||||
 | 
					                block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                #NOTE: We could still do non-manhattan (but still orthogonal) grids by getting
 | 
				
			||||||
 | 
					                #       creative with counter-rotated nested patterns, but probably not worth it.
 | 
				
			||||||
 | 
					                # Instead, just break appart the grid into individual elements:
 | 
				
			||||||
 | 
					                for aa in numpy.arange(subpat.a_count):
 | 
				
			||||||
 | 
					                    for bb in numpy.arange(subpat.b_count):
 | 
				
			||||||
 | 
					                        block.add_blockref(encoded_name, subpat.offset + aa * a + bb * b, dxfattribs=attribs)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _shapes_to_elements(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],
 | 
				
			||||||
 | 
					                        shapes: List[Shape],
 | 
				
			||||||
 | 
					                        polygonize_paths: bool = False):
 | 
				
			||||||
 | 
					    # Add `LWPolyline`s for each shape.
 | 
				
			||||||
 | 
					    #   Could set do paths with width setting, but need to consider endcaps.
 | 
				
			||||||
 | 
					    for shape in shapes:
 | 
				
			||||||
 | 
					        attribs = {'layer': _mlayer2dxf(shape.layer)}
 | 
				
			||||||
 | 
					        for polygon in shape.to_polygons():
 | 
				
			||||||
 | 
					            xy_open = polygon.vertices + polygon.offset
 | 
				
			||||||
 | 
					            xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
 | 
				
			||||||
 | 
					            block.add_lwpolyline(xy_closed, dxfattribs=attribs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _labels_to_texts(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],
 | 
				
			||||||
 | 
					                     labels: List[Label]):
 | 
				
			||||||
 | 
					    for label in labels:
 | 
				
			||||||
 | 
					        attribs = {'layer': _mlayer2dxf(label.layer)}
 | 
				
			||||||
 | 
					        xy = label.offset
 | 
				
			||||||
 | 
					        block.add_text(label.string, dxfattribs=attribs).set_pos(xy, align='BOTTOM_LEFT')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _mlayer2dxf(layer: layer_t) -> str:
 | 
				
			||||||
 | 
					    if isinstance(layer, str):
 | 
				
			||||||
 | 
					        return layer
 | 
				
			||||||
 | 
					    if isinstance(layer, int):
 | 
				
			||||||
 | 
					        return str(layer)
 | 
				
			||||||
 | 
					    if isinstance(layer, tuple):
 | 
				
			||||||
 | 
					        return f'{layer[0]}.{layer[1]}'
 | 
				
			||||||
 | 
					    raise PatternError(f'Unknown layer type: {layer} ({type(layer)})')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def disambiguate_pattern_names(patterns,
 | 
				
			||||||
 | 
					                               max_name_length: int = 32,
 | 
				
			||||||
 | 
					                               suffix_length: int = 6,
 | 
				
			||||||
 | 
					                               dup_warn_filter: Callable[[str,], bool] = None,      # If returns False, don't warn about this name
 | 
				
			||||||
 | 
					                               ):
 | 
				
			||||||
 | 
					    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,\n originally "{}"'.format(pat.name))
 | 
				
			||||||
 | 
					        if len(suffixed_name) > max_name_length:
 | 
				
			||||||
 | 
					            raise PatternError('Pattern name "{!r}" length > {} after encode,\n originally "{}"'.format(suffixed_name, max_name_length, pat.name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pat.name = suffixed_name
 | 
				
			||||||
 | 
					        used_names.append(suffixed_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1,226 +1,188 @@
 | 
				
			|||||||
"""
 | 
					"""
 | 
				
			||||||
GDSII file format readers and writers
 | 
					GDSII file format readers and writers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Note that GDSII references follow the same convention as `masque`,
 | 
				
			||||||
 | 
					  with this order of operations:
 | 
				
			||||||
 | 
					   1. Mirroring
 | 
				
			||||||
 | 
					   2. Rotation
 | 
				
			||||||
 | 
					   3. Scaling
 | 
				
			||||||
 | 
					   4. Offset and array expansion (no mirroring/rotation/scaling applied to offsets)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Scaling, rotation, and mirroring apply to individual instances, not grid
 | 
				
			||||||
 | 
					   vectors or offsets.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import io
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
 | 
					import numpy
 | 
				
			||||||
 | 
					import base64
 | 
				
			||||||
 | 
					import struct
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import pathlib
 | 
				
			||||||
 | 
					import gzip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# python-gdsii
 | 
					# python-gdsii
 | 
				
			||||||
import gdsii.library
 | 
					import gdsii.library
 | 
				
			||||||
import gdsii.structure
 | 
					import gdsii.structure
 | 
				
			||||||
import gdsii.elements
 | 
					import gdsii.elements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from typing import List, Any, Dict, Tuple
 | 
					from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose
 | 
				
			||||||
import re
 | 
					from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t
 | 
				
			||||||
import numpy
 | 
					 | 
				
			||||||
import base64
 | 
					 | 
				
			||||||
import struct
 | 
					 | 
				
			||||||
import logging
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from .utils import mangle_name, make_dose_table
 | 
					 | 
				
			||||||
from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape
 | 
					 | 
				
			||||||
from ..shapes import Polygon, Path
 | 
					from ..shapes import Polygon, Path
 | 
				
			||||||
from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar
 | 
					from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
 | 
				
			||||||
from ..utils import remove_colinear_vertices
 | 
					from ..utils import remove_colinear_vertices, normalize_mirror
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#TODO absolute positioning
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def write(patterns: Pattern or List[Pattern],
 | 
					path_cap_map = {
 | 
				
			||||||
          filename: str,
 | 
					                None: Path.Cap.Flush,
 | 
				
			||||||
 | 
					                0: Path.Cap.Flush,
 | 
				
			||||||
 | 
					                1: Path.Cap.Circle,
 | 
				
			||||||
 | 
					                2: Path.Cap.Square,
 | 
				
			||||||
 | 
					                4: Path.Cap.SquareCustom,
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def write(patterns: Union[Pattern, List[Pattern]],
 | 
				
			||||||
 | 
					          stream: io.BufferedIOBase,
 | 
				
			||||||
          meters_per_unit: float,
 | 
					          meters_per_unit: float,
 | 
				
			||||||
          logical_units_per_unit: float = 1,
 | 
					          logical_units_per_unit: float = 1,
 | 
				
			||||||
          library_name: str = 'masque-gdsii-write'):
 | 
					          library_name: str = 'masque-gdsii-write',
 | 
				
			||||||
 | 
					          modify_originals: bool = False,
 | 
				
			||||||
 | 
					          disambiguate_func: Callable[[Iterable[Pattern]], None] = None):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Write a Pattern or list of patterns to a GDSII file, by first calling
 | 
					    Write a `Pattern` or list of patterns to a GDSII file, by first calling
 | 
				
			||||||
     .polygonize() to change the shapes into polygons, and then writing patterns
 | 
					     `.polygonize()` to change the shapes into polygons, and then writing patterns
 | 
				
			||||||
     as GDSII structures, polygons as boundary elements, and subpatterns as structure
 | 
					     as GDSII structures, polygons as boundary elements, and subpatterns as structure
 | 
				
			||||||
     references (sref).
 | 
					     references (sref).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
     For each shape,
 | 
					     For each shape,
 | 
				
			||||||
        layer is chosen to be equal to shape.layer if it is an int,
 | 
					        layer is chosen to be equal to `shape.layer` if it is an int,
 | 
				
			||||||
            or shape.layer[0] if it is a tuple
 | 
					            or `shape.layer[0]` if it is a tuple
 | 
				
			||||||
        datatype is chosen to be shape.layer[1] if available,
 | 
					        datatype is chosen to be `shape.layer[1]` if available,
 | 
				
			||||||
            otherwise 0
 | 
					            otherwise `0`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    It is often a good idea to run pattern.subpatternize() prior to calling this function,
 | 
					    If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
 | 
				
			||||||
     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.
 | 
					     prior to calling this function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param patterns: A Pattern or list of patterns to write to file. Modified by this function.
 | 
					    Args:
 | 
				
			||||||
    :param filename: Filename to write to.
 | 
					        patterns: A Pattern or list of patterns to write to the stream.
 | 
				
			||||||
    :param meters_per_unit: Written into the GDSII file, meters per (database) length unit.
 | 
					        stream: Stream object to write to.
 | 
				
			||||||
        All distances are assumed to be an integer multiple of this unit, and are stored as such.
 | 
					        meters_per_unit: Written into the GDSII file, meters per (database) length unit.
 | 
				
			||||||
    :param logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
 | 
					            All distances are assumed to be an integer multiple of this unit, and are stored as such.
 | 
				
			||||||
        "logical" unit which is different from the "database" unit, for display purposes.
 | 
					        logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
 | 
				
			||||||
        Default 1.
 | 
					            "logical" unit which is different from the "database" unit, for display purposes.
 | 
				
			||||||
    :param library_name: Library name written into the GDSII file.
 | 
					            Default `1`.
 | 
				
			||||||
        Default 'masque-gdsii-write'.
 | 
					        library_name: Library name written into the GDSII file.
 | 
				
			||||||
 | 
					            Default 'masque-gdsii-write'.
 | 
				
			||||||
 | 
					        modify_originals: If `True`, the original pattern is modified as part of the writing
 | 
				
			||||||
 | 
					            process. Otherwise, a copy is made and `deepunlock()`-ed.
 | 
				
			||||||
 | 
					            Default `False`.
 | 
				
			||||||
 | 
					        disambiguate_func: Function which takes a list of patterns and alters them
 | 
				
			||||||
 | 
					            to make their names valid and unique. Default is `disambiguate_pattern_names`, which
 | 
				
			||||||
 | 
					            attempts to adhere to the GDSII standard as well as possible.
 | 
				
			||||||
 | 
					            WARNING: No additional error checking is performed on the results.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    if isinstance(patterns, Pattern):
 | 
				
			||||||
 | 
					        patterns = [patterns]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if disambiguate_func is None:
 | 
				
			||||||
 | 
					        disambiguate_func = disambiguate_pattern_names
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not modify_originals:
 | 
				
			||||||
 | 
					        patterns = [p.deepunlock() for p in copy.deepcopy(patterns)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Create library
 | 
					    # Create library
 | 
				
			||||||
    lib = gdsii.library.Library(version=600,
 | 
					    lib = gdsii.library.Library(version=600,
 | 
				
			||||||
                                name=library_name.encode('ASCII'),
 | 
					                                name=library_name.encode('ASCII'),
 | 
				
			||||||
                                logical_unit=logical_units_per_unit,
 | 
					                                logical_unit=logical_units_per_unit,
 | 
				
			||||||
                                physical_unit=meters_per_unit)
 | 
					                                physical_unit=meters_per_unit)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if isinstance(patterns, Pattern):
 | 
					 | 
				
			||||||
        patterns = [patterns]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Get a dict of id(pattern) -> pattern
 | 
					    # Get a dict of id(pattern) -> pattern
 | 
				
			||||||
    patterns_by_id = {id(pattern): pattern for pattern in patterns}
 | 
					    patterns_by_id = {id(pattern): pattern for pattern in patterns}
 | 
				
			||||||
    for pattern in patterns:
 | 
					    for pattern in patterns:
 | 
				
			||||||
        patterns_by_id.update(pattern.referenced_patterns_by_id())
 | 
					        for i, p in pattern.referenced_patterns_by_id().items():
 | 
				
			||||||
 | 
					            patterns_by_id[i] = p
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _disambiguate_pattern_names(patterns_by_id.values())
 | 
					    disambiguate_func(patterns_by_id.values())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Now create a structure for each pattern, and add in any Boundary and SREF elements
 | 
					    # Now create a structure for each pattern, and add in any Boundary and SREF elements
 | 
				
			||||||
    for pat in patterns_by_id.values():
 | 
					    for pat in patterns_by_id.values():
 | 
				
			||||||
        structure = gdsii.structure.Structure(name=pat.name)
 | 
					        structure = gdsii.structure.Structure(name=pat.name)
 | 
				
			||||||
        lib.append(structure)
 | 
					        lib.append(structure)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Add a Boundary element for each shape
 | 
					        structure += _shapes_to_elements(pat.shapes)
 | 
				
			||||||
        structure += _shapes_to_boundaries(pat.shapes)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        structure += _labels_to_texts(pat.labels)
 | 
					        structure += _labels_to_texts(pat.labels)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Add an SREF / AREF for each subpattern entry
 | 
					 | 
				
			||||||
        structure += _subpatterns_to_refs(pat.subpatterns)
 | 
					        structure += _subpatterns_to_refs(pat.subpatterns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with open(filename, mode='wb') as stream:
 | 
					    lib.save(stream)
 | 
				
			||||||
        lib.save(stream)
 | 
					    return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def write_dose2dtype(patterns: Pattern or List[Pattern],
 | 
					def writefile(patterns: Union[List[Pattern], Pattern],
 | 
				
			||||||
                     filename: str,
 | 
					              filename: Union[str, pathlib.Path],
 | 
				
			||||||
                     meters_per_unit: float,
 | 
					              *args,
 | 
				
			||||||
                     *args,
 | 
					              **kwargs,
 | 
				
			||||||
                     **kwargs,
 | 
					              ):
 | 
				
			||||||
                     ) -> List[float]:
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Write a Pattern or list of patterns to a GDSII file, by first calling
 | 
					    Wrapper for `gdsii.write()` that takes a filename or path instead of a stream.
 | 
				
			||||||
     .polygonize() to change the shapes into polygons, and then writing patterns
 | 
					 | 
				
			||||||
     as GDSII structures, polygons as boundary elements, and subpatterns as structure
 | 
					 | 
				
			||||||
     references (sref).
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
     For each shape,
 | 
					    Will automatically compress the file if it has a .gz suffix.
 | 
				
			||||||
        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 arbitrarily, based on calcualted dose for each shape.
 | 
					 | 
				
			||||||
            Shapes with equal calcualted dose will have the same datatype.
 | 
					 | 
				
			||||||
            A list of doses is retured, providing a mapping between datatype
 | 
					 | 
				
			||||||
            (list index) and dose (list entry).
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Note that this function modifies the Pattern(s).
 | 
					    Args:
 | 
				
			||||||
 | 
					        patterns: `Pattern` or list of patterns to save
 | 
				
			||||||
    It is often a good idea to run pattern.subpatternize() prior to calling this function,
 | 
					        filename: Filename to save to.
 | 
				
			||||||
     especially if calling .polygonize() will result in very many vertices.
 | 
					        *args: passed to `gdsii.write`
 | 
				
			||||||
 | 
					        **kwargs: passed to `gdsii.write`
 | 
				
			||||||
    If you want pattern polygonized with non-default arguments, just call pattern.polygonize()
 | 
					 | 
				
			||||||
     prior to calling this function.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :param patterns: A Pattern or list of patterns 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 (database) length unit.
 | 
					 | 
				
			||||||
        All distances are assumed to be an integer multiple of this unit, and are stored as such.
 | 
					 | 
				
			||||||
    :param args: passed to masque.file.gdsii.write().
 | 
					 | 
				
			||||||
    :param kwargs: passed to masque.file.gdsii.write().
 | 
					 | 
				
			||||||
    :returns: A list of doses, providing a mapping between datatype (int, list index)
 | 
					 | 
				
			||||||
                and dose (float, list entry).
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    patterns, dose_vals = dose2dtype(patterns)
 | 
					    path = pathlib.Path(filename)
 | 
				
			||||||
    write(patterns, filename, meters_per_unit, *args, **kwargs)
 | 
					    if path.suffix == '.gz':
 | 
				
			||||||
    return dose_vals
 | 
					        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 dose2dtype(patterns: Pattern or List[Pattern],
 | 
					def readfile(filename: Union[str, pathlib.Path],
 | 
				
			||||||
              ) -> Tuple[List[Pattern], List[float]]:
 | 
					             *args,
 | 
				
			||||||
 | 
					             **kwargs,
 | 
				
			||||||
 | 
					             ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
     For each shape in each pattern, set shape.layer to the tuple
 | 
					    Wrapper for `gdsii.read()` that takes a filename or path instead of a stream.
 | 
				
			||||||
     (base_layer, datatype), where:
 | 
					 | 
				
			||||||
        layer is chosen to be equal to the original shape.layer if it is an int,
 | 
					 | 
				
			||||||
            or shape.layer[0] if it is a tuple
 | 
					 | 
				
			||||||
        datatype is chosen arbitrarily, based on calcualted dose for each shape.
 | 
					 | 
				
			||||||
            Shapes with equal calcualted dose will have the same datatype.
 | 
					 | 
				
			||||||
            A list of doses is retured, providing a mapping between datatype
 | 
					 | 
				
			||||||
            (list index) and dose (list entry).
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Note that this function modifies the input Pattern(s).
 | 
					    Will automatically decompress files with a .gz suffix.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param patterns: A Pattern or list of patterns to write to file. Modified by this function.
 | 
					    Args:
 | 
				
			||||||
    :returns: (patterns, dose_list)
 | 
					        filename: Filename to save to.
 | 
				
			||||||
            patterns: modified input patterns
 | 
					        *args: passed to `gdsii.read`
 | 
				
			||||||
            dose_list: A list of doses, providing a mapping between datatype (int, list index)
 | 
					        **kwargs: passed to `gdsii.read`
 | 
				
			||||||
                       and dose (float, list entry).
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if isinstance(patterns, Pattern):
 | 
					    path = pathlib.Path(filename)
 | 
				
			||||||
        patterns = [patterns]
 | 
					    if path.suffix == '.gz':
 | 
				
			||||||
 | 
					        open_func: Callable = gzip.open
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        open_func = open
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Get a dict of id(pattern) -> pattern
 | 
					    with io.BufferedReader(open_func(path, mode='rb')) as stream:
 | 
				
			||||||
    patterns_by_id = {id(pattern): pattern for pattern in patterns}
 | 
					        results = read(stream, *args, **kwargs)
 | 
				
			||||||
    for pattern in patterns:
 | 
					    return results
 | 
				
			||||||
        patterns_by_id.update(pattern.referenced_patterns_by_id())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Get a table of (id(pat), written_dose) for each pattern and subpattern
 | 
					 | 
				
			||||||
    sd_table = make_dose_table(patterns)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # 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)))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Create a new pattern for each non-1-dose entry in the dose table
 | 
					 | 
				
			||||||
    #   and update the shapes to reflect their new dose
 | 
					 | 
				
			||||||
    new_pats = {} # (id, dose) -> new_pattern mapping
 | 
					 | 
				
			||||||
    for pat_id, pat_dose in sd_table:
 | 
					 | 
				
			||||||
        if pat_dose == 1:
 | 
					 | 
				
			||||||
            new_pats[(pat_id, pat_dose)] = patterns_by_id[pat_id]
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        pat = patterns_by_id[pat_id].deepcopy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        encoded_name = mangle_name(pat, pat_dose).encode('ASCII')
 | 
					 | 
				
			||||||
        if len(encoded_name) == 0:
 | 
					 | 
				
			||||||
            raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for shape in pat.shapes:
 | 
					 | 
				
			||||||
            data_type = dose_vals_list.index(shape.dose * pat_dose)
 | 
					 | 
				
			||||||
            if is_scalar(shape.layer):
 | 
					 | 
				
			||||||
                layer = (shape.layer, data_type)
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                layer = (shape.layer[0], data_type)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        new_pats[(pat_id, pat_dose)] = pat
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Go back through all the dose-specific patterns and fix up their subpattern entries
 | 
					 | 
				
			||||||
    for (pat_id, pat_dose), pat in new_pats.items():
 | 
					 | 
				
			||||||
        for subpat in pat.subpatterns:
 | 
					 | 
				
			||||||
            dose_mult = subpat.dose * pat_dose
 | 
					 | 
				
			||||||
            subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return patterns, list(dose_vals)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]):
 | 
					def read(stream: io.BufferedIOBase,
 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Alias for read(filename, use_dtype_as_dose=True)
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    return read(filename, use_dtype_as_dose=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def read(filename: str,
 | 
					 | 
				
			||||||
         use_dtype_as_dose: bool = False,
 | 
					         use_dtype_as_dose: bool = False,
 | 
				
			||||||
         clean_vertices: bool = True,
 | 
					         clean_vertices: bool = True,
 | 
				
			||||||
         ) -> (Dict[str, Pattern], Dict[str, Any]):
 | 
					         ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
 | 
					    Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
 | 
				
			||||||
     translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
 | 
					     translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
 | 
				
			||||||
@ -232,18 +194,23 @@ def read(filename: str,
 | 
				
			|||||||
      'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns)
 | 
					      'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns)
 | 
				
			||||||
                                per database unit
 | 
					                                per database unit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param filename: Filename specifying a GDSII file to read from.
 | 
					    Args:
 | 
				
			||||||
    :param use_dtype_as_dose: If false, set each polygon's layer to (gds_layer, gds_datatype).
 | 
					        stream: Stream to read from.
 | 
				
			||||||
            If true, set the layer to gds_layer and the dose to gds_datatype.
 | 
					        use_dtype_as_dose: If `False`, set each polygon's layer to `(gds_layer, gds_datatype)`.
 | 
				
			||||||
            Default False.
 | 
					            If `True`, set the layer to `gds_layer` and the dose to `gds_datatype`.
 | 
				
			||||||
    :param clean_vertices: If true, remove any redundant vertices when loading polygons.
 | 
					            Default `False`.
 | 
				
			||||||
 | 
					            NOTE: This will be deprecated in the future in favor of
 | 
				
			||||||
 | 
					                    `pattern.apply(masque.file.utils.dtype2dose)`.
 | 
				
			||||||
 | 
					        clean_vertices: If `True`, remove any redundant vertices when loading polygons.
 | 
				
			||||||
            The cleaning process removes any polygons with zero area or <3 vertices.
 | 
					            The cleaning process removes any polygons with zero area or <3 vertices.
 | 
				
			||||||
            Default True.
 | 
					            Default `True`.
 | 
				
			||||||
    :return: Tuple: (Dict of pattern_name:Patterns generated from GDSII structures, Dict of GDSII library info)
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        - Dict of pattern_name:Patterns generated from GDSII structures
 | 
				
			||||||
 | 
					        - Dict of GDSII library info
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with open(filename, mode='rb') as stream:
 | 
					    lib = gdsii.library.Library.load(stream)
 | 
				
			||||||
        lib = gdsii.library.Library.load(stream)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    library_info = {'name': lib.name.decode('ASCII'),
 | 
					    library_info = {'name': lib.name.decode('ASCII'),
 | 
				
			||||||
                    'meters_per_unit': lib.physical_unit,
 | 
					                    'meters_per_unit': lib.physical_unit,
 | 
				
			||||||
@ -256,46 +223,48 @@ def read(filename: str,
 | 
				
			|||||||
        for element in structure:
 | 
					        for element in structure:
 | 
				
			||||||
            # Switch based on element type:
 | 
					            # Switch based on element type:
 | 
				
			||||||
            if isinstance(element, gdsii.elements.Boundary):
 | 
					            if isinstance(element, gdsii.elements.Boundary):
 | 
				
			||||||
                if use_dtype_as_dose:
 | 
					                args = {'vertices': element.xy[:-1],
 | 
				
			||||||
                    shape = Polygon(vertices=element.xy[:-1],
 | 
					                        'layer': (element.layer, element.data_type),
 | 
				
			||||||
                                    dose=element.data_type,
 | 
					                       }
 | 
				
			||||||
                                    layer=element.layer)
 | 
					
 | 
				
			||||||
                else:
 | 
					                poly = Polygon(**args)
 | 
				
			||||||
                    shape = Polygon(vertices=element.xy[:-1],
 | 
					
 | 
				
			||||||
                                    layer=(element.layer, element.data_type))
 | 
					 | 
				
			||||||
                if clean_vertices:
 | 
					                if clean_vertices:
 | 
				
			||||||
                    try:
 | 
					                    try:
 | 
				
			||||||
                        shape.clean_vertices()
 | 
					                        poly.clean_vertices()
 | 
				
			||||||
                    except PatternError:
 | 
					                    except PatternError:
 | 
				
			||||||
                        continue
 | 
					                        continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                pat.shapes.append(shape)
 | 
					                pat.shapes.append(poly)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if isinstance(element, gdsii.elements.Path):
 | 
					            if isinstance(element, gdsii.elements.Path):
 | 
				
			||||||
                cap_map = {0: Path.Cap.Flush,
 | 
					                if element.path_type in path_cap_map:
 | 
				
			||||||
                           1: Path.Cap.Circle,
 | 
					                    cap = path_cap_map[element.path_type]
 | 
				
			||||||
                           2: Path.Cap.Square,
 | 
					 | 
				
			||||||
                          #3: custom?
 | 
					 | 
				
			||||||
                          }
 | 
					 | 
				
			||||||
                if element.path_type in cap_map:
 | 
					 | 
				
			||||||
                    cap = cap_map[element.path_type]
 | 
					 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    raise PatternError('Unrecognized path type: {}'.format(element.path_type))
 | 
					                    raise PatternError('Unrecognized path type: {}'.format(element.path_type))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if use_dtype_as_dose:
 | 
					                args = {'vertices': element.xy,
 | 
				
			||||||
                    shape = Path(vertices=element.xy,
 | 
					                        'layer': (element.layer, element.data_type),
 | 
				
			||||||
                                 dose=element.data_type,
 | 
					                        'width': element.width if element.width is not None else 0.0,
 | 
				
			||||||
                                 layer=element.layer)
 | 
					                        'cap': cap,
 | 
				
			||||||
                else:
 | 
					                       }
 | 
				
			||||||
                    shape = Path(vertices=element.xy,
 | 
					
 | 
				
			||||||
                                 layer=(element.layer, element.data_type))
 | 
					                if cap == Path.Cap.SquareCustom:
 | 
				
			||||||
 | 
					                    args['cap_extensions'] = numpy.zeros(2)
 | 
				
			||||||
 | 
					                    if element.bgn_extn is not None:
 | 
				
			||||||
 | 
					                        args['cap_extensions'][0] = element.bgn_extn
 | 
				
			||||||
 | 
					                    if element.end_extn is not None:
 | 
				
			||||||
 | 
					                        args['cap_extensions'][1] = element.end_extn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                path = Path(**args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if clean_vertices:
 | 
					                if clean_vertices:
 | 
				
			||||||
                    try:
 | 
					                    try:
 | 
				
			||||||
                        shape.clean_vertices()
 | 
					                        path.clean_vertices()
 | 
				
			||||||
                    except PatternError as err:
 | 
					                    except PatternError as err:
 | 
				
			||||||
                        continue
 | 
					                        continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                pat.shapes.append(shape)
 | 
					                pat.shapes.append(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            elif isinstance(element, gdsii.elements.Text):
 | 
					            elif isinstance(element, gdsii.elements.Text):
 | 
				
			||||||
                label = Label(offset=element.xy,
 | 
					                label = Label(offset=element.xy,
 | 
				
			||||||
@ -309,41 +278,51 @@ def read(filename: str,
 | 
				
			|||||||
            elif isinstance(element, gdsii.elements.ARef):
 | 
					            elif isinstance(element, gdsii.elements.ARef):
 | 
				
			||||||
                pat.subpatterns.append(_aref_to_gridrep(element))
 | 
					                pat.subpatterns.append(_aref_to_gridrep(element))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if use_dose_as_dtype:
 | 
				
			||||||
 | 
					            logger.warning('use_dose_as_dtype will be removed in the future!')
 | 
				
			||||||
 | 
					            pat = dose2dtype(pat)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        patterns.append(pat)
 | 
					        patterns.append(pat)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries
 | 
					    # 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).
 | 
					    #  according to the subpattern.identifier (which is deleted after use).
 | 
				
			||||||
    patterns_dict = dict(((p.name, p) for p in patterns))
 | 
					    patterns_dict = dict(((p.name, p) for p in patterns))
 | 
				
			||||||
    for p in patterns_dict.values():
 | 
					    for p in patterns_dict.values():
 | 
				
			||||||
        for sp in p.subpatterns:
 | 
					        for sp in p.subpatterns:
 | 
				
			||||||
            sp.pattern = patterns_dict[sp.ref_name.decode('ASCII')]
 | 
					            sp.pattern = patterns_dict[sp.identifier[0].decode('ASCII')]
 | 
				
			||||||
            del sp.ref_name
 | 
					            del sp.identifier
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return patterns_dict, library_info
 | 
					    return patterns_dict, library_info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _mlayer2gds(mlayer):
 | 
					def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]:
 | 
				
			||||||
    if is_scalar(mlayer):
 | 
					    """ Helper to turn a layer tuple-or-int into a layer and datatype"""
 | 
				
			||||||
 | 
					    if isinstance(mlayer, int):
 | 
				
			||||||
        layer = mlayer
 | 
					        layer = mlayer
 | 
				
			||||||
        data_type = 0
 | 
					        data_type = 0
 | 
				
			||||||
    else:
 | 
					    elif isinstance(mlayer, tuple):
 | 
				
			||||||
        layer = mlayer[0]
 | 
					        layer = mlayer[0]
 | 
				
			||||||
        if len(mlayer) > 1:
 | 
					        if len(mlayer) > 1:
 | 
				
			||||||
            data_type = mlayer[1]
 | 
					            data_type = mlayer[1]
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            data_type = 0
 | 
					            data_type = 0
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        raise PatternError(f'Invalid layer for gdsii: {layer}. Note that gdsii layers cannot be strings.')
 | 
				
			||||||
    return layer, data_type
 | 
					    return layer, data_type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern:
 | 
					def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern:
 | 
				
			||||||
    # Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None
 | 
					    """
 | 
				
			||||||
    #  and sets the instance attribute .ref_name to the struct_name.
 | 
					    Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None
 | 
				
			||||||
    #
 | 
					     and sets the instance .identifier to (struct_name,).
 | 
				
			||||||
    # BUG: "Absolute" means not affected by parent elements.
 | 
					
 | 
				
			||||||
    #       That's not currently supported by masque at all, so need to either tag it and
 | 
					    BUG:
 | 
				
			||||||
    #       undo the parent transformations, or implement it in masque.
 | 
					        "Absolute" means not affected by parent elements.
 | 
				
			||||||
 | 
					          That's not currently supported by masque at all, so need to either tag it and
 | 
				
			||||||
 | 
					          undo the parent transformations, or implement it in masque.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    subpat = SubPattern(pattern=None, offset=element.xy)
 | 
					    subpat = SubPattern(pattern=None, offset=element.xy)
 | 
				
			||||||
    subpat.ref_name = element.struct_name
 | 
					    subpat.identifier = (element.struct_name,)
 | 
				
			||||||
    if element.strans is not None:
 | 
					    if element.strans is not None:
 | 
				
			||||||
        if element.mag is not None:
 | 
					        if element.mag is not None:
 | 
				
			||||||
            subpat.scale = element.mag
 | 
					            subpat.scale = element.mag
 | 
				
			||||||
@ -359,22 +338,24 @@ def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern:
 | 
				
			|||||||
                raise PatternError('Absolute rotation is not implemented yet!')
 | 
					                raise PatternError('Absolute rotation is not implemented yet!')
 | 
				
			||||||
        # Bit 0 means mirror x-axis
 | 
					        # Bit 0 means mirror x-axis
 | 
				
			||||||
        if get_bit(element.strans, 15 - 0):
 | 
					        if get_bit(element.strans, 15 - 0):
 | 
				
			||||||
            subpat.mirror(axis=0)
 | 
					            subpat.mirrored[0] = 1
 | 
				
			||||||
    return subpat
 | 
					    return subpat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
 | 
					def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
 | 
				
			||||||
    # Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None
 | 
					    """
 | 
				
			||||||
    #  and sets the instance attribute .ref_name to the struct_name.
 | 
					    Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None
 | 
				
			||||||
    #
 | 
					     and sets the instance .identifier to (struct_name,).
 | 
				
			||||||
    # BUG: "Absolute" means not affected by parent elements.
 | 
					 | 
				
			||||||
    #       That's not currently supported by masque at all, so need to either tag it and
 | 
					 | 
				
			||||||
    #       undo the parent transformations, or implement it in masque.i
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    BUG:
 | 
				
			||||||
 | 
					        "Absolute" means not affected by parent elements.
 | 
				
			||||||
 | 
					          That's not currently supported by masque at all, so need to either tag it and
 | 
				
			||||||
 | 
					          undo the parent transformations, or implement it in masque.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    rotation = 0
 | 
					    rotation = 0
 | 
				
			||||||
    offset = numpy.array(element.xy[0])
 | 
					    offset = numpy.array(element.xy[0])
 | 
				
			||||||
    scale = 1
 | 
					    scale = 1
 | 
				
			||||||
    mirror_signs = numpy.ones(2)
 | 
					    mirror_across_x = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if element.strans is not None:
 | 
					    if element.strans is not None:
 | 
				
			||||||
        if element.mag is not None:
 | 
					        if element.mag is not None:
 | 
				
			||||||
@ -389,15 +370,11 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
 | 
				
			|||||||
                raise PatternError('Absolute rotation is not implemented yet!')
 | 
					                raise PatternError('Absolute rotation is not implemented yet!')
 | 
				
			||||||
        # Bit 0 means mirror x-axis
 | 
					        # Bit 0 means mirror x-axis
 | 
				
			||||||
        if get_bit(element.strans, 15 - 0):
 | 
					        if get_bit(element.strans, 15 - 0):
 | 
				
			||||||
            mirror_signs[0] = -1
 | 
					            mirror_across_x = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    counts = [element.cols, element.rows]
 | 
					    counts = [element.cols, element.rows]
 | 
				
			||||||
    vec_a0 = element.xy[1] - offset
 | 
					    a_vector = (element.xy[1] - offset) / counts[0]
 | 
				
			||||||
    vec_b0 = element.xy[2] - offset
 | 
					    b_vector = (element.xy[2] - offset) / counts[1]
 | 
				
			||||||
 | 
					 | 
				
			||||||
    a_vector = numpy.dot(rotation_matrix_2d(-rotation), vec_a0 / scale / counts[0]) * mirror_signs
 | 
					 | 
				
			||||||
    b_vector = numpy.dot(rotation_matrix_2d(-rotation), vec_b0 / scale / counts[1]) * mirror_signs
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    gridrep = GridRepetition(pattern=None,
 | 
					    gridrep = GridRepetition(pattern=None,
 | 
				
			||||||
                            a_vector=a_vector,
 | 
					                            a_vector=a_vector,
 | 
				
			||||||
@ -407,25 +384,28 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
 | 
				
			|||||||
                            offset=offset,
 | 
					                            offset=offset,
 | 
				
			||||||
                            rotation=rotation,
 | 
					                            rotation=rotation,
 | 
				
			||||||
                            scale=scale,
 | 
					                            scale=scale,
 | 
				
			||||||
                            mirrored=(mirror_signs == -1))
 | 
					                            mirrored=(mirror_across_x, False))
 | 
				
			||||||
    gridrep.ref_name = element.struct_name
 | 
					    gridrep.identifier = (element.struct_name,)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return gridrep
 | 
					    return gridrep
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition]
 | 
					def _subpatterns_to_refs(subpatterns: List[subpattern_t]
 | 
				
			||||||
                        ) -> List[gdsii.elements.ARef or gdsii.elements.SRef]:
 | 
					                        ) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]:
 | 
				
			||||||
    #  strans must be set for angle and mag to take effect
 | 
					 | 
				
			||||||
    refs = []
 | 
					    refs = []
 | 
				
			||||||
    for subpat in subpatterns:
 | 
					    for subpat in subpatterns:
 | 
				
			||||||
 | 
					        if subpat.pattern is None:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
        encoded_name = subpat.pattern.name
 | 
					        encoded_name = subpat.pattern.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Note: GDS mirrors first and rotates second
 | 
				
			||||||
 | 
					        mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
 | 
				
			||||||
 | 
					        ref: Union[gdsii.elements.SRef, gdsii.elements.ARef]
 | 
				
			||||||
        if isinstance(subpat, GridRepetition):
 | 
					        if isinstance(subpat, GridRepetition):
 | 
				
			||||||
            mirror_signs = (-1) ** numpy.array(subpat.mirrored)
 | 
					 | 
				
			||||||
            xy = numpy.array(subpat.offset) + [
 | 
					            xy = numpy.array(subpat.offset) + [
 | 
				
			||||||
                  [0, 0],
 | 
					                  [0, 0],
 | 
				
			||||||
                  numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.a_vector * mirror_signs) * subpat.scale * subpat.a_count,
 | 
					                  subpat.a_vector * subpat.a_count,
 | 
				
			||||||
                  numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.b_vector * mirror_signs) * subpat.scale * subpat.b_count,
 | 
					                  subpat.b_vector * subpat.b_count,
 | 
				
			||||||
                 ]
 | 
					                 ]
 | 
				
			||||||
            ref = gdsii.elements.ARef(struct_name=encoded_name,
 | 
					            ref = gdsii.elements.ARef(struct_name=encoded_name,
 | 
				
			||||||
                                       xy=numpy.round(xy).astype(int),
 | 
					                                       xy=numpy.round(xy).astype(int),
 | 
				
			||||||
@ -435,36 +415,40 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition]
 | 
				
			|||||||
            ref = gdsii.elements.SRef(struct_name=encoded_name,
 | 
					            ref = gdsii.elements.SRef(struct_name=encoded_name,
 | 
				
			||||||
                                      xy=numpy.round([subpat.offset]).astype(int))
 | 
					                                      xy=numpy.round([subpat.offset]).astype(int))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ref.strans = 0
 | 
					        ref.angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360
 | 
				
			||||||
        ref.angle = subpat.rotation * 180 / numpy.pi
 | 
					        #  strans must be non-None for angle and mag to take effect
 | 
				
			||||||
        mirror_x, mirror_y = subpat.mirrored
 | 
					        ref.strans = set_bit(0, 15 - 0, mirror_across_x)
 | 
				
			||||||
        if mirror_x and mirror_y:
 | 
					 | 
				
			||||||
            ref.angle += 180
 | 
					 | 
				
			||||||
        elif mirror_x:
 | 
					 | 
				
			||||||
            ref.strans = set_bit(ref.strans, 15 - 0, True)
 | 
					 | 
				
			||||||
        elif mirror_y:
 | 
					 | 
				
			||||||
            ref.angle += 180
 | 
					 | 
				
			||||||
            ref.strans = set_bit(ref.strans, 15 - 0, True)
 | 
					 | 
				
			||||||
        ref.angle %= 360
 | 
					 | 
				
			||||||
        ref.mag = subpat.scale
 | 
					        ref.mag = subpat.scale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        refs.append(ref)
 | 
					        refs.append(ref)
 | 
				
			||||||
    return refs
 | 
					    return refs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _shapes_to_boundaries(shapes: List[Shape]
 | 
					def _shapes_to_elements(shapes: List[Shape],
 | 
				
			||||||
                         ) -> List[gdsii.elements.Boundary]:
 | 
					                        polygonize_paths: bool = False
 | 
				
			||||||
    # Add a Boundary element for each shape
 | 
					                       ) -> List[Union[gdsii.elements.Boundary, gdsii.elements.Path]]:
 | 
				
			||||||
    boundaries = []
 | 
					    elements: List[Union[gdsii.elements.Boundary, gdsii.elements.Path]] = []
 | 
				
			||||||
 | 
					    # Add a Boundary element for each shape, and Path elements if necessary
 | 
				
			||||||
    for shape in shapes:
 | 
					    for shape in shapes:
 | 
				
			||||||
        layer, data_type = _mlayer2gds(shape.layer)
 | 
					        layer, data_type = _mlayer2gds(shape.layer)
 | 
				
			||||||
        for polygon in shape.to_polygons():
 | 
					        if isinstance(shape, Path) and not polygonize_paths:
 | 
				
			||||||
            xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int)
 | 
					            xy = numpy.round(shape.vertices + shape.offset).astype(int)
 | 
				
			||||||
            xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
 | 
					            width = numpy.round(shape.width).astype(int)
 | 
				
			||||||
            boundaries.append(gdsii.elements.Boundary(layer=layer,
 | 
					            path_type = next(k for k, v in path_cap_map.items() if v == shape.cap)    #reverse lookup
 | 
				
			||||||
                                                      data_type=data_type,
 | 
					            path = gdsii.elements.Path(layer=layer,
 | 
				
			||||||
                                                      xy=xy_closed))
 | 
					                                       data_type=data_type,
 | 
				
			||||||
    return boundaries
 | 
					                                       xy=xy)
 | 
				
			||||||
 | 
					            path.path_type = path_type
 | 
				
			||||||
 | 
					            path.width = width
 | 
				
			||||||
 | 
					            elements.append(path)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            for polygon in shape.to_polygons():
 | 
				
			||||||
 | 
					                xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int)
 | 
				
			||||||
 | 
					                xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
 | 
				
			||||||
 | 
					                elements.append(gdsii.elements.Boundary(layer=layer,
 | 
				
			||||||
 | 
					                                                        data_type=data_type,
 | 
				
			||||||
 | 
					                                                        xy=xy_closed))
 | 
				
			||||||
 | 
					    return elements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
 | 
					def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
 | 
				
			||||||
@ -479,10 +463,21 @@ def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
 | 
				
			|||||||
    return texts
 | 
					    return texts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _disambiguate_pattern_names(patterns):
 | 
					def disambiguate_pattern_names(patterns,
 | 
				
			||||||
 | 
					                               max_name_length: int = 32,
 | 
				
			||||||
 | 
					                               suffix_length: int = 6,
 | 
				
			||||||
 | 
					                               dup_warn_filter: Callable[[str,], bool] = None,      # If returns False, don't warn about this name
 | 
				
			||||||
 | 
					                               ):
 | 
				
			||||||
    used_names = []
 | 
					    used_names = []
 | 
				
			||||||
    for pat in patterns:
 | 
					    for pat in patterns:
 | 
				
			||||||
        sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name)
 | 
					        if len(pat.name) > max_name_length:
 | 
				
			||||||
 | 
					            shortened_name = pat.name[:max_name_length - suffix_length]
 | 
				
			||||||
 | 
					            logger.warning('Pattern name "{}" is too long ({}/{} chars),\n'.format(pat.name, len(pat.name), max_name_length) +
 | 
				
			||||||
 | 
					                           ' shortening to "{}" before generating suffix'.format(shortened_name))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            shortened_name = pat.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        i = 0
 | 
					        i = 0
 | 
				
			||||||
        suffixed_name = sanitized_name
 | 
					        suffixed_name = sanitized_name
 | 
				
			||||||
@ -495,14 +490,16 @@ def _disambiguate_pattern_names(patterns):
 | 
				
			|||||||
        if sanitized_name == '':
 | 
					        if sanitized_name == '':
 | 
				
			||||||
            logger.warning('Empty pattern name saved as "{}"'.format(suffixed_name))
 | 
					            logger.warning('Empty pattern name saved as "{}"'.format(suffixed_name))
 | 
				
			||||||
        elif suffixed_name != sanitized_name:
 | 
					        elif suffixed_name != sanitized_name:
 | 
				
			||||||
            logger.warning('Pattern name "{}" appears multiple times; renaming to "{}"'.format(pat.name, suffixed_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))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        encoded_name = suffixed_name.encode('ASCII')
 | 
					        encoded_name = suffixed_name.encode('ASCII')
 | 
				
			||||||
        if len(encoded_name) == 0:
 | 
					        if len(encoded_name) == 0:
 | 
				
			||||||
            # Should never happen since zero-length names are replaced
 | 
					            # Should never happen since zero-length names are replaced
 | 
				
			||||||
            raise PatternError('Zero-length name after sanitize+encode, originally "{}"'.format(pat.name))
 | 
					            raise PatternError('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name))
 | 
				
			||||||
        if len(encoded_name) > 32:
 | 
					        if len(encoded_name) > max_name_length:
 | 
				
			||||||
            raise PatternError('Pattern name "{}" length > 32 after encode, originally "{}"'.format(encoded_name, pat.name))
 | 
					            raise PatternError('Pattern name "{!r}" length > {} after encode,\n originally "{}"'.format(encoded_name, max_name_length, pat.name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pat.name = encoded_name
 | 
					        pat.name = encoded_name
 | 
				
			||||||
        used_names.append(suffixed_name)
 | 
					        used_names.append(suffixed_name)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										441
									
								
								masque/file/oasis.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										441
									
								
								masque/file/oasis.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,441 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					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 numpy
 | 
				
			||||||
 | 
					import base64
 | 
				
			||||||
 | 
					import struct
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import pathlib
 | 
				
			||||||
 | 
					import gzip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					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__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					path_cap_map = {
 | 
				
			||||||
 | 
					                PathExtensionScheme.Flush: Path.Cap.Flush,
 | 
				
			||||||
 | 
					                PathExtensionScheme.HalfWidth: Path.Cap.Square,
 | 
				
			||||||
 | 
					                PathExtensionScheme.Arbitrary: Path.Cap.SquareCustom,
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def write(patterns: Union[Pattern, List[Pattern]],
 | 
				
			||||||
 | 
					          stream: io.BufferedIOBase,
 | 
				
			||||||
 | 
					          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):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Write a `Pattern` or list of patterns to a OASIS file, writing patterns
 | 
				
			||||||
 | 
					     as OASIS cells, polygons as Polygon records, and subpatterns as Placement
 | 
				
			||||||
 | 
					     records. Other shape types may be converted to polygons if no equivalent
 | 
				
			||||||
 | 
					     record type exists (or is not implemented here yet). #TODO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     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 write to file.
 | 
				
			||||||
 | 
					        stream: Stream object to write to.
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					        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`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    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, validation=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for name, layer_num in layer_map.items():
 | 
				
			||||||
 | 
					        layer, data_type = _mlayer2oas(layer_num)
 | 
				
			||||||
 | 
					        lib.layer_names.append(                     #TODO figure out how to deal with text layers
 | 
				
			||||||
 | 
					                LayerName(nstring=name,
 | 
				
			||||||
 | 
					                          layer_interval=(layer, layer),
 | 
				
			||||||
 | 
					                          type_interval=(data_type, data_type),
 | 
				
			||||||
 | 
					                          is_textlayer=False))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def layer2oas(layer: layer_t) -> Tuple[int, int]:
 | 
				
			||||||
 | 
					        layer_num = layer_map[layer] if isinstance(layer, str) else layer
 | 
				
			||||||
 | 
					        return _mlayer2oas(layer_num)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # 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=NString(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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lib.write(stream)
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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:Patterns generated from GDSII structures
 | 
				
			||||||
 | 
					        - Dict of GDSII library info
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lib = fatamorgana.OASISLayout.read(stream)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    library_info = {'units_per_micrometer': lib.unit,
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    patterns = []
 | 
				
			||||||
 | 
					    for cell in lib.cells:
 | 
				
			||||||
 | 
					        pat = Pattern(name=cell.name.string)
 | 
				
			||||||
 | 
					        for element in cell.geometry:
 | 
				
			||||||
 | 
					            if element.repetition is not None:
 | 
				
			||||||
 | 
					                raise PatternError('masque OASIS reader does not implement repetitions for shapes yet')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Switch based on element type:
 | 
				
			||||||
 | 
					            if isinstance(element, fatrec.Polygon):
 | 
				
			||||||
 | 
					                args = {'vertices': element.point_list,
 | 
				
			||||||
 | 
					                        'layer':  (element.layer, element.data_type)
 | 
				
			||||||
 | 
					                        'offset': (element.x, element.y),
 | 
				
			||||||
 | 
					                       }
 | 
				
			||||||
 | 
					                poly = Polygon(**args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if clean_vertices:
 | 
				
			||||||
 | 
					                    try:
 | 
				
			||||||
 | 
					                        poly.clean_vertices()
 | 
				
			||||||
 | 
					                    except PatternError:
 | 
				
			||||||
 | 
					                        continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                pat.shapes.append(poly)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if isinstance(element, fatrec.Path):
 | 
				
			||||||
 | 
					                cap_start = path_cap_map[element.extension_start[0]]
 | 
				
			||||||
 | 
					                cap_end   = path_cap_map[element.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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                args = {'vertices': element.point_list,
 | 
				
			||||||
 | 
					                        'layer':  (element.layer, element.data_type)
 | 
				
			||||||
 | 
					                        'offset': (element.x, element.y),
 | 
				
			||||||
 | 
					                        'width': element.half_width * 2,
 | 
				
			||||||
 | 
					                        'cap': cap,
 | 
				
			||||||
 | 
					                       }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if cap == Path.Cap.SquareCustom:
 | 
				
			||||||
 | 
					                    args['cap_extensions'] = numpy.array((element.extension_start[1],
 | 
				
			||||||
 | 
					                                                          element.extension_end[1]))
 | 
				
			||||||
 | 
					                path = Path(**args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if clean_vertices:
 | 
				
			||||||
 | 
					                    try:
 | 
				
			||||||
 | 
					                        path.clean_vertices()
 | 
				
			||||||
 | 
					                    except PatternError as err:
 | 
				
			||||||
 | 
					                        continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                pat.shapes.append(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            elif isinstance(element, fatrec.Text):
 | 
				
			||||||
 | 
					                args = {'layer':  (element.layer, element.data_type)
 | 
				
			||||||
 | 
					                        'offset': (element.x, element.y),
 | 
				
			||||||
 | 
					                        'string': str(element.string),
 | 
				
			||||||
 | 
					                       }
 | 
				
			||||||
 | 
					                pat.labels.append(Label(**args))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for placement in cell.placements:
 | 
				
			||||||
 | 
					            pat.subpattterns.append += _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:
 | 
				
			||||||
 | 
					            sp.pattern = patterns_dict[sp.identifier[0]]
 | 
				
			||||||
 | 
					            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.')  #TODO allow string layers using layer map def
 | 
				
			||||||
 | 
					    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))
 | 
				
			||||||
 | 
					    kwargs = {
 | 
				
			||||||
 | 
					       'pattern': None,
 | 
				
			||||||
 | 
					       'mirrored': (placement.flip, False),
 | 
				
			||||||
 | 
					       'rotation': float(placement.angle * pi/180)
 | 
				
			||||||
 | 
					       'scale': placement.magnification,
 | 
				
			||||||
 | 
					       'identifier': (placement.name,),
 | 
				
			||||||
 | 
					       }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					                                **kwargs)
 | 
				
			||||||
 | 
					        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, **kwargs))
 | 
				
			||||||
 | 
					    elif rep is None
 | 
				
			||||||
 | 
					        subpats = [SubPattern(offset=xy + rep_offset, **kwargs)]
 | 
				
			||||||
 | 
					    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 = {
 | 
				
			||||||
 | 
					            'x': xy[0],
 | 
				
			||||||
 | 
					            'y': xy[1],
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if isinstance(subpat, GridRepetition):
 | 
				
			||||||
 | 
					            kwargs['rep'] = 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))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ref = fatrec.Placement(
 | 
				
			||||||
 | 
					                name=subpat.pattern.name,
 | 
				
			||||||
 | 
					                flip=mirror_across_x,
 | 
				
			||||||
 | 
					                angle=((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360,
 | 
				
			||||||
 | 
					                magnification=subpat.scale,
 | 
				
			||||||
 | 
					                **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        refs.append(ref)
 | 
				
			||||||
 | 
					    return refs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _shapes_to_elements(shapes: List[Shape],
 | 
				
			||||||
 | 
					                        layer2oas: Callable[[layer_t], Tuple[int, int]],
 | 
				
			||||||
 | 
					                        polygonize_paths: bool = False,
 | 
				
			||||||
 | 
					                       ) -> List[Union[fatrec.Polygon, fatrec.Path]]:
 | 
				
			||||||
 | 
					    # Add a Polygon record for each shape, and Path elements if necessary
 | 
				
			||||||
 | 
					    elements: List[Union[fatrec.Polygon, fatrec.Path]] = []
 | 
				
			||||||
 | 
					    for shape in shapes:
 | 
				
			||||||
 | 
					        layer, data_type = layer2oas(shape.layer)
 | 
				
			||||||
 | 
					        if isinstance(shape, Path) and not polygonize_paths:
 | 
				
			||||||
 | 
					            offset = numpy.round(shape.offset).astype(int)
 | 
				
			||||||
 | 
					            points = numpy.round(shape.vertices).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
 | 
				
			||||||
 | 
					            path = fatrec.Path(layer=layer,
 | 
				
			||||||
 | 
					                               data_type=data_type,
 | 
				
			||||||
 | 
					                               point_list=points,
 | 
				
			||||||
 | 
					                               half_width=half_width,
 | 
				
			||||||
 | 
					                               x=offset[0],
 | 
				
			||||||
 | 
					                               y=offset[1],
 | 
				
			||||||
 | 
					                               extension_start=path_type,       #TODO implement multiple cap types?
 | 
				
			||||||
 | 
					                               extension_end=path_type,
 | 
				
			||||||
 | 
					                               )
 | 
				
			||||||
 | 
					            elements.append(path)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            for polygon in shape.to_polygons():
 | 
				
			||||||
 | 
					                points = numpy.round(polygon.vertices).astype(int)
 | 
				
			||||||
 | 
					                offset = numpy.round(polygon.offset).astype(int)
 | 
				
			||||||
 | 
					                elements.append(fatrec.Polygon(layer=layer,
 | 
				
			||||||
 | 
					                                               data_type=data_type,
 | 
				
			||||||
 | 
					                                               x=offset[0],
 | 
				
			||||||
 | 
					                                               y=offset[1],
 | 
				
			||||||
 | 
					                                               point_list=point_list))
 | 
				
			||||||
 | 
					    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, text_type = layer2oas(label.layer)
 | 
				
			||||||
 | 
					        xy = numpy.round(label.offset).astype(int)
 | 
				
			||||||
 | 
					        texts.append(fatrec.Text(layer=layer,
 | 
				
			||||||
 | 
					                                 text_type=text_type,
 | 
				
			||||||
 | 
					                                 x=xy[0],
 | 
				
			||||||
 | 
					                                 y=xy[1],
 | 
				
			||||||
 | 
					                                 string=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))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        encoded_name = suffixed_name.encode('ASCII')
 | 
				
			||||||
 | 
					        if len(encoded_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 = encoded_name
 | 
				
			||||||
 | 
					        used_names.append(suffixed_name)
 | 
				
			||||||
@ -1,18 +1,16 @@
 | 
				
			|||||||
"""
 | 
					"""
 | 
				
			||||||
SVG file format readers and writers
 | 
					SVG file format readers and writers
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					from typing import Dict, Optional
 | 
				
			||||||
import svgwrite
 | 
					import svgwrite
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
 | 
					import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .utils import mangle_name
 | 
					from .utils import mangle_name
 | 
				
			||||||
from .. import Pattern
 | 
					from .. import Pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					def writefile(pattern: Pattern,
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def write(pattern: Pattern,
 | 
					 | 
				
			||||||
          filename: str,
 | 
					          filename: str,
 | 
				
			||||||
          custom_attributes: bool=False):
 | 
					          custom_attributes: bool=False):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@ -23,26 +21,32 @@ def write(pattern: Pattern,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Note that this function modifies the Pattern.
 | 
					    Note that this function modifies the Pattern.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    If custom_attributes is True, non-standard pattern_layer and pattern_dose attributes
 | 
					    If `custom_attributes` is `True`, non-standard `pattern_layer` and `pattern_dose` attributes
 | 
				
			||||||
     are written to the relevant elements.
 | 
					     are written to the relevant elements.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    It is often a good idea to run pattern.subpatternize() on pattern prior to
 | 
					    It is often a good idea to run `pattern.subpatternize()` on pattern prior to
 | 
				
			||||||
     calling this function, especially if calling .polygonize() will result in very
 | 
					     calling this function, especially if calling `.polygonize()` will result in very
 | 
				
			||||||
     many vertices.
 | 
					     many vertices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    If you want pattern polygonized with non-default arguments, just call pattern.polygonize()
 | 
					    If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
 | 
				
			||||||
     prior to calling this function.
 | 
					     prior to calling this function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param pattern: Pattern to write to file. Modified by this function.
 | 
					    Args:
 | 
				
			||||||
    :param filename: Filename to write to.
 | 
					        pattern: Pattern to write to file. Modified by this function.
 | 
				
			||||||
    :param custom_attributes: Whether to write non-standard pattern_layer and
 | 
					        filename: Filename to write to.
 | 
				
			||||||
            pattern_dose attributes to the SVG elements.
 | 
					        custom_attributes: Whether to write non-standard `pattern_layer` and
 | 
				
			||||||
 | 
					            `pattern_dose` attributes to the SVG elements.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Polygonize pattern
 | 
					    # Polygonize pattern
 | 
				
			||||||
    pattern.polygonize()
 | 
					    pattern.polygonize()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [bounds_min, bounds_max] = pattern.get_bounds()
 | 
					    bounds = pattern.get_bounds()
 | 
				
			||||||
 | 
					    if bounds is None:
 | 
				
			||||||
 | 
					        bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
 | 
				
			||||||
 | 
					        warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        bounds_min, bounds_max = bounds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2))
 | 
					    viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2))
 | 
				
			||||||
    viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox)
 | 
					    viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox)
 | 
				
			||||||
@ -52,11 +56,13 @@ def write(pattern: Pattern,
 | 
				
			|||||||
                           debug=(not custom_attributes))
 | 
					                           debug=(not custom_attributes))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Get a dict of id(pattern) -> pattern
 | 
					    # Get a dict of id(pattern) -> pattern
 | 
				
			||||||
    patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern}
 | 
					    patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern}        # type: Dict[int, Optional[Pattern]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Now create a group for each row in sd_table (ie, each pattern + dose combination)
 | 
					    # Now create a group for each row in sd_table (ie, each pattern + dose combination)
 | 
				
			||||||
    #  and add in any Boundary and Use elements
 | 
					    #  and add in any Boundary and Use elements
 | 
				
			||||||
    for pat in patterns_by_id.values():
 | 
					    for pat in patterns_by_id.values():
 | 
				
			||||||
 | 
					        if pat is None:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
        svg_group = svg.g(id=mangle_name(pat), fill='blue', stroke='red')
 | 
					        svg_group = svg.g(id=mangle_name(pat), fill='blue', stroke='red')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for shape in pat.shapes:
 | 
					        for shape in pat.shapes:
 | 
				
			||||||
@ -71,6 +77,8 @@ def write(pattern: Pattern,
 | 
				
			|||||||
                svg_group.add(path)
 | 
					                svg_group.add(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for subpat in pat.subpatterns:
 | 
					        for subpat in pat.subpatterns:
 | 
				
			||||||
 | 
					            if subpat.pattern is None:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
            transform = 'scale({:g}) rotate({:g}) translate({:g},{:g})'.format(
 | 
					            transform = 'scale({:g}) rotate({:g}) translate({:g},{:g})'.format(
 | 
				
			||||||
                subpat.scale, subpat.rotation, subpat.offset[0], subpat.offset[1])
 | 
					                subpat.scale, subpat.rotation, subpat.offset[0], subpat.offset[1])
 | 
				
			||||||
            use = svg.use(href='#' + mangle_name(subpat.pattern), transform=transform)
 | 
					            use = svg.use(href='#' + mangle_name(subpat.pattern), transform=transform)
 | 
				
			||||||
@ -83,25 +91,31 @@ def write(pattern: Pattern,
 | 
				
			|||||||
    svg.save()
 | 
					    svg.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def write_inverted(pattern: Pattern, filename: str):
 | 
					def writefile_inverted(pattern: Pattern, filename: str):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Write an inverted Pattern to an SVG file, by first calling .polygonize() and
 | 
					    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
 | 
					     `.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
 | 
					     box and drawing the polygons with reverse vertex order inside it, all within
 | 
				
			||||||
     one <path> element.
 | 
					     one `<path>` element.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Note that this function modifies the Pattern.
 | 
					    Note that this function modifies the Pattern.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    If you want pattern polygonized with non-default arguments, just call pattern.polygonize()
 | 
					    If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
 | 
				
			||||||
     prior to calling this function.
 | 
					     prior to calling this function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param pattern: Pattern to write to file. Modified by this function.
 | 
					    Args:
 | 
				
			||||||
    :param filename: Filename to write to.
 | 
					        pattern: Pattern to write to file. Modified by this function.
 | 
				
			||||||
 | 
					        filename: Filename to write to.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    # Polygonize and flatten pattern
 | 
					    # Polygonize and flatten pattern
 | 
				
			||||||
    pattern.polygonize().flatten()
 | 
					    pattern.polygonize().flatten()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [bounds_min, bounds_max] = pattern.get_bounds()
 | 
					    bounds = pattern.get_bounds()
 | 
				
			||||||
 | 
					    if bounds is None:
 | 
				
			||||||
 | 
					        bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
 | 
				
			||||||
 | 
					        warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox')
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        bounds_min, bounds_max = bounds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2))
 | 
					    viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2))
 | 
				
			||||||
    viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox)
 | 
					    viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox)
 | 
				
			||||||
@ -129,8 +143,11 @@ def poly2path(vertices: numpy.ndarray) -> str:
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    Create an SVG path string from an Nx2 list of vertices.
 | 
					    Create an SVG path string from an Nx2 list of vertices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param vertices: Nx2 array of vertices.
 | 
					    Args:
 | 
				
			||||||
    :return: SVG path-string.
 | 
					        vertices: Nx2 array of vertices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        SVG path-string.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1])
 | 
					    commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1])
 | 
				
			||||||
    for vertex in vertices[1:]:
 | 
					    for vertex in vertices[1:]:
 | 
				
			||||||
 | 
				
			|||||||
@ -7,16 +7,16 @@ from typing import Set, Tuple, List
 | 
				
			|||||||
from masque.pattern import Pattern
 | 
					from masque.pattern import Pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str:
 | 
					def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Create a name using pattern.name, id(pattern), and the dose multiplier.
 | 
					    Create a name using `pattern.name`, `id(pattern)`, and the dose multiplier.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param pattern: Pattern whose name we want to mangle.
 | 
					    Args:
 | 
				
			||||||
    :param dose_multiplier: Dose multiplier to mangle with.
 | 
					        pattern: Pattern whose name we want to mangle.
 | 
				
			||||||
    :return: Mangled name.
 | 
					        dose_multiplier: Dose multiplier to mangle with.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        Mangled name.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    expression = re.compile('[^A-Za-z0-9_\?\$]')
 | 
					    expression = re.compile('[^A-Za-z0-9_\?\$]')
 | 
				
			||||||
    full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern))
 | 
					    full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern))
 | 
				
			||||||
@ -26,17 +26,127 @@ def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[Tuple[int, float]]:
 | 
					def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[Tuple[int, float]]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Create a set containing (id(pat), written_dose) for each pattern (including subpatterns)
 | 
					    Create a set containing `(id(pat), written_dose)` for each pattern (including subpatterns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param pattern: Source Patterns.
 | 
					    Args:
 | 
				
			||||||
    :param dose_multiplier: Multiplier for all written_dose entries.
 | 
					        pattern: Source Patterns.
 | 
				
			||||||
    :return: {(id(subpat.pattern), written_dose), ...}
 | 
					        dose_multiplier: Multiplier for all written_dose entries.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        `{(id(subpat.pattern), written_dose), ...}`
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    dose_table = {(id(pattern), dose_multiplier) for pattern in patterns}
 | 
					    dose_table = {(id(pattern), dose_multiplier) for pattern in patterns}
 | 
				
			||||||
    for pattern in patterns:
 | 
					    for pattern in patterns:
 | 
				
			||||||
        for subpat in pattern.subpatterns:
 | 
					        for subpat in pattern.subpatterns:
 | 
				
			||||||
 | 
					            if subpat.pattern is None:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
            subpat_dose_entry = (id(subpat.pattern), subpat.dose * dose_multiplier)
 | 
					            subpat_dose_entry = (id(subpat.pattern), subpat.dose * dose_multiplier)
 | 
				
			||||||
            if subpat_dose_entry not in dose_table:
 | 
					            if subpat_dose_entry not in dose_table:
 | 
				
			||||||
                subpat_dose_table = make_dose_table([subpat.pattern], subpat.dose * dose_multiplier)
 | 
					                subpat_dose_table = make_dose_table([subpat.pattern], subpat.dose * dose_multiplier)
 | 
				
			||||||
                dose_table = dose_table.union(subpat_dose_table)
 | 
					                dose_table = dose_table.union(subpat_dose_table)
 | 
				
			||||||
    return dose_table
 | 
					    return dose_table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def dtype2dose(pattern: Pattern) -> Pattern:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    For each shape in the pattern, if the layer is a tuple, set the
 | 
				
			||||||
 | 
					      layer to the tuple's first element and set the dose to the
 | 
				
			||||||
 | 
					      tuple's second element.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Generally intended for use with `Pattern.apply()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        pattern: Pattern to modify
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        pattern
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    for shape in pattern.shapes:
 | 
				
			||||||
 | 
					        if isinstance(shape.layer, tuple):
 | 
				
			||||||
 | 
					            shape.dose = shape.layer[1]
 | 
				
			||||||
 | 
					            shape.layer = shape.layer[0]
 | 
				
			||||||
 | 
					    return pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def dose2dtype(patterns: List[Pattern],
 | 
				
			||||||
 | 
					               ) -> Tuple[List[Pattern], List[float]]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					     For each shape in each pattern, set shape.layer to the tuple
 | 
				
			||||||
 | 
					     (base_layer, datatype), where:
 | 
				
			||||||
 | 
					        layer is chosen to be equal to the original shape.layer if it is an int,
 | 
				
			||||||
 | 
					            or shape.layer[0] if it is a tuple. `str` layers raise a PatterError.
 | 
				
			||||||
 | 
					        datatype is chosen arbitrarily, based on calcualted dose for each shape.
 | 
				
			||||||
 | 
					            Shapes with equal calcualted dose will have the same datatype.
 | 
				
			||||||
 | 
					            A list of doses is retured, providing a mapping between datatype
 | 
				
			||||||
 | 
					            (list index) and dose (list entry).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Note that this function modifies the input Pattern(s).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        patterns: A `Pattern` or list of patterns to write to file. Modified by this function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        (patterns, dose_list)
 | 
				
			||||||
 | 
					            patterns: modified input patterns
 | 
				
			||||||
 | 
					            dose_list: A list of doses, providing a mapping between datatype (int, list index)
 | 
				
			||||||
 | 
					                       and dose (float, list entry).
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    # Get a dict of id(pattern) -> pattern
 | 
				
			||||||
 | 
					    patterns_by_id = {id(pattern): pattern for pattern in patterns}
 | 
				
			||||||
 | 
					    for pattern in patterns:
 | 
				
			||||||
 | 
					        for i, p in pattern.referenced_patterns_by_id().items():
 | 
				
			||||||
 | 
					            patterns_by_id[i] = p
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Get a table of (id(pat), written_dose) for each pattern and subpattern
 | 
				
			||||||
 | 
					    sd_table = make_dose_table(patterns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Figure out all the unique doses necessary to write this pattern
 | 
				
			||||||
 | 
					    #  This means going through each row in sd_table and adding the dose values needed to write
 | 
				
			||||||
 | 
					    #  that subpattern at that dose level
 | 
				
			||||||
 | 
					    dose_vals = set()
 | 
				
			||||||
 | 
					    for pat_id, pat_dose in sd_table:
 | 
				
			||||||
 | 
					        pat = patterns_by_id[pat_id]
 | 
				
			||||||
 | 
					        for shape in pat.shapes:
 | 
				
			||||||
 | 
					            dose_vals.add(shape.dose * pat_dose)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create a new pattern for each non-1-dose entry in the dose table
 | 
				
			||||||
 | 
					    #   and update the shapes to reflect their new dose
 | 
				
			||||||
 | 
					    new_pats = {} # (id, dose) -> new_pattern mapping
 | 
				
			||||||
 | 
					    for pat_id, pat_dose in sd_table:
 | 
				
			||||||
 | 
					        if pat_dose == 1:
 | 
				
			||||||
 | 
					            new_pats[(pat_id, pat_dose)] = patterns_by_id[pat_id]
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        old_pat = patterns_by_id[pat_id]
 | 
				
			||||||
 | 
					        pat = old_pat.copy() # keep old subpatterns
 | 
				
			||||||
 | 
					        pat.shapes = copy.deepcopy(old_pat.shapes)
 | 
				
			||||||
 | 
					        pat.labels = copy.deepcopy(old_pat.labels)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        encoded_name = mangle_name(pat, pat_dose)
 | 
				
			||||||
 | 
					        if len(encoded_name) == 0:
 | 
				
			||||||
 | 
					            raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name))
 | 
				
			||||||
 | 
					        pat.name = encoded_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for shape in pat.shapes:
 | 
				
			||||||
 | 
					            data_type = dose_vals_list.index(shape.dose * pat_dose)
 | 
				
			||||||
 | 
					            if isinstance(shape.layer, int):
 | 
				
			||||||
 | 
					                shape.layer = (shape.layer, data_type)
 | 
				
			||||||
 | 
					            elif isinstance(shape.layer, tuple):
 | 
				
			||||||
 | 
					                shape.layer = (shape.layer[0], data_type)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                raise PatternError(f'Invalid layer for gdsii: {shape.layer}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new_pats[(pat_id, pat_dose)] = pat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Go back through all the dose-specific patterns and fix up their subpattern entries
 | 
				
			||||||
 | 
					    for (pat_id, pat_dose), pat in new_pats.items():
 | 
				
			||||||
 | 
					        for subpat in pat.subpatterns:
 | 
				
			||||||
 | 
					            dose_mult = subpat.dose * pat_dose
 | 
				
			||||||
 | 
					            subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return patterns, dose_vals_list
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										123
									
								
								masque/label.py
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								masque/label.py
									
									
									
									
									
								
							@ -1,28 +1,37 @@
 | 
				
			|||||||
from typing import List, Tuple
 | 
					from typing import List, Tuple, Dict
 | 
				
			||||||
import copy
 | 
					import copy
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
from numpy import pi
 | 
					from numpy import pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import PatternError
 | 
					from .error import PatternError, PatternLockedError
 | 
				
			||||||
from .utils import is_scalar, vector2, rotation_matrix_2d
 | 
					from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Label:
 | 
					class Label:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    A circle, which has a position and radius.
 | 
					    A text annotation with a position and layer (but no size; it is not drawn)
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # [x_offset, y_offset]
 | 
					    _offset: numpy.ndarray
 | 
				
			||||||
    _offset = numpy.array([0.0, 0.0])   # type: numpy.ndarray
 | 
					    """ [x_offset, y_offset] """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Layer (integer >= 0)
 | 
					    _layer: layer_t
 | 
				
			||||||
    _layer = 0                          # type: int or Tuple
 | 
					    """ Layer (integer >= 0, or 2-Tuple of integers) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Label string
 | 
					    _string: str
 | 
				
			||||||
    _string = None                      # type: str
 | 
					    """ Label string """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    identifier: Tuple
 | 
				
			||||||
 | 
					    """ Arbitrary identifier tuple, useful for keeping track of history when flattening """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    locked: bool
 | 
				
			||||||
 | 
					    """ If `True`, any changes to the label will raise a `PatternLockedError` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __setattr__(self, name, value):
 | 
				
			||||||
 | 
					        if self.locked and name != 'locked':
 | 
				
			||||||
 | 
					            raise PatternLockedError()
 | 
				
			||||||
 | 
					        object.__setattr__(self, name, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ---- Properties
 | 
					    # ---- Properties
 | 
				
			||||||
    # offset property
 | 
					    # offset property
 | 
				
			||||||
@ -30,8 +39,6 @@ class Label:
 | 
				
			|||||||
    def offset(self) -> numpy.ndarray:
 | 
					    def offset(self) -> numpy.ndarray:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        [x, y] offset
 | 
					        [x, y] offset
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: [x_offset, y_offset]
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._offset
 | 
					        return self._offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -42,20 +49,18 @@ class Label:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if val.size != 2:
 | 
					        if val.size != 2:
 | 
				
			||||||
            raise PatternError('Offset must be convertible to size-2 ndarray')
 | 
					            raise PatternError('Offset must be convertible to size-2 ndarray')
 | 
				
			||||||
        self._offset = val.flatten()
 | 
					        self._offset = val.flatten().astype(float)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # layer property
 | 
					    # layer property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def layer(self) -> int or Tuple[int]:
 | 
					    def layer(self) -> layer_t:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Layer number (int or tuple of ints)
 | 
					        Layer number or name (int, tuple of ints, or string)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: Layer
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._layer
 | 
					        return self._layer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @layer.setter
 | 
					    @layer.setter
 | 
				
			||||||
    def layer(self, val: int or List[int]):
 | 
					    def layer(self, val: layer_t):
 | 
				
			||||||
        self._layer = val
 | 
					        self._layer = val
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # string property
 | 
					    # string property
 | 
				
			||||||
@ -63,8 +68,6 @@ class Label:
 | 
				
			|||||||
    def string(self) -> str:
 | 
					    def string(self) -> str:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Label string (str)
 | 
					        Label string (str)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: string
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._string
 | 
					        return self._string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -74,39 +77,58 @@ class Label:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __init__(self,
 | 
					    def __init__(self,
 | 
				
			||||||
                 string: str,
 | 
					                 string: str,
 | 
				
			||||||
                 offset: vector2=(0.0, 0.0),
 | 
					                 offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                 layer: int=0):
 | 
					                 layer: layer_t = 0,
 | 
				
			||||||
 | 
					                 locked: bool = False):
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.identifier = ()
 | 
				
			||||||
        self.string = string
 | 
					        self.string = string
 | 
				
			||||||
        self.offset = numpy.array(offset, dtype=float)
 | 
					        self.offset = numpy.array(offset, dtype=float, copy=True)
 | 
				
			||||||
        self.layer = layer
 | 
					        self.layer = layer
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __copy__(self) -> 'Label':
 | 
				
			||||||
 | 
					        return Label(string=self.string,
 | 
				
			||||||
 | 
					                     offset=self.offset.copy(),
 | 
				
			||||||
 | 
					                     layer=self.layer,
 | 
				
			||||||
 | 
					                     locked=self.locked)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __deepcopy__(self, memo: Dict = None) -> 'Label':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = copy.copy(self).unlock()
 | 
				
			||||||
 | 
					        new._offset = self._offset.copy()
 | 
				
			||||||
 | 
					        new.locked = self.locked
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ---- Non-abstract methods
 | 
					 | 
				
			||||||
    def copy(self) -> 'Label':
 | 
					    def copy(self) -> 'Label':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns a deep copy of the shape.
 | 
					        Returns a deep copy of the label.
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: Deep copy of self
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return copy.deepcopy(self)
 | 
					        return copy.deepcopy(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def translate(self, offset: vector2) -> 'Label':
 | 
					    def translate(self, offset: vector2) -> 'Label':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Translate the shape by the given offset
 | 
					        Translate the label by the given offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param offset: [x_offset, y,offset]
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            offset: [x_offset, y,offset]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.offset += offset
 | 
					        self.offset += offset
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def rotate_around(self, pivot: vector2, rotation: float) -> 'Label':
 | 
					    def rotate_around(self, pivot: vector2, rotation: float) -> 'Label':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate the shape around a point.
 | 
					        Rotate the label around a point.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param pivot: Point (x, y) to rotate around
 | 
					        Args:
 | 
				
			||||||
        :param rotation: Angle to rotate by (counterclockwise, radians)
 | 
					            pivot: Point (x, y) to rotate around
 | 
				
			||||||
        :return: self
 | 
					            rotation: Angle to rotate by (counterclockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pivot = numpy.array(pivot, dtype=float)
 | 
					        pivot = numpy.array(pivot, dtype=float)
 | 
				
			||||||
        self.translate(-pivot)
 | 
					        self.translate(-pivot)
 | 
				
			||||||
@ -122,8 +144,33 @@ class Label:
 | 
				
			|||||||
        bounds = [self.offset,
 | 
					        bounds = [self.offset,
 | 
				
			||||||
                  self.offset]
 | 
					                  self.offset]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: Bounds [[xmin, xmax], [ymin, ymax]]
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Bounds [[xmin, xmax], [ymin, ymax]]
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return numpy.array([self.offset, self.offset])
 | 
					        return numpy.array([self.offset, self.offset])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self) -> 'Label':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Lock the Label, causing any modifications to raise an exception.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Return:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = False
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', True)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self) -> 'Label':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Unlock the Label, re-allowing changes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Return:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = True
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        return f'<Label "{self.string}" l{self.layer} o{self.offset}{locked}>'
 | 
				
			||||||
 | 
				
			|||||||
@ -1,58 +1,77 @@
 | 
				
			|||||||
"""
 | 
					"""
 | 
				
			||||||
 Base object for containing a lithography mask.
 | 
					 Base object representing a lithography mask.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from typing import List, Callable, Tuple, Dict, Union
 | 
					from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type, overload
 | 
				
			||||||
 | 
					from typing import MutableMapping, Iterable
 | 
				
			||||||
import copy
 | 
					import copy
 | 
				
			||||||
import itertools
 | 
					import itertools
 | 
				
			||||||
import pickle
 | 
					import pickle
 | 
				
			||||||
from collections import defaultdict
 | 
					from collections import defaultdict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
 | 
					from numpy import inf
 | 
				
			||||||
# .visualize imports matplotlib and matplotlib.collections
 | 
					# .visualize imports matplotlib and matplotlib.collections
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .subpattern import SubPattern
 | 
					from .subpattern import SubPattern, subpattern_t
 | 
				
			||||||
from .repetition import GridRepetition
 | 
					from .repetition import GridRepetition
 | 
				
			||||||
from .shapes import Shape, Polygon
 | 
					from .shapes import Shape, Polygon
 | 
				
			||||||
from .label import Label
 | 
					from .label import Label
 | 
				
			||||||
from .utils import rotation_matrix_2d, vector2
 | 
					from .utils import rotation_matrix_2d, vector2, normalize_mirror
 | 
				
			||||||
from .error import PatternError
 | 
					from .error import PatternError, PatternLockedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					
 | 
				
			||||||
 | 
					visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Pattern:
 | 
					class Pattern:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    2D layout consisting of some set of shapes and references to other Pattern objects
 | 
					    2D layout consisting of some set of shapes, labels, and references to other Pattern objects
 | 
				
			||||||
     (via SubPattern). Shapes are assumed to inherit from .shapes.Shape or provide equivalent
 | 
					     (via SubPattern and GridRepetition). Shapes are assumed to inherit from
 | 
				
			||||||
     functions.
 | 
					     masque.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 = None               # type: List[Shape]
 | 
					    __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked')
 | 
				
			||||||
    labels = None               # type: List[Labels]
 | 
					
 | 
				
			||||||
    subpatterns = None          # type: List[SubPattern or GridRepetition]
 | 
					    shapes: List[Shape]
 | 
				
			||||||
    name = None                 # type: str
 | 
					    """ List of all shapes in this Pattern.
 | 
				
			||||||
 | 
					    Elements in this list are assumed to inherit from Shape or provide equivalent functions.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    labels: List[Label]
 | 
				
			||||||
 | 
					    """ List of all labels in this Pattern. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    subpatterns: List[subpattern_t]
 | 
				
			||||||
 | 
					    """ List of all objects referencing other patterns in this Pattern.
 | 
				
			||||||
 | 
					    Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays")
 | 
				
			||||||
 | 
					    Multiple objects in this list may reference the same Pattern object
 | 
				
			||||||
 | 
					    (multiple instances of the same object).
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name: str
 | 
				
			||||||
 | 
					    """ A name for this pattern """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    locked: bool
 | 
				
			||||||
 | 
					    """ When the pattern is locked, no changes may be made. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self,
 | 
					    def __init__(self,
 | 
				
			||||||
                 shapes: List[Shape]=(),
 | 
					                 name: str = '',
 | 
				
			||||||
                 labels: List[Label]=(),
 | 
					                 shapes: Sequence[Shape] = (),
 | 
				
			||||||
                 subpatterns: List[SubPattern]=(),
 | 
					                 labels: Sequence[Label] = (),
 | 
				
			||||||
                 name: str='',
 | 
					                 subpatterns: Sequence[subpattern_t] = (),
 | 
				
			||||||
 | 
					                 locked: bool = False,
 | 
				
			||||||
                 ):
 | 
					                 ):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Basic init; arguments get assigned to member variables.
 | 
					        Basic init; arguments get assigned to member variables.
 | 
				
			||||||
         Non-list inputs for shapes and subpatterns get converted to lists.
 | 
					         Non-list inputs for shapes and subpatterns get converted to lists.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param shapes: Initial shapes in the Pattern
 | 
					        Args:
 | 
				
			||||||
        :param labels: Initial labels in the Pattern
 | 
					            shapes: Initial shapes in the Pattern
 | 
				
			||||||
        :param subpatterns: Initial subpatterns in the Pattern
 | 
					            labels: Initial labels in the Pattern
 | 
				
			||||||
        :param name: An identifier for the Pattern
 | 
					            subpatterns: Initial subpatterns in the Pattern
 | 
				
			||||||
 | 
					            name: An identifier for the Pattern
 | 
				
			||||||
 | 
					            locked: Whether to lock the pattern after construction
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
        if isinstance(shapes, list):
 | 
					        if isinstance(shapes, list):
 | 
				
			||||||
            self.shapes = shapes
 | 
					            self.shapes = shapes
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
@ -69,14 +88,39 @@ class Pattern:
 | 
				
			|||||||
            self.subpatterns = list(subpatterns)
 | 
					            self.subpatterns = list(subpatterns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.name = name
 | 
					        self.name = name
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __setattr__(self, name, value):
 | 
				
			||||||
 | 
					        if self.locked and name != 'locked':
 | 
				
			||||||
 | 
					            raise PatternLockedError()
 | 
				
			||||||
 | 
					        object.__setattr__(self, name, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __copy__(self, memo: Dict = None) -> 'Pattern':
 | 
				
			||||||
 | 
					        return Pattern(name=self.name,
 | 
				
			||||||
 | 
					                       shapes=copy.deepcopy(self.shapes),
 | 
				
			||||||
 | 
					                       labels=copy.deepcopy(self.labels),
 | 
				
			||||||
 | 
					                       subpatterns=[copy.copy(sp) for sp in self.subpatterns],
 | 
				
			||||||
 | 
					                       locked=self.locked)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __deepcopy__(self, memo: Dict = None) -> 'Pattern':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = Pattern(name=self.name,
 | 
				
			||||||
 | 
					                shapes=copy.deepcopy(self.shapes, memo),
 | 
				
			||||||
 | 
					                labels=copy.deepcopy(self.labels, memo),
 | 
				
			||||||
 | 
					                subpatterns=copy.deepcopy(self.subpatterns, memo),
 | 
				
			||||||
 | 
					                locked=self.locked)
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def append(self, other_pattern: 'Pattern') -> 'Pattern':
 | 
					    def append(self, other_pattern: 'Pattern') -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Appends all shapes, labels and subpatterns from other_pattern to self's shapes,
 | 
					        Appends all shapes, labels and subpatterns from other_pattern to self's shapes,
 | 
				
			||||||
          labels, and supbatterns.
 | 
					          labels, and supbatterns.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param other_pattern: The Pattern to append
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					           other_pattern: The Pattern to append
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.subpatterns += other_pattern.subpatterns
 | 
					        self.subpatterns += other_pattern.subpatterns
 | 
				
			||||||
        self.shapes += other_pattern.shapes
 | 
					        self.shapes += other_pattern.shapes
 | 
				
			||||||
@ -84,28 +128,33 @@ class Pattern:
 | 
				
			|||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def subset(self,
 | 
					    def subset(self,
 | 
				
			||||||
               shapes_func: Callable[[Shape], bool]=None,
 | 
					               shapes_func: Callable[[Shape], bool] = None,
 | 
				
			||||||
               labels_func: Callable[[Label], bool]=None,
 | 
					               labels_func: Callable[[Label], bool] = None,
 | 
				
			||||||
               subpatterns_func: Callable[[SubPattern], bool]=None,
 | 
					               subpatterns_func: Callable[[subpattern_t], bool] = None,
 | 
				
			||||||
               recursive: bool=False,
 | 
					               recursive: bool = False,
 | 
				
			||||||
               ) -> 'Pattern':
 | 
					               ) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns a Pattern containing only the entities (e.g. shapes) for which the
 | 
					        Returns a Pattern containing only the entities (e.g. shapes) for which the
 | 
				
			||||||
          given entity_func returns True.
 | 
					          given entity_func returns True.
 | 
				
			||||||
        Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied.
 | 
					        Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param shapes_func: Given a shape, returns a boolean denoting whether the shape is a member
 | 
					        Args:
 | 
				
			||||||
             of the subset. Default always returns False.
 | 
					            shapes_func: Given a shape, returns a boolean denoting whether the shape is a member
 | 
				
			||||||
        :param labels_func: Given a label, returns a boolean denoting whether the label is a member
 | 
					                of the subset. Default always returns False.
 | 
				
			||||||
             of the subset. Default always returns False.
 | 
					            labels_func: Given a label, returns a boolean denoting whether the label is a member
 | 
				
			||||||
        :param subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member
 | 
					                of the subset. Default always returns False.
 | 
				
			||||||
             of the subset. Default always returns False.
 | 
					            subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member
 | 
				
			||||||
        :param recursive: If True, also calls .subset() recursively on patterns referenced by this
 | 
					                of the subset. Default always returns False.
 | 
				
			||||||
             pattern.
 | 
					            recursive: If True, also calls .subset() recursively on patterns referenced by this
 | 
				
			||||||
        :return: A Pattern containing all the shapes and subpatterns for which the parameter
 | 
					                pattern.
 | 
				
			||||||
             functions return True
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            A Pattern containing all the shapes and subpatterns for which the parameter
 | 
				
			||||||
 | 
					                functions return True
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        def do_subset(src):
 | 
					        def do_subset(src: Optional['Pattern']) -> Optional['Pattern']:
 | 
				
			||||||
 | 
					            if src is None:
 | 
				
			||||||
 | 
					                return None
 | 
				
			||||||
            pat = Pattern(name=src.name)
 | 
					            pat = Pattern(name=src.name)
 | 
				
			||||||
            if shapes_func is not None:
 | 
					            if shapes_func is not None:
 | 
				
			||||||
                pat.shapes = [s for s in src.shapes if shapes_func(s)]
 | 
					                pat.shapes = [s for s in src.shapes if shapes_func(s)]
 | 
				
			||||||
@ -119,12 +168,14 @@ class Pattern:
 | 
				
			|||||||
            pat = self.apply(do_subset)
 | 
					            pat = self.apply(do_subset)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            pat = do_subset(self)
 | 
					            pat = do_subset(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert(pat is not None)
 | 
				
			||||||
        return pat
 | 
					        return pat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def apply(self,
 | 
					    def apply(self,
 | 
				
			||||||
              func: Callable[['Pattern'], 'Pattern'],
 | 
					              func: Callable[[Optional['Pattern']], Optional['Pattern']],
 | 
				
			||||||
              memo: Dict[int, 'Pattern']=None,
 | 
					              memo: Optional[Dict[int, Optional['Pattern']]] = None,
 | 
				
			||||||
              ) -> 'Pattern':
 | 
					              ) -> Optional['Pattern']:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Recursively apply func() to this pattern and any pattern it references.
 | 
					        Recursively apply func() to this pattern and any pattern it references.
 | 
				
			||||||
        func() is expected to take and return a Pattern.
 | 
					        func() is expected to take and return a Pattern.
 | 
				
			||||||
@ -132,12 +183,17 @@ class Pattern:
 | 
				
			|||||||
        It is only applied to any given pattern once, regardless of how many times it is
 | 
					        It is only applied to any given pattern once, regardless of how many times it is
 | 
				
			||||||
            referenced.
 | 
					            referenced.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param func: Function which accepts a Pattern, and returns a pattern.
 | 
					        Args:
 | 
				
			||||||
        :param memo: Dictionary used to avoid re-running on multiply-referenced patterns.
 | 
					            func: Function which accepts a Pattern, and returns a pattern.
 | 
				
			||||||
            Stores {id(pattern): func(pattern)} for patterns which have already been processed.
 | 
					            memo: Dictionary used to avoid re-running on multiply-referenced patterns.
 | 
				
			||||||
            Default None (no already-processed patterns).
 | 
					                Stores `{id(pattern): func(pattern)}` for patterns which have already been processed.
 | 
				
			||||||
        :return: The result of applying func() to this pattern and all subpatterns.
 | 
					                Default `None` (no already-processed patterns).
 | 
				
			||||||
        :raises: PatternError if called on a pattern containing a circular reference.
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            The result of applying func() to this pattern and all subpatterns.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Raises:
 | 
				
			||||||
 | 
					            PatternError if called on a pattern containing a circular reference.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if memo is None:
 | 
					        if memo is None:
 | 
				
			||||||
            memo = {}
 | 
					            memo = {}
 | 
				
			||||||
@ -146,8 +202,12 @@ class Pattern:
 | 
				
			|||||||
        if pat_id not in memo:
 | 
					        if pat_id not in memo:
 | 
				
			||||||
            memo[pat_id] = None
 | 
					            memo[pat_id] = None
 | 
				
			||||||
            pat = func(self)
 | 
					            pat = func(self)
 | 
				
			||||||
            for subpat in pat.subpatterns:
 | 
					            if pat is not None:
 | 
				
			||||||
                subpat.pattern = subpat.pattern.apply(func, memo)
 | 
					                for subpat in pat.subpatterns:
 | 
				
			||||||
 | 
					                    if subpat.pattern is None:
 | 
				
			||||||
 | 
					                        subpat.pattern = func(None)
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        subpat.pattern = subpat.pattern.apply(func, memo)
 | 
				
			||||||
            memo[pat_id] = pat
 | 
					            memo[pat_id] = pat
 | 
				
			||||||
        elif memo[pat_id] is None:
 | 
					        elif memo[pat_id] is None:
 | 
				
			||||||
            raise PatternError('.apply() called on pattern with circular reference')
 | 
					            raise PatternError('.apply() called on pattern with circular reference')
 | 
				
			||||||
@ -155,41 +215,130 @@ class Pattern:
 | 
				
			|||||||
            pat = memo[pat_id]
 | 
					            pat = memo[pat_id]
 | 
				
			||||||
        return pat
 | 
					        return pat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def dfs(self,
 | 
				
			||||||
 | 
					            visit_before: visitor_function_t = None,
 | 
				
			||||||
 | 
					            visit_after: visitor_function_t = None,
 | 
				
			||||||
 | 
					            transform: Union[numpy.ndarray, bool, None] = False,
 | 
				
			||||||
 | 
					            memo: Optional[Dict] = None,
 | 
				
			||||||
 | 
					            hierarchy: Tuple['Pattern', ...] = (),
 | 
				
			||||||
 | 
					            ) -> 'Pattern':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Experimental convenience function.
 | 
				
			||||||
 | 
					        Performs a depth-first traversal of this pattern and its subpatterns.
 | 
				
			||||||
 | 
					        At each pattern in the tree, the following sequence is called:
 | 
				
			||||||
 | 
					            ```
 | 
				
			||||||
 | 
					            current_pattern = visit_before(current_pattern, **vist_args)
 | 
				
			||||||
 | 
					            for sp in current_pattern.subpatterns]
 | 
				
			||||||
 | 
					                sp.pattern = sp.pattern.df(visit_before, visit_after, updated_transform,
 | 
				
			||||||
 | 
					                                           memo, (current_pattern,) + hierarchy)
 | 
				
			||||||
 | 
					            current_pattern = visit_after(current_pattern, **visit_args)
 | 
				
			||||||
 | 
					            ```
 | 
				
			||||||
 | 
					          where `visit_args` are
 | 
				
			||||||
 | 
					            `hierarchy`:  (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern)
 | 
				
			||||||
 | 
					                          tuple of all parent-and-higher patterns
 | 
				
			||||||
 | 
					            `transform`:  numpy.ndarray containing cumulative
 | 
				
			||||||
 | 
					                          [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
 | 
				
			||||||
 | 
					                          for the instance being visited
 | 
				
			||||||
 | 
					            `memo`:  Arbitrary dict (not altered except by visit_*())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            visit_before: Function to call before traversing subpatterns.
 | 
				
			||||||
 | 
					                Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
 | 
				
			||||||
 | 
					                pattern. Default `None` (not called).
 | 
				
			||||||
 | 
					            visit_after: Function to call after traversing subpatterns.
 | 
				
			||||||
 | 
					                Should accept a Pattern and **visit_args, and return the (possibly modified)
 | 
				
			||||||
 | 
					                pattern. Default `None` (not called).
 | 
				
			||||||
 | 
					            transform: Initial value for `visit_args['transform']`.
 | 
				
			||||||
 | 
					                Can be `False`, in which case the transform is not calculated.
 | 
				
			||||||
 | 
					                `True` or `None` is interpreted as `[0, 0, 0, 0]`.
 | 
				
			||||||
 | 
					            memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict).
 | 
				
			||||||
 | 
					            hierarchy: Tuple of patterns specifying the hierarchy above the current pattern.
 | 
				
			||||||
 | 
					                Appended to the start of the generated `visit_args['hierarchy']`.
 | 
				
			||||||
 | 
					                Default is an empty tuple.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            The result, including `visit_before(self, ...)` and `visit_after(self, ...)`.
 | 
				
			||||||
 | 
					            Note that `self` may also be altered!
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if memo is None:
 | 
				
			||||||
 | 
					            memo = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if transform is None or transform is True:
 | 
				
			||||||
 | 
					            transform = numpy.zeros(4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self in hierarchy:
 | 
				
			||||||
 | 
					            raise PatternError('.dfs() called on pattern with circular reference')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pat = self
 | 
				
			||||||
 | 
					        if visit_before is not None:
 | 
				
			||||||
 | 
					            pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform)        # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for subpattern in self.subpatterns:
 | 
				
			||||||
 | 
					            if transform is not False:
 | 
				
			||||||
 | 
					                sign = numpy.ones(2)
 | 
				
			||||||
 | 
					                if transform[3]:
 | 
				
			||||||
 | 
					                   sign[1] = -1
 | 
				
			||||||
 | 
					                xy = numpy.dot(rotation_matrix_2d(transform[2]), subpattern.offset * sign)
 | 
				
			||||||
 | 
					                mirror_x, angle = normalize_mirror(subpattern.mirrored)
 | 
				
			||||||
 | 
					                angle += subpattern.rotation
 | 
				
			||||||
 | 
					                sp_transform = transform + (xy[0], xy[1], angle, mirror_x)
 | 
				
			||||||
 | 
					                sp_transform[3] %= 2
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                sp_transform = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if subpattern.pattern is not None:
 | 
				
			||||||
 | 
					                subpattern.pattern = subpattern.pattern.dfs(visit_before=visit_before,
 | 
				
			||||||
 | 
					                                                            visit_after=visit_after,
 | 
				
			||||||
 | 
					                                                            transform=sp_transform,
 | 
				
			||||||
 | 
					                                                            memo=memo,
 | 
				
			||||||
 | 
					                                                            hierarchy=hierarchy + (self,))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if visit_after is not None:
 | 
				
			||||||
 | 
					            pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform)         # type: ignore
 | 
				
			||||||
 | 
					        return pat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def polygonize(self,
 | 
					    def polygonize(self,
 | 
				
			||||||
                   poly_num_points: int=None,
 | 
					                   poly_num_points: Optional[int] = None,
 | 
				
			||||||
                   poly_max_arclen: float=None
 | 
					                   poly_max_arclen: Optional[float] = None,
 | 
				
			||||||
                   ) -> 'Pattern':
 | 
					                   ) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Calls .to_polygons(...) on all the shapes in this Pattern and any referenced patterns,
 | 
					        Calls `.to_polygons(...)` on all the shapes in this Pattern and any referenced patterns,
 | 
				
			||||||
         replacing them with the returned polygons.
 | 
					         replacing them with the returned polygons.
 | 
				
			||||||
        Arguments are passed directly to shape.to_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
 | 
					        Args:
 | 
				
			||||||
             poly_max_arclen if that results in more points. Optional, defaults to shapes'
 | 
					            poly_num_points: Number of points to use for each polygon. Can be overridden by
 | 
				
			||||||
             internal defaults.
 | 
					                `poly_max_arclen` if that results in more points. Optional, defaults to shapes'
 | 
				
			||||||
        :param poly_max_arclen: Maximum arclength which can be approximated by a single line
 | 
					                internal defaults.
 | 
				
			||||||
 | 
					            poly_max_arclen: Maximum arclength which can be approximated by a single line
 | 
				
			||||||
             segment. Optional, defaults to shapes' internal defaults.
 | 
					             segment. Optional, defaults to shapes' internal defaults.
 | 
				
			||||||
        :return: self
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        old_shapes = self.shapes
 | 
					        old_shapes = self.shapes
 | 
				
			||||||
        self.shapes = list(itertools.chain.from_iterable(
 | 
					        self.shapes = list(itertools.chain.from_iterable(
 | 
				
			||||||
                        (shape.to_polygons(poly_num_points, poly_max_arclen)
 | 
					                        (shape.to_polygons(poly_num_points, poly_max_arclen)
 | 
				
			||||||
                         for shape in old_shapes)))
 | 
					                         for shape in old_shapes)))
 | 
				
			||||||
        for subpat in self.subpatterns:
 | 
					        for subpat in self.subpatterns:
 | 
				
			||||||
            subpat.pattern.polygonize(poly_num_points, poly_max_arclen)
 | 
					            if subpat.pattern is not None:
 | 
				
			||||||
 | 
					                subpat.pattern.polygonize(poly_num_points, poly_max_arclen)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def manhattanize(self,
 | 
					    def manhattanize(self,
 | 
				
			||||||
                     grid_x: numpy.ndarray,
 | 
					                     grid_x: numpy.ndarray,
 | 
				
			||||||
                     grid_y: numpy.ndarray
 | 
					                     grid_y: numpy.ndarray,
 | 
				
			||||||
                     ) -> 'Pattern':
 | 
					                     ) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Calls .polygonize() and .flatten on the pattern, then calls .manhattanize() on all the
 | 
					        Calls `.polygonize()` and `.flatten()` on the pattern, then calls `.manhattanize()` on all the
 | 
				
			||||||
         resulting shapes, replacing them with the returned Manhattan polygons.
 | 
					         resulting shapes, replacing them with the returned Manhattan polygons.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
 | 
					        Args:
 | 
				
			||||||
        :param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
 | 
					            grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
 | 
				
			||||||
        :return: self
 | 
					            grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.polygonize().flatten()
 | 
					        self.polygonize().flatten()
 | 
				
			||||||
@ -199,26 +348,30 @@ class Pattern:
 | 
				
			|||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def subpatternize(self,
 | 
					    def subpatternize(self,
 | 
				
			||||||
                      recursive: bool=True,
 | 
					                      recursive: bool = True,
 | 
				
			||||||
                      norm_value: int=1e6,
 | 
					                      norm_value: int = int(1e6),
 | 
				
			||||||
                      exclude_types: Tuple[Shape]=(Polygon,)
 | 
					                      exclude_types: Tuple[Type] = (Polygon,)
 | 
				
			||||||
                      ) -> 'Pattern':
 | 
					                      ) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Iterates through this Pattern and all referenced Patterns. Within each Pattern, it iterates
 | 
					        Iterates through this `Pattern` and all referenced `Pattern`s. Within each `Pattern`, it iterates
 | 
				
			||||||
         over all shapes, calling .normalized_form(norm_value) on them to retrieve a scale-,
 | 
					         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
 | 
					         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
 | 
					         more than once is removed and re-added using subpattern objects referencing a newly-created
 | 
				
			||||||
         Pattern containing only the normalized form of the shape.
 | 
					         `Pattern` containing only the normalized form of the shape.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Note that the default norm_value was chosen to give a reasonable precision when converting
 | 
					        Note:
 | 
				
			||||||
         to GDSII, which uses integer values for pixel coordinates.
 | 
					            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.
 | 
					        Args:
 | 
				
			||||||
        :param norm_value: Passed to shape.normalized_form(norm_value). Default 1e6 (see function
 | 
					            recursive: Whether to call recursively on self's subpatterns. Default `True`.
 | 
				
			||||||
 | 
					            norm_value: Passed to `shape.normalized_form(norm_value)`. Default `1e6` (see function
 | 
				
			||||||
                note about GDSII)
 | 
					                note about GDSII)
 | 
				
			||||||
        :param exclude_types: Shape types passed in this argument are always left untouched, for
 | 
					            exclude_types: Shape types passed in this argument are always left untouched, for
 | 
				
			||||||
                speed or convenience. Default: (Shapes.Polygon,)
 | 
					                speed or convenience. Default: `(shapes.Polygon,)`
 | 
				
			||||||
        :return: self
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if exclude_types is None:
 | 
					        if exclude_types is None:
 | 
				
			||||||
@ -226,14 +379,16 @@ class Pattern:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if recursive:
 | 
					        if recursive:
 | 
				
			||||||
            for subpat in self.subpatterns:
 | 
					            for subpat in self.subpatterns:
 | 
				
			||||||
 | 
					                if subpat.pattern is None:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
                subpat.pattern.subpatternize(recursive=True,
 | 
					                subpat.pattern.subpatternize(recursive=True,
 | 
				
			||||||
                                             norm_value=norm_value,
 | 
					                                             norm_value=norm_value,
 | 
				
			||||||
                                             exclude_types=exclude_types)
 | 
					                                             exclude_types=exclude_types)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Create a dict which uses the label tuple from .normalized_form() as a key, and which
 | 
					        # 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
 | 
					        #  stores `(function_to_create_normalized_shape, [(index_in_shapes, values), ...])`, where
 | 
				
			||||||
        #  values are the (offset, scale, rotation, dose) values as calculated by .normalized_form()
 | 
					        #  values are the `(offset, scale, rotation, dose)` values as calculated by `.normalized_form()`
 | 
				
			||||||
        shape_table = defaultdict(lambda: [None, list()])
 | 
					        shape_table: MutableMapping[Tuple, List] = defaultdict(lambda: [None, list()])
 | 
				
			||||||
        for i, shape in enumerate(self.shapes):
 | 
					        for i, shape in enumerate(self.shapes):
 | 
				
			||||||
            if not any((isinstance(shape, t) for t in exclude_types)):
 | 
					            if not any((isinstance(shape, t) for t in exclude_types)):
 | 
				
			||||||
                label, values, func = shape.normalized_form(norm_value)
 | 
					                label, values, func = shape.normalized_form(norm_value)
 | 
				
			||||||
@ -241,9 +396,9 @@ class Pattern:
 | 
				
			|||||||
                shape_table[label][1].append((i, values))
 | 
					                shape_table[label][1].append((i, values))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Iterate over the normalized shapes in the table. If any normalized shape occurs more than
 | 
					        # 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
 | 
					        #  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
 | 
					        #  entries for each occurrence in self. Also, note down that we should delete the
 | 
				
			||||||
        #  self.shapes entries for which we made SubPatterns.
 | 
					        #  `self.shapes` entries for which we made SubPatterns.
 | 
				
			||||||
        shapes_to_remove = []
 | 
					        shapes_to_remove = []
 | 
				
			||||||
        for label in shape_table:
 | 
					        for label in shape_table:
 | 
				
			||||||
            if len(shape_table[label][1]) > 1:
 | 
					            if len(shape_table[label][1]) > 1:
 | 
				
			||||||
@ -251,9 +406,9 @@ class Pattern:
 | 
				
			|||||||
                pat = Pattern(shapes=[shape])
 | 
					                pat = Pattern(shapes=[shape])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                for i, values in shape_table[label][1]:
 | 
					                for i, values in shape_table[label][1]:
 | 
				
			||||||
                    (offset, scale, rotation, dose) = values
 | 
					                    (offset, scale, rotation, mirror_x, dose) = values
 | 
				
			||||||
                    subpat = SubPattern(pattern=pat, offset=offset, scale=scale,
 | 
					                    subpat = SubPattern(pattern=pat, offset=offset, scale=scale,
 | 
				
			||||||
                                        rotation=rotation, dose=dose)
 | 
					                                        rotation=rotation, dose=dose, mirrored=(mirror_x, False))
 | 
				
			||||||
                    self.subpatterns.append(subpat)
 | 
					                    self.subpatterns.append(subpat)
 | 
				
			||||||
                    shapes_to_remove.append(i)
 | 
					                    shapes_to_remove.append(i)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -267,85 +422,155 @@ class Pattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Represents the pattern as a list of polygons.
 | 
					        Represents the pattern as a list of polygons.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Deep-copies the pattern, then calls .polygonize() and .flatten() on the copy in order to
 | 
					        Deep-copies the pattern, then calls `.polygonize()` and `.flatten()` on the copy in order to
 | 
				
			||||||
         generate the list of polygons.
 | 
					         generate the list of polygons.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: A list of (Ni, 2) numpy.ndarrays specifying vertices of the polygons. Each ndarray
 | 
					        Returns:
 | 
				
			||||||
            is of the form [[x0, y0], [x1, y1],...].
 | 
					            A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray
 | 
				
			||||||
 | 
					             is of the form `[[x0, y0], [x1, y1],...]`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pat = copy.deepcopy(self).polygonize().flatten()
 | 
					        pat = self.deepcopy().deepunlock().polygonize().flatten()
 | 
				
			||||||
        return [shape.vertices + shape.offset for shape in pat.shapes]
 | 
					        return [shape.vertices + shape.offset for shape in pat.shapes]      # type: ignore      # mypy can't figure out that shapes are all Polygons now
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @overload
 | 
				
			||||||
    def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']:
 | 
					    def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @overload
 | 
				
			||||||
 | 
					    def referenced_patterns_by_id(self, include_none: bool) -> Dict[int, Optional['Pattern']]:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def referenced_patterns_by_id(self,
 | 
				
			||||||
 | 
					                                  include_none: bool = False
 | 
				
			||||||
 | 
					                                  ) -> Union[Dict[int, Optional['Pattern']],
 | 
				
			||||||
 | 
					                                             Dict[int, 'Pattern']]:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Create a dictionary of {id(pat): pat} for all Pattern objects referenced by this
 | 
					        Create a dictionary with `{id(pat): pat}` for all Pattern objects referenced by this
 | 
				
			||||||
         Pattern (operates recursively on all referenced Patterns as well)
 | 
					         Pattern (operates recursively on all referenced Patterns as well)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: Dictionary of {id(pat): pat} for all referenced Pattern objects
 | 
					        Args:
 | 
				
			||||||
 | 
					            include_none: If `True`, references to `None` will be included. Default `False`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Dictionary with `{id(pat): pat}` for all referenced Pattern objects
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        ids = {}
 | 
					        ids: Dict[int, Optional['Pattern']] = {}
 | 
				
			||||||
        for subpat in self.subpatterns:
 | 
					        for subpat in self.subpatterns:
 | 
				
			||||||
            if id(subpat.pattern) not in ids:
 | 
					            if id(subpat.pattern) not in ids:
 | 
				
			||||||
                ids[id(subpat.pattern)] = subpat.pattern
 | 
					                if subpat.pattern is not None:
 | 
				
			||||||
                ids.update(subpat.pattern.referenced_patterns_by_id())
 | 
					                    ids[id(subpat.pattern)] = subpat.pattern
 | 
				
			||||||
 | 
					                    ids.update(subpat.pattern.referenced_patterns_by_id())
 | 
				
			||||||
 | 
					                elif include_none:
 | 
				
			||||||
 | 
					                    ids[id(subpat.pattern)] = subpat.pattern
 | 
				
			||||||
        return ids
 | 
					        return ids
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def referenced_patterns_by_name(self, **kwargs) -> List[Tuple[Optional[str], Optional['Pattern']]]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Create a list of `(pat.name, pat)` tuples for all Pattern objects referenced by this
 | 
				
			||||||
 | 
					         Pattern (operates recursively on all referenced Patterns as well).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Note that names are not necessarily unique, so a list of tuples is returned
 | 
				
			||||||
 | 
					         rather than a dict.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            **kwargs: passed to `referenced_patterns_by_id()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            List of `(pat.name, pat)` tuples for all referenced Pattern objects
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        pats_by_id = self.referenced_patterns_by_id(**kwargs)
 | 
				
			||||||
 | 
					        pat_list = [(p.name if p is not None else None, p) for p in pats_by_id.values()]
 | 
				
			||||||
 | 
					        return pat_list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_bounds(self) -> Union[numpy.ndarray, None]:
 | 
					    def get_bounds(self) -> Union[numpy.ndarray, None]:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the
 | 
					        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.
 | 
					         extent of the Pattern's contents in each dimension.
 | 
				
			||||||
        Returns None if the Pattern is empty.
 | 
					        Returns `None` if the Pattern is empty.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: [[x_min, y_min], [x_max, y_max]] or None
 | 
					        Returns:
 | 
				
			||||||
 | 
					            `[[x_min, y_min], [x_max, y_max]]` or `None`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        entries = self.shapes + self.subpatterns + self.labels
 | 
					        entries = self.shapes + self.subpatterns + self.labels
 | 
				
			||||||
        if not entries:
 | 
					        if not entries:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        init_bounds = entries[0].get_bounds()
 | 
					        min_bounds = numpy.array((+inf, +inf))
 | 
				
			||||||
        min_bounds = init_bounds[0, :]
 | 
					        max_bounds = numpy.array((-inf, -inf))
 | 
				
			||||||
        max_bounds = init_bounds[1, :]
 | 
					        for entry in entries:
 | 
				
			||||||
        for entry in entries[1:]:
 | 
					 | 
				
			||||||
            bounds = entry.get_bounds()
 | 
					            bounds = entry.get_bounds()
 | 
				
			||||||
 | 
					            if bounds is None:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
            min_bounds = numpy.minimum(min_bounds, bounds[0, :])
 | 
					            min_bounds = numpy.minimum(min_bounds, bounds[0, :])
 | 
				
			||||||
            max_bounds = numpy.maximum(max_bounds, bounds[1, :])
 | 
					            max_bounds = numpy.maximum(max_bounds, bounds[1, :])
 | 
				
			||||||
        return numpy.vstack((min_bounds, max_bounds))
 | 
					        if (max_bounds < min_bounds).any():
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return numpy.vstack((min_bounds, max_bounds))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def flatten(self) -> 'Pattern':
 | 
					    def flatten(self) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Removes all subpatterns and adds equivalent shapes.
 | 
					        Removes all subpatterns and adds equivalent shapes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: self
 | 
					        Shape identifiers are changed to represent their original position in the
 | 
				
			||||||
 | 
					         pattern hierarchy:
 | 
				
			||||||
 | 
					         `(L1_name (str), L1_index (int), L2_name, L2_index, ..., *original_shape_identifier)`
 | 
				
			||||||
 | 
					         where
 | 
				
			||||||
 | 
					         `L1_name` is the first-level subpattern's name (e.g. `self.subpatterns[0].pattern.name`),
 | 
				
			||||||
 | 
					         `L2_name` is the next-level subpattern's name (e.g.
 | 
				
			||||||
 | 
					            `self.subpatterns[0].pattern.subpatterns[0].pattern.name`) and
 | 
				
			||||||
 | 
					         `L1_index` is an integer used to differentiate between multiple instance ofi the same
 | 
				
			||||||
 | 
					            (or same-named) subpatterns.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        subpatterns = copy.deepcopy(self.subpatterns)
 | 
					        subpatterns = copy.deepcopy(self.subpatterns)
 | 
				
			||||||
        self.subpatterns = []
 | 
					        self.subpatterns = []
 | 
				
			||||||
 | 
					        shape_counts: Dict[Tuple, int] = {}
 | 
				
			||||||
        for subpat in subpatterns:
 | 
					        for subpat in subpatterns:
 | 
				
			||||||
 | 
					            if subpat.pattern is None:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
            subpat.pattern.flatten()
 | 
					            subpat.pattern.flatten()
 | 
				
			||||||
            p = subpat.as_pattern()
 | 
					            p = subpat.as_pattern()
 | 
				
			||||||
            self.shapes += p.shapes
 | 
					
 | 
				
			||||||
            self.labels += p.labels
 | 
					            # Update identifiers so each shape has a unique one
 | 
				
			||||||
 | 
					            for shape in p.shapes:
 | 
				
			||||||
 | 
					                combined_identifier = (subpat.pattern.name,) + shape.identifier
 | 
				
			||||||
 | 
					                shape_count = shape_counts.get(combined_identifier, 0)
 | 
				
			||||||
 | 
					                shape.identifier = (subpat.pattern.name, shape_count) + shape.identifier
 | 
				
			||||||
 | 
					                shape_counts[combined_identifier] = shape_count + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.append(p)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def translate_elements(self, offset: vector2) -> 'Pattern':
 | 
					    def translate_elements(self, offset: vector2) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Translates all shapes, label, and subpatterns by the given offset.
 | 
					        Translates all shapes, label, and subpatterns by the given offset.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param offset: Offset to translate by
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            offset: (x, y) to translate by
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for entry in self.shapes + self.subpatterns + self.labels:
 | 
					        for entry in self.shapes + self.subpatterns + self.labels:
 | 
				
			||||||
            entry.translate(offset)
 | 
					            entry.translate(offset)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def scale_elements(self, scale: float) -> 'Pattern':
 | 
					    def scale_elements(self, c: float) -> 'Pattern':
 | 
				
			||||||
        """"
 | 
					        """"
 | 
				
			||||||
        Scales all shapes and subpatterns by the given value.
 | 
					        Scales all shapes and subpatterns by the given value.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param scale: value to scale by
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            c: factor to scale by
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for entry in self.shapes + self.subpatterns:
 | 
					        for entry in self.shapes + self.subpatterns:
 | 
				
			||||||
            entry.scale(scale)
 | 
					            entry.scale_by(c)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def scale_by(self, c: float) -> 'Pattern':
 | 
					    def scale_by(self, c: float) -> 'Pattern':
 | 
				
			||||||
@ -353,21 +578,29 @@ class Pattern:
 | 
				
			|||||||
        Scale this Pattern by the given value
 | 
					        Scale this Pattern by the given value
 | 
				
			||||||
         (all shapes and subpatterns and their offsets are scaled)
 | 
					         (all shapes and subpatterns and their offsets are scaled)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param c: value to scale by
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            c: factor to scale by
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for entry in self.shapes + self.subpatterns:
 | 
					        for entry in self.shapes + self.subpatterns:
 | 
				
			||||||
            entry.offset *= c
 | 
					            entry.offset *= c
 | 
				
			||||||
            entry.scale_by(c)
 | 
					            entry.scale_by(c)
 | 
				
			||||||
 | 
					        for label in self.labels:
 | 
				
			||||||
 | 
					            label.offset *= c
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def rotate_around(self, pivot: vector2, rotation: float) -> 'Pattern':
 | 
					    def rotate_around(self, pivot: vector2, rotation: float) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate the Pattern around the a location.
 | 
					        Rotate the Pattern around the a location.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param pivot: Location to rotate around
 | 
					        Args:
 | 
				
			||||||
        :param rotation: Angle to rotate by (counter-clockwise, radians)
 | 
					            pivot: (x, y) location to rotate around
 | 
				
			||||||
        :return: self
 | 
					            rotation: Angle to rotate by (counter-clockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pivot = numpy.array(pivot)
 | 
					        pivot = numpy.array(pivot)
 | 
				
			||||||
        self.translate_elements(-pivot)
 | 
					        self.translate_elements(-pivot)
 | 
				
			||||||
@ -380,8 +613,11 @@ class Pattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate the offsets of all shapes, labels, and subpatterns around (0, 0)
 | 
					        Rotate the offsets of all shapes, labels, and subpatterns around (0, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param rotation: Angle to rotate by (counter-clockwise, radians)
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            rotation: Angle to rotate by (counter-clockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for entry in self.shapes + self.subpatterns + self.labels:
 | 
					        for entry in self.shapes + self.subpatterns + self.labels:
 | 
				
			||||||
            entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset)
 | 
					            entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset)
 | 
				
			||||||
@ -391,8 +627,11 @@ class Pattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate each shape and subpattern around its center (offset)
 | 
					        Rotate each shape and subpattern around its center (offset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param rotation: Angle to rotate by (counter-clockwise, radians)
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            rotation: Angle to rotate by (counter-clockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for entry in self.shapes + self.subpatterns:
 | 
					        for entry in self.shapes + self.subpatterns:
 | 
				
			||||||
            entry.rotate(rotation)
 | 
					            entry.rotate(rotation)
 | 
				
			||||||
@ -402,8 +641,12 @@ class Pattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Mirror the offsets of all shapes, labels, and subpatterns across an axis
 | 
					        Mirror the offsets of all shapes, labels, and subpatterns across an axis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param axis: Axis to mirror across
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            axis: Axis to mirror across
 | 
				
			||||||
 | 
					                (0: mirror across x axis, 1: mirror across y axis)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for entry in self.shapes + self.subpatterns + self.labels:
 | 
					        for entry in self.shapes + self.subpatterns + self.labels:
 | 
				
			||||||
            entry.offset[axis - 1] *= -1
 | 
					            entry.offset[axis - 1] *= -1
 | 
				
			||||||
@ -412,10 +655,14 @@ class Pattern:
 | 
				
			|||||||
    def mirror_elements(self, axis: int) -> 'Pattern':
 | 
					    def mirror_elements(self, axis: int) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Mirror each shape and subpattern across an axis, relative to its
 | 
					        Mirror each shape and subpattern across an axis, relative to its
 | 
				
			||||||
          center (offset)
 | 
					          offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param axis: Axis to mirror across
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            axis: Axis to mirror across
 | 
				
			||||||
 | 
					                (0: mirror across x axis, 1: mirror across y axis)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for entry in self.shapes + self.subpatterns:
 | 
					        for entry in self.shapes + self.subpatterns:
 | 
				
			||||||
            entry.mirror(axis)
 | 
					            entry.mirror(axis)
 | 
				
			||||||
@ -425,87 +672,167 @@ class Pattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Mirror the Pattern across an axis
 | 
					        Mirror the Pattern across an axis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param axis: Axis to mirror across
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            axis: Axis to mirror across
 | 
				
			||||||
 | 
					                (0: mirror across x axis, 1: mirror across y axis)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.mirror_elements(axis)
 | 
					        self.mirror_elements(axis)
 | 
				
			||||||
        self.mirror_element_centers(axis)
 | 
					        self.mirror_element_centers(axis)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def scale_element_doses(self, factor: float) -> 'Pattern':
 | 
					    def scale_element_doses(self, c: float) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Multiply all shape and subpattern doses by a factor
 | 
					        Multiply all shape and subpattern doses by a factor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param factor: Factor to multiply doses by
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            c: Factor to multiply doses by
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Return:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        for entry in self.shapes + self.subpatterns:
 | 
					        for entry in self.shapes + self.subpatterns:
 | 
				
			||||||
            entry.dose *= factor
 | 
					            entry.dose *= c
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def copy(self) -> 'Pattern':
 | 
					    def copy(self) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a copy of the Pattern, deep-copying shapes and copying subpattern entries, but not
 | 
					        Return a copy of the Pattern, deep-copying shapes and copying subpattern
 | 
				
			||||||
         deep-copying any referenced patterns.
 | 
					         entries, but not deep-copying any referenced patterns.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        See also: Pattern.deepcopy()
 | 
					        See also: `Pattern.deepcopy()`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: A copy of the current Pattern.
 | 
					        Returns:
 | 
				
			||||||
 | 
					            A copy of the current Pattern.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        cp = copy.copy(self)
 | 
					        return copy.copy(self)
 | 
				
			||||||
        cp.shapes = copy.deepcopy(cp.shapes)
 | 
					 | 
				
			||||||
        cp.labels = copy.deepcopy(cp.labels)
 | 
					 | 
				
			||||||
        cp.subpatterns = [copy.copy(subpat) for subpat in cp.subpatterns]
 | 
					 | 
				
			||||||
        return cp
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deepcopy(self) -> 'Pattern':
 | 
					    def deepcopy(self) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Convenience method for copy.deepcopy(pattern)
 | 
					        Convenience method for `copy.deepcopy(pattern)`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: A deep copy of the current Pattern.
 | 
					        Returns:
 | 
				
			||||||
 | 
					            A deep copy of the current Pattern.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return copy.deepcopy(self)
 | 
					        return copy.deepcopy(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_empty(self) -> bool:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            True if the pattern is contains no shapes, labels, or subpatterns.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return (len(self.subpatterns) == 0 and
 | 
				
			||||||
 | 
					                len(self.shapes) == 0 and
 | 
				
			||||||
 | 
					                len(self.labels) == 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self) -> 'Pattern':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Lock the pattern, raising an exception if it is modified.
 | 
				
			||||||
 | 
					        Also see `deeplock()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.shapes = tuple(self.shapes)
 | 
				
			||||||
 | 
					        self.labels = tuple(self.labels)
 | 
				
			||||||
 | 
					        self.subpatterns = tuple(self.subpatterns)
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', True)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self) -> 'Pattern':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Unlock the pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.shapes = list(self.shapes)
 | 
				
			||||||
 | 
					        self.labels = list(self.labels)
 | 
				
			||||||
 | 
					        self.subpatterns = list(self.subpatterns)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deeplock(self) -> 'Pattern':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Recursively lock the pattern, all referenced shapes, subpatterns, and labels.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.lock()
 | 
				
			||||||
 | 
					        for ss in self.shapes + self.labels:
 | 
				
			||||||
 | 
					            ss.lock()
 | 
				
			||||||
 | 
					        for sp in self.subpatterns:
 | 
				
			||||||
 | 
					            sp.deeplock()
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deepunlock(self) -> 'Pattern':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Recursively unlock the pattern, all referenced shapes, subpatterns, and labels.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is dangerous unless you have just performed a deepcopy, since anything
 | 
				
			||||||
 | 
					          you change will be changed everywhere it is referenced!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Return:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.unlock()
 | 
				
			||||||
 | 
					        for ss in self.shapes + self.labels:
 | 
				
			||||||
 | 
					            ss.unlock()
 | 
				
			||||||
 | 
					        for sp in self.subpatterns:
 | 
				
			||||||
 | 
					            sp.deepunlock()
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def load(filename: str) -> 'Pattern':
 | 
					    def load(filename: str) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Load a Pattern from a file
 | 
					        Load a Pattern from a file using pickle
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param filename: Filename to load from
 | 
					        Args:
 | 
				
			||||||
        :return: Loaded Pattern
 | 
					            filename: Filename to load from
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Loaded Pattern
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        with open(filename, 'rb') as f:
 | 
					        with open(filename, 'rb') as f:
 | 
				
			||||||
            tmp_dict = pickle.load(f)
 | 
					            pattern = pickle.load(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pattern = Pattern()
 | 
					 | 
				
			||||||
        pattern.__dict__.update(tmp_dict)
 | 
					 | 
				
			||||||
        return pattern
 | 
					        return pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, filename: str) -> 'Pattern':
 | 
					    def save(self, filename: str) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Save the Pattern to a file
 | 
					        Save the Pattern to a file using pickle
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param filename: Filename to save to
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            filename: Filename to save to
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        with open(filename, 'wb') as f:
 | 
					        with open(filename, 'wb') as f:
 | 
				
			||||||
            pickle.dump(self.__dict__, f, protocol=2)
 | 
					            pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def visualize(self,
 | 
					    def visualize(self,
 | 
				
			||||||
                  offset: vector2=(0., 0.),
 | 
					                  offset: vector2 = (0., 0.),
 | 
				
			||||||
                  line_color: str='k',
 | 
					                  line_color: str = 'k',
 | 
				
			||||||
                  fill_color: str='none',
 | 
					                  fill_color: str = 'none',
 | 
				
			||||||
                  overdraw: bool=False):
 | 
					                  overdraw: bool = False):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Draw a picture of the Pattern and wait for the user to inspect it
 | 
					        Draw a picture of the Pattern and wait for the user to inspect it
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Imports matplotlib.
 | 
					        Imports `matplotlib`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param offset: Coordinates to offset by before drawing
 | 
					        Note that this can be slow; it is often faster to export to GDSII and use
 | 
				
			||||||
        :param line_color: Outlines are drawn with this color (passed to matplotlib PolyCollection)
 | 
					         klayout or a different GDS viewer!
 | 
				
			||||||
        :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
 | 
					        Args:
 | 
				
			||||||
 | 
					            offset: Coordinates to offset by before drawing
 | 
				
			||||||
 | 
					            line_color: Outlines are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
 | 
				
			||||||
 | 
					            fill_color: Interiors are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
 | 
				
			||||||
 | 
					            overdraw: Whether to create a new figure or draw on a pre-existing one
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # TODO: add text labels to visualize()
 | 
					        # TODO: add text labels to visualize()
 | 
				
			||||||
        from matplotlib import pyplot
 | 
					        from matplotlib import pyplot
 | 
				
			||||||
@ -537,3 +864,39 @@ class Pattern:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if not overdraw:
 | 
					        if not overdraw:
 | 
				
			||||||
            pyplot.show()
 | 
					            pyplot.show()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def find_toplevel(patterns: Iterable['Pattern']) -> List['Pattern']:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Given a list of Pattern objects, return those that are not referenced by
 | 
				
			||||||
 | 
					         any other pattern.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            patterns: A list of patterns to filter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            A filtered list in which no pattern is referenced by any other pattern.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        def get_children(pat: Pattern, memo: Set) -> Set:
 | 
				
			||||||
 | 
					            if pat in memo:
 | 
				
			||||||
 | 
					                return memo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            children = set(sp.pattern for sp in pat.subpatterns if sp.pattern is not None)
 | 
				
			||||||
 | 
					            new_children = children - memo
 | 
				
			||||||
 | 
					            memo |= children
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for child_pat in new_children:
 | 
				
			||||||
 | 
					                memo |= get_children(child_pat, memo)
 | 
				
			||||||
 | 
					            return memo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        patterns = set(patterns)
 | 
				
			||||||
 | 
					        not_toplevel: Set['Pattern'] = set()
 | 
				
			||||||
 | 
					        for pattern in patterns:
 | 
				
			||||||
 | 
					            not_toplevel |= get_children(pattern, not_toplevel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        toplevel = list(patterns - not_toplevel)
 | 
				
			||||||
 | 
					        return toplevel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        return (f'<Pattern "{self.name}": sh{len(self.shapes)} sp{len(self.subpatterns)} la{len(self.labels)}{locked}>')
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										81
									
								
								masque/positionable.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								masque/positionable.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					from typing import List, Tuple, Callable, TypeVar, Optional
 | 
				
			||||||
 | 
					from abc import ABCMeta, abstractmethod
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
 | 
					import numpy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ..error import PatternError, PatternLockedError
 | 
				
			||||||
 | 
					from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					T = TypeVar('T', bound='Positionable')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Positionable(metaclass=ABCMeta):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Abstract class for all positionable entities
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('_offset',)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _offset: numpy.ndarray
 | 
				
			||||||
 | 
					    """ `[x_offset, y_offset]` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # --- Abstract methods
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def get_bounds(self) -> numpy.ndarray:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ---- Non-abstract properties
 | 
				
			||||||
 | 
					    # offset property
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def offset(self) -> numpy.ndarray:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        [x, 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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ---- Non-abstract methods
 | 
				
			||||||
 | 
					    def translate(self: T, offset: vector2) -> T:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Translate the entity by the given offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            offset: [x_offset, y,offset]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.offset += offset
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self: T) -> T:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Lock the entity, disallowing further changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = False
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self: T) -> T:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Unlock the entity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = True
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
@ -3,63 +3,140 @@
 | 
				
			|||||||
     instances of a Pattern in the same parent Pattern.
 | 
					     instances of a Pattern in the same parent Pattern.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from typing import Union, List
 | 
					from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any
 | 
				
			||||||
import copy
 | 
					import copy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
from numpy import pi
 | 
					from numpy import pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .error import PatternError
 | 
					from .error import PatternError, PatternLockedError
 | 
				
			||||||
from .utils import is_scalar, rotation_matrix_2d, vector2
 | 
					from .utils import is_scalar, rotation_matrix_2d, vector2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					    from . import Pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied
 | 
					# TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GridRepetition:
 | 
					class GridRepetition:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    GridRepetition provides support for efficiently embedding multiple copies of a Pattern
 | 
					    GridRepetition provides support for efficiently embedding multiple copies of a `Pattern`
 | 
				
			||||||
     into another Pattern at regularly-spaced offsets.
 | 
					     into another `Pattern` at regularly-spaced offsets.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Note that rotation, scaling, and mirroring are applied to individual instances of the
 | 
				
			||||||
 | 
					     pattern, not to the grid vectors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The order of operations is
 | 
				
			||||||
 | 
					        1. A single refernce instance to the target pattern is mirrored
 | 
				
			||||||
 | 
					        2. The single instance is rotated.
 | 
				
			||||||
 | 
					        3. The instance is scaled by the scaling factor.
 | 
				
			||||||
 | 
					        4. The instance is shifted by the provided offset
 | 
				
			||||||
 | 
					            (no mirroring/scaling/rotation is applied to the offset).
 | 
				
			||||||
 | 
					        5. Additional copies of the instance will appear at coordinates specified by
 | 
				
			||||||
 | 
					            `(offset + aa * a_vector + bb * b_vector)`, with `aa in range(0, a_count)`
 | 
				
			||||||
 | 
					            and `bb in range(0, b_count)`. All instance locations remain unaffected by
 | 
				
			||||||
 | 
					            mirroring/scaling/rotation, though each instance's data will be transformed
 | 
				
			||||||
 | 
					            relative to the instance's location (i.e. relative to the contained pattern's
 | 
				
			||||||
 | 
					            (0, 0) point).
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('_pattern',
 | 
				
			||||||
 | 
					                 '_offset',
 | 
				
			||||||
 | 
					                 '_rotation',
 | 
				
			||||||
 | 
					                 '_dose',
 | 
				
			||||||
 | 
					                 '_scale',
 | 
				
			||||||
 | 
					                 '_mirrored',
 | 
				
			||||||
 | 
					                 '_a_vector',
 | 
				
			||||||
 | 
					                 '_b_vector',
 | 
				
			||||||
 | 
					                 '_a_count',
 | 
				
			||||||
 | 
					                 '_b_count',
 | 
				
			||||||
 | 
					                 'identifier',
 | 
				
			||||||
 | 
					                 'locked')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _pattern: Optional['Pattern']
 | 
				
			||||||
 | 
					    """ The `Pattern` being instanced """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _offset: numpy.ndarray
 | 
				
			||||||
 | 
					    """ (x, y) offset for the base instance """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _dose: float
 | 
				
			||||||
 | 
					    """ Scaling factor applied to the dose """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _rotation: float
 | 
				
			||||||
 | 
					    """ Rotation of the individual instances in the grid (not the grid vectors).
 | 
				
			||||||
 | 
					    Radians, counterclockwise.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pattern = None          # type: Pattern
 | 
					    _scale: float
 | 
				
			||||||
 | 
					    """ Scaling factor applied to individual instances in the grid (not the grid vectors) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _offset = (0.0, 0.0)    # type: numpy.ndarray
 | 
					    _mirrored: numpy.ndarray        # ndarray[bool]
 | 
				
			||||||
    _rotation = 0.0         # type: float
 | 
					    """ Whether to mirror individual instances across the x and y axes
 | 
				
			||||||
    _dose = 1.0             # type: float
 | 
					    (Applies to individual instances in the grid, not the grid vectors)
 | 
				
			||||||
    _scale = 1.0            # type: float
 | 
					    """
 | 
				
			||||||
    _mirrored = None        # type: List[bool]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _a_vector = None        # type: numpy.ndarray
 | 
					    _a_vector: numpy.ndarray
 | 
				
			||||||
    _b_vector = None        # type: numpy.ndarray
 | 
					    """ Vector `[x, y]` specifying the first lattice vector of the grid.
 | 
				
			||||||
    a_count = None          # type: int
 | 
					        Specifies center-to-center spacing between adjacent elements.
 | 
				
			||||||
    b_count = 1             # type: int
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _a_count: int
 | 
				
			||||||
 | 
					    """ Number of instances along the direction specified by the `a_vector` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _b_vector: Optional[numpy.ndarray]
 | 
				
			||||||
 | 
					    """ Vector `[x, y]` specifying a second lattice vector for the grid.
 | 
				
			||||||
 | 
					        Specifies center-to-center spacing between adjacent elements.
 | 
				
			||||||
 | 
					        Can be `None` for a 1D array.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _b_count: int
 | 
				
			||||||
 | 
					    """ Number of instances along the direction specified by the `b_vector` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    identifier: Tuple[Any, ...]
 | 
				
			||||||
 | 
					    """ Arbitrary identifier, used internally by some `masque` functions. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    locked: bool
 | 
				
			||||||
 | 
					    """ If `True`, disallows changes to the GridRepetition """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self,
 | 
					    def __init__(self,
 | 
				
			||||||
                 pattern: 'Pattern',
 | 
					                 pattern: Optional['Pattern'],
 | 
				
			||||||
                 a_vector: numpy.ndarray,
 | 
					                 a_vector: numpy.ndarray,
 | 
				
			||||||
                 a_count: int,
 | 
					                 a_count: int,
 | 
				
			||||||
                 b_vector: numpy.ndarray = None,
 | 
					                 b_vector: Optional[numpy.ndarray] = None,
 | 
				
			||||||
                 b_count: int = 1,
 | 
					                 b_count: Optional[int] = 1,
 | 
				
			||||||
                 offset: vector2 = (0.0, 0.0),
 | 
					                 offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                 rotation: float = 0.0,
 | 
					                 rotation: float = 0.0,
 | 
				
			||||||
                 mirrored: List[bool] = None,
 | 
					                 mirrored: Optional[Sequence[bool]] = None,
 | 
				
			||||||
                 dose: float = 1.0,
 | 
					                 dose: float = 1.0,
 | 
				
			||||||
                 scale: float = 1.0):
 | 
					                 scale: float = 1.0,
 | 
				
			||||||
 | 
					                 locked: bool = False,
 | 
				
			||||||
 | 
					                 identifier: Tuple[Any, ...] = ()):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        :param a_vector: First lattice vector, of the form [x, y].
 | 
					        Args:
 | 
				
			||||||
            Specifies center-to-center spacing between adjacent elements.
 | 
					            pattern: Pattern to reference.
 | 
				
			||||||
        :param a_count: Number of elements in the a_vector direction.
 | 
					            a_vector: First lattice vector, of the form `[x, y]`.
 | 
				
			||||||
        :param b_vector: Second lattice vector, of the form [x, y].
 | 
					                Specifies center-to-center spacing between adjacent instances.
 | 
				
			||||||
            Specifies center-to-center spacing between adjacent elements.
 | 
					            a_count: Number of elements in the a_vector direction.
 | 
				
			||||||
            Can be omitted when specifying a 1D array.
 | 
					            b_vector: Second lattice vector, of the form `[x, y]`.
 | 
				
			||||||
        :param b_count: Number of elements in the b_vector direction.
 | 
					                Specifies center-to-center spacing between adjacent instances.
 | 
				
			||||||
            Should be omitted if b_vector was omitted.
 | 
					                Can be omitted when specifying a 1D array.
 | 
				
			||||||
        :raises: InvalidDataError if b_* inputs conflict with each other
 | 
					            b_count: Number of elements in the `b_vector` direction.
 | 
				
			||||||
            or a_count < 1.
 | 
					                Should be omitted if `b_vector` was omitted.
 | 
				
			||||||
 | 
					            offset: (x, y) offset applied to all instances.
 | 
				
			||||||
 | 
					            rotation: Rotation (radians, counterclockwise) applied to each instance.
 | 
				
			||||||
 | 
					                       Relative to each instance's (0, 0).
 | 
				
			||||||
 | 
					            mirrored: Whether to mirror individual instances across the x and y axes.
 | 
				
			||||||
 | 
					            dose: Scaling factor applied to the dose.
 | 
				
			||||||
 | 
					            scale: Scaling factor applied to the instances' geometry.
 | 
				
			||||||
 | 
					            locked: Whether the `GridRepetition` is locked after initialization.
 | 
				
			||||||
 | 
					            identifier: Arbitrary tuple, used internally by some `masque` functions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Raises:
 | 
				
			||||||
 | 
					            PatternError if `b_*` inputs conflict with each other
 | 
				
			||||||
 | 
					            or `a_count < 1`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        if b_count is None:
 | 
				
			||||||
 | 
					            b_count = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if b_vector is None:
 | 
					        if b_vector is None:
 | 
				
			||||||
            if b_count > 1:
 | 
					            if b_count > 1:
 | 
				
			||||||
                raise PatternError('Repetition has b_count > 1 but no b_vector')
 | 
					                raise PatternError('Repetition has b_count > 1 but no b_vector')
 | 
				
			||||||
@ -67,16 +144,19 @@ class GridRepetition:
 | 
				
			|||||||
                b_vector = numpy.array([0.0, 0.0])
 | 
					                b_vector = numpy.array([0.0, 0.0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if a_count < 1:
 | 
					        if a_count < 1:
 | 
				
			||||||
            raise InvalidDataError('Repetition has too-small a_count: '
 | 
					            raise PatternError('Repetition has too-small a_count: '
 | 
				
			||||||
                                   '{}'.format(a_count))
 | 
					                               '{}'.format(a_count))
 | 
				
			||||||
        if b_count < 1:
 | 
					        if b_count < 1:
 | 
				
			||||||
            raise InvalidDataError('Repetition has too-small b_count: '
 | 
					            raise PatternError('Repetition has too-small b_count: '
 | 
				
			||||||
                                   '{}'.format(b_count))
 | 
					                               '{}'.format(b_count))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
        self.a_vector = a_vector
 | 
					        self.a_vector = a_vector
 | 
				
			||||||
        self.b_vector = b_vector
 | 
					        self.b_vector = b_vector
 | 
				
			||||||
        self.a_count = a_count
 | 
					        self.a_count = a_count
 | 
				
			||||||
        self.b_count = b_count
 | 
					        self.b_count = b_count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.identifier = identifier
 | 
				
			||||||
        self.pattern = pattern
 | 
					        self.pattern = pattern
 | 
				
			||||||
        self.offset = offset
 | 
					        self.offset = offset
 | 
				
			||||||
        self.rotation = rotation
 | 
					        self.rotation = rotation
 | 
				
			||||||
@ -85,6 +165,45 @@ class GridRepetition:
 | 
				
			|||||||
        if mirrored is None:
 | 
					        if mirrored is None:
 | 
				
			||||||
            mirrored = [False, False]
 | 
					            mirrored = [False, False]
 | 
				
			||||||
        self.mirrored = mirrored
 | 
					        self.mirrored = mirrored
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __setattr__(self, name, value):
 | 
				
			||||||
 | 
					        if self.locked and name != 'locked':
 | 
				
			||||||
 | 
					            raise PatternLockedError()
 | 
				
			||||||
 | 
					        object.__setattr__(self, name, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __copy__(self) -> 'GridRepetition':
 | 
				
			||||||
 | 
					        new = GridRepetition(pattern=self.pattern,
 | 
				
			||||||
 | 
					                             a_vector=self.a_vector.copy(),
 | 
				
			||||||
 | 
					                             b_vector=copy.copy(self.b_vector),
 | 
				
			||||||
 | 
					                             a_count=self.a_count,
 | 
				
			||||||
 | 
					                             b_count=self.b_count,
 | 
				
			||||||
 | 
					                             offset=self.offset.copy(),
 | 
				
			||||||
 | 
					                             rotation=self.rotation,
 | 
				
			||||||
 | 
					                             dose=self.dose,
 | 
				
			||||||
 | 
					                             scale=self.scale,
 | 
				
			||||||
 | 
					                             mirrored=self.mirrored.copy(),
 | 
				
			||||||
 | 
					                             locked=self.locked)
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __deepcopy__(self, memo: Dict = None) -> 'GridRepetition':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = copy.copy(self).unlock()
 | 
				
			||||||
 | 
					        new.pattern = copy.deepcopy(self.pattern, memo)
 | 
				
			||||||
 | 
					        new.locked = self.locked
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # pattern property
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def pattern(self) -> Optional['Pattern']:
 | 
				
			||||||
 | 
					        return self._pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @pattern.setter
 | 
				
			||||||
 | 
					    def pattern(self, val: Optional['Pattern']):
 | 
				
			||||||
 | 
					        from .pattern import Pattern
 | 
				
			||||||
 | 
					        if val is not None and not isinstance(val, Pattern):
 | 
				
			||||||
 | 
					            raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val))
 | 
				
			||||||
 | 
					        self._pattern = val
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # offset property
 | 
					    # offset property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
@ -93,6 +212,9 @@ class GridRepetition:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @offset.setter
 | 
					    @offset.setter
 | 
				
			||||||
    def offset(self, val: vector2):
 | 
					    def offset(self, val: vector2):
 | 
				
			||||||
 | 
					        if self.locked:
 | 
				
			||||||
 | 
					            raise PatternLockedError()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not isinstance(val, numpy.ndarray):
 | 
					        if not isinstance(val, numpy.ndarray):
 | 
				
			||||||
            val = numpy.array(val, dtype=float)
 | 
					            val = numpy.array(val, dtype=float)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -139,14 +261,14 @@ class GridRepetition:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Mirrored property
 | 
					    # Mirrored property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def mirrored(self) -> List[bool]:
 | 
					    def mirrored(self) -> numpy.ndarray:        # ndarray[bool]
 | 
				
			||||||
        return self._mirrored
 | 
					        return self._mirrored
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @mirrored.setter
 | 
					    @mirrored.setter
 | 
				
			||||||
    def mirrored(self, val: List[bool]):
 | 
					    def mirrored(self, val: Sequence[bool]):
 | 
				
			||||||
        if is_scalar(val):
 | 
					        if is_scalar(val):
 | 
				
			||||||
            raise PatternError('Mirrored must be a 2-element list of booleans')
 | 
					            raise PatternError('Mirrored must be a 2-element list of booleans')
 | 
				
			||||||
        self._mirrored = val
 | 
					        self._mirrored = numpy.array(val, dtype=bool, copy=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # a_vector property
 | 
					    # a_vector property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
@ -160,7 +282,7 @@ class GridRepetition:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if val.size != 2:
 | 
					        if val.size != 2:
 | 
				
			||||||
            raise PatternError('a_vector must be convertible to size-2 ndarray')
 | 
					            raise PatternError('a_vector must be convertible to size-2 ndarray')
 | 
				
			||||||
        self._a_vector = val.flatten()
 | 
					        self._a_vector = val.flatten().astype(float)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # b_vector property
 | 
					    # b_vector property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
@ -170,30 +292,50 @@ class GridRepetition:
 | 
				
			|||||||
    @b_vector.setter
 | 
					    @b_vector.setter
 | 
				
			||||||
    def b_vector(self, val: vector2):
 | 
					    def b_vector(self, val: vector2):
 | 
				
			||||||
        if not isinstance(val, numpy.ndarray):
 | 
					        if not isinstance(val, numpy.ndarray):
 | 
				
			||||||
            val = numpy.array(val, dtype=float)
 | 
					            val = numpy.array(val, dtype=float, copy=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if val.size != 2:
 | 
					        if val.size != 2:
 | 
				
			||||||
            raise PatternError('b_vector must be convertible to size-2 ndarray')
 | 
					            raise PatternError('b_vector must be convertible to size-2 ndarray')
 | 
				
			||||||
        self._b_vector = val.flatten()
 | 
					        self._b_vector = val.flatten()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # a_count property
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def a_count(self) -> int:
 | 
				
			||||||
 | 
					        return self._a_count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @a_count.setter
 | 
				
			||||||
 | 
					    def a_count(self, val: int):
 | 
				
			||||||
 | 
					        if val != int(val):
 | 
				
			||||||
 | 
					            raise PatternError('a_count must be convertable to an int!')
 | 
				
			||||||
 | 
					        self._a_count = int(val)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # b_count property
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def b_count(self) -> int:
 | 
				
			||||||
 | 
					        return self._b_count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @b_count.setter
 | 
				
			||||||
 | 
					    def b_count(self, val: int):
 | 
				
			||||||
 | 
					        if val != int(val):
 | 
				
			||||||
 | 
					            raise PatternError('b_count must be convertable to an int!')
 | 
				
			||||||
 | 
					        self._b_count = int(val)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def as_pattern(self) -> 'Pattern':
 | 
					    def as_pattern(self) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns a copy of self.pattern which has been scaled, rotated, etc. according to this
 | 
					        Returns a copy of self.pattern which has been scaled, rotated, repeated, etc.
 | 
				
			||||||
         SubPattern's properties.
 | 
					          etc. according to this `GridRepetition`'s properties.
 | 
				
			||||||
        :return: Copy of self.pattern that has been altered to reflect the SubPattern's properties.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        #xy = numpy.array(element.xy)
 | 
					 | 
				
			||||||
        #origin = xy[0]
 | 
					 | 
				
			||||||
        #col_spacing = (xy[1] - origin) / element.cols
 | 
					 | 
				
			||||||
        #row_spacing = (xy[2] - origin) / element.rows
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            A copy of self.pattern which has been scaled, rotated, repeated, etc.
 | 
				
			||||||
 | 
					             etc. according to this `GridRepetition`'s properties.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        assert(self.pattern is not None)
 | 
				
			||||||
        patterns = []
 | 
					        patterns = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for a in range(self.a_count):
 | 
					        for a in range(self.a_count):
 | 
				
			||||||
            for b in range(self.b_count):
 | 
					            for b in range(self.b_count):
 | 
				
			||||||
                offset = a * self.a_vector + b * self.b_vector
 | 
					                offset = a * self.a_vector + b * self.b_vector
 | 
				
			||||||
                newPat = self.pattern.deepcopy()
 | 
					                newPat = self.pattern.deepcopy().deepunlock()
 | 
				
			||||||
                newPat.translate_elements(offset)
 | 
					                newPat.translate_elements(offset)
 | 
				
			||||||
                patterns.append(newPat)
 | 
					                patterns.append(newPat)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -213,19 +355,25 @@ class GridRepetition:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Translate by the given offset
 | 
					        Translate by the given offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param offset: Translate by this offset
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            offset: `[x, y]` to translate by
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.offset += offset
 | 
					        self.offset += offset
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def rotate_around(self, pivot: vector2, rotation: float) -> 'GridRepetition':
 | 
					    def rotate_around(self, pivot: vector2, rotation: float) -> 'GridRepetition':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate around a point
 | 
					        Rotate the array around a point
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param pivot: Point to rotate around
 | 
					        Args:
 | 
				
			||||||
        :param rotation: Angle to rotate by (counterclockwise, radians)
 | 
					            pivot: Point `[x, y]` to rotate around
 | 
				
			||||||
        :return: self
 | 
					            rotation: Angle to rotate by (counterclockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pivot = numpy.array(pivot, dtype=float)
 | 
					        pivot = numpy.array(pivot, dtype=float)
 | 
				
			||||||
        self.translate(-pivot)
 | 
					        self.translate(-pivot)
 | 
				
			||||||
@ -238,37 +386,101 @@ class GridRepetition:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate around (0, 0)
 | 
					        Rotate around (0, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param rotation: Angle to rotate by (counterclockwise, radians)
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            rotation: Angle to rotate by (counterclockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.rotate_elements(rotation)
 | 
				
			||||||
 | 
					        self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector)
 | 
				
			||||||
 | 
					        if self.b_vector is not None:
 | 
				
			||||||
 | 
					            self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def rotate_elements(self, rotation: float) -> 'GridRepetition':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Rotate each element around its origin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            rotation: Angle to rotate by (counterclockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.rotation += rotation
 | 
					        self.rotation += rotation
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def mirror(self, axis: int) -> 'GridRepetition':
 | 
					    def mirror(self, axis: int) -> 'GridRepetition':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Mirror the subpattern across an axis.
 | 
					        Mirror the GridRepetition across an axis.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param axis: Axis to mirror across.
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            axis: Axis to mirror across.
 | 
				
			||||||
 | 
					                (0: mirror across x-axis, 1: mirror across y-axis)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.mirrored[axis] = not self.mirrored[axis]
 | 
					        self.mirror_elements(axis)
 | 
				
			||||||
 | 
					        self.a_vector[1-axis] *= -1
 | 
				
			||||||
 | 
					        if self.b_vector is not None:
 | 
				
			||||||
 | 
					            self.b_vector[1-axis] *= -1
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_bounds(self) -> numpy.ndarray or None:
 | 
					    def mirror_elements(self, axis: int) -> 'GridRepetition':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the
 | 
					        Mirror each element across an axis relative to its origin.
 | 
				
			||||||
         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
 | 
					        Args:
 | 
				
			||||||
 | 
					            axis: Axis to mirror across.
 | 
				
			||||||
 | 
					                (0: mirror across x-axis, 1: mirror across y-axis)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        self.mirrored[axis] = not self.mirrored[axis]
 | 
				
			||||||
 | 
					        self.rotation *= -1
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_bounds(self) -> Optional[numpy.ndarray]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
 | 
				
			||||||
 | 
					         extent of the `GridRepetition` in each dimension.
 | 
				
			||||||
 | 
					        Returns `None` if the contained `Pattern` is empty.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            `[[x_min, y_min], [x_max, y_max]]` or `None`
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.pattern is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
        return self.as_pattern().get_bounds()
 | 
					        return self.as_pattern().get_bounds()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def scale_by(self, c: float) -> 'GridRepetition':
 | 
					    def scale_by(self, c: float) -> 'GridRepetition':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Scale the subpattern by a factor
 | 
					        Scale the GridRepetition by a factor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param c: scaling factor
 | 
					        Args:
 | 
				
			||||||
 | 
					            c: scaling factor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.scale_elements_by(c)
 | 
				
			||||||
 | 
					        self.a_vector *= c
 | 
				
			||||||
 | 
					        if self.b_vector is not None:
 | 
				
			||||||
 | 
					            self.b_vector *= c
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def scale_elements_by(self, c: float) -> 'GridRepetition':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Scale each element by a factor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            c: scaling factor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.scale *= c
 | 
					        self.scale *= c
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@ -277,15 +489,84 @@ class GridRepetition:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a shallow copy of the repetition.
 | 
					        Return a shallow copy of the repetition.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: copy.copy(self)
 | 
					        Returns:
 | 
				
			||||||
 | 
					            `copy.copy(self)`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return copy.copy(self)
 | 
					        return copy.copy(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deepcopy(self) -> 'SubPattern':
 | 
					    def deepcopy(self) -> 'GridRepetition':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a deep copy of the repetition.
 | 
					        Return a deep copy of the repetition.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: copy.copy(self)
 | 
					        Returns:
 | 
				
			||||||
 | 
					            `copy.deepcopy(self)`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return copy.deepcopy(self)
 | 
					        return copy.deepcopy(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self) -> 'GridRepetition':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Lock the `GridRepetition`, disallowing changes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = False
 | 
				
			||||||
 | 
					        self.a_vector.flags.writeable = False
 | 
				
			||||||
 | 
					        self.mirrored.flags.writeable = False
 | 
				
			||||||
 | 
					        if self.b_vector is not None:
 | 
				
			||||||
 | 
					            self.b_vector.flags.writeable = False
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', True)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self) -> 'GridRepetition':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Unlock the `GridRepetition`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = True
 | 
				
			||||||
 | 
					        self.a_vector.flags.writeable = True
 | 
				
			||||||
 | 
					        self.mirrored.flags.writeable = True
 | 
				
			||||||
 | 
					        if self.b_vector is not None:
 | 
				
			||||||
 | 
					            self.b_vector.flags.writeable = True
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deeplock(self) -> 'GridRepetition':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Recursively lock the `GridRepetition` and its contained pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        assert(self.pattern is not None)
 | 
				
			||||||
 | 
					        self.lock()
 | 
				
			||||||
 | 
					        self.pattern.deeplock()
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deepunlock(self) -> 'GridRepetition':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Recursively unlock the `GridRepetition` and its contained pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is dangerous unless you have just performed a deepcopy, since
 | 
				
			||||||
 | 
					            the component parts may be reused elsewhere.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        assert(self.pattern is not None)
 | 
				
			||||||
 | 
					        self.unlock()
 | 
				
			||||||
 | 
					        self.pattern.deepunlock()
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        name = self.pattern.name if self.pattern is not None else None
 | 
				
			||||||
 | 
					        rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
 | 
				
			||||||
 | 
					        scale = f' d{self.scale:g}' if self.scale != 1 else ''
 | 
				
			||||||
 | 
					        mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
 | 
				
			||||||
 | 
					        dose = f' d{self.dose:g}' if self.dose != 1 else ''
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        bv = f', {self.b_vector}' if self.b_vector is not None else ''
 | 
				
			||||||
 | 
					        return (f'<GridRepetition "{name}" at {self.offset} {rotation}{scale}{mirrored}{dose}'
 | 
				
			||||||
 | 
					                f' {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>')
 | 
				
			||||||
 | 
				
			|||||||
@ -10,3 +10,4 @@ from .circle import Circle
 | 
				
			|||||||
from .ellipse import Ellipse
 | 
					from .ellipse import Ellipse
 | 
				
			||||||
from .arc import Arc
 | 
					from .arc import Arc
 | 
				
			||||||
from .text import Text
 | 
					from .text import Text
 | 
				
			||||||
 | 
					from .path import Path
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,12 @@
 | 
				
			|||||||
from typing import List
 | 
					from typing import List, Tuple, Dict, Optional, Sequence
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
import math
 | 
					import math
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
from numpy import pi
 | 
					from numpy import pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
 | 
					from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
 | 
				
			||||||
from .. import PatternError
 | 
					from .. import PatternError
 | 
				
			||||||
from ..utils import is_scalar, vector2
 | 
					from ..utils import is_scalar, vector2, layer_t
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Arc(Shape):
 | 
					class Arc(Shape):
 | 
				
			||||||
@ -20,23 +18,31 @@ class Arc(Shape):
 | 
				
			|||||||
    The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
 | 
					    The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
 | 
				
			||||||
    The start and stop angle are measured counterclockwise from the first (x) radius.
 | 
					    The start and stop angle are measured counterclockwise from the first (x) radius.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('_radii', '_angles', '_width', '_rotation',
 | 
				
			||||||
 | 
					                 'poly_num_points', 'poly_max_arclen')
 | 
				
			||||||
 | 
					    _radii: numpy.ndarray
 | 
				
			||||||
 | 
					    """ Two radii for defining an ellipse """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _radii = None           # type: numpy.ndarray
 | 
					    _rotation: float
 | 
				
			||||||
    _angles = None          # type: numpy.ndarray
 | 
					    """ Rotation (ccw, radians) from the x axis to the first radius """
 | 
				
			||||||
    _width = 1.0            # type: float
 | 
					 | 
				
			||||||
    _rotation = 0.0         # type: float
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Defaults for to_polygons
 | 
					    _angles: numpy.ndarray
 | 
				
			||||||
    poly_num_points = DEFAULT_POLY_NUM_POINTS   # type: int
 | 
					    """ Start and stop angles (ccw, radians) for choosing an arc from the ellipse, measured from the first radius """
 | 
				
			||||||
    poly_max_arclen = None                      # type: float
 | 
					
 | 
				
			||||||
 | 
					    _width: float
 | 
				
			||||||
 | 
					    """ Width of the arc """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    poly_num_points: Optional[int]
 | 
				
			||||||
 | 
					    """ Sets the default number of points for `.polygonize()` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    poly_max_arclen: Optional[float]
 | 
				
			||||||
 | 
					    """ Sets the default max segement length for `.polygonize()` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # radius properties
 | 
					    # radius properties
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def radii(self) -> numpy.ndarray:
 | 
					    def radii(self) -> numpy.ndarray:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return the radii [rx, ry]
 | 
					        Return the radii `[rx, ry]`
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: [rx, ry]
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._radii
 | 
					        return self._radii
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -71,12 +77,13 @@ class Arc(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # arc start/stop angle properties
 | 
					    # arc start/stop angle properties
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def angles(self) -> vector2:
 | 
					    def angles(self) -> numpy.ndarray:          #ndarray[float]
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return the start and stop angles [a_start, a_stop].
 | 
					        Return the start and stop angles `[a_start, a_stop]`.
 | 
				
			||||||
        Angles are measured from x-axis after rotation
 | 
					        Angles are measured from x-axis after rotation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: [a_start, a_stop]
 | 
					        Returns:
 | 
				
			||||||
 | 
					            `[a_start, a_stop]`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._angles
 | 
					        return self._angles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -109,7 +116,8 @@ class Arc(Shape):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotation of radius_x from x_axis, counterclockwise, in radians. Stored mod 2*pi
 | 
					        Rotation of radius_x from x_axis, counterclockwise, in radians. Stored mod 2*pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: rotation counterclockwise in radians
 | 
					        Returns:
 | 
				
			||||||
 | 
					            rotation counterclockwise in radians
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._rotation
 | 
					        return self._rotation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -125,7 +133,8 @@ class Arc(Shape):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Width of the arc (difference between inner and outer radii)
 | 
					        Width of the arc (difference between inner and outer radii)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: width
 | 
					        Returns:
 | 
				
			||||||
 | 
					            width
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._width
 | 
					        return self._width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -141,13 +150,16 @@ class Arc(Shape):
 | 
				
			|||||||
                 radii: vector2,
 | 
					                 radii: vector2,
 | 
				
			||||||
                 angles: vector2,
 | 
					                 angles: vector2,
 | 
				
			||||||
                 width: float,
 | 
					                 width: float,
 | 
				
			||||||
                 poly_num_points: int=DEFAULT_POLY_NUM_POINTS,
 | 
					                 poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
 | 
				
			||||||
                 poly_max_arclen: float=None,
 | 
					                 poly_max_arclen: Optional[float] = None,
 | 
				
			||||||
                 offset: vector2=(0.0, 0.0),
 | 
					                 offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                 rotation: float=0,
 | 
					                 rotation: float = 0,
 | 
				
			||||||
                 mirrored: Tuple[bool] = (False, False),
 | 
					                 mirrored: Sequence[bool] = (False, False),
 | 
				
			||||||
                 layer: int=0,
 | 
					                 layer: layer_t = 0,
 | 
				
			||||||
                 dose: float=1.0):
 | 
					                 dose: float = 1.0,
 | 
				
			||||||
 | 
					                 locked: bool = False):
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.identifier = ()
 | 
				
			||||||
        self.radii = radii
 | 
					        self.radii = radii
 | 
				
			||||||
        self.angles = angles
 | 
					        self.angles = angles
 | 
				
			||||||
        self.width = width
 | 
					        self.width = width
 | 
				
			||||||
@ -158,8 +170,21 @@ class Arc(Shape):
 | 
				
			|||||||
        self.dose = dose
 | 
					        self.dose = dose
 | 
				
			||||||
        self.poly_num_points = poly_num_points
 | 
					        self.poly_num_points = poly_num_points
 | 
				
			||||||
        self.poly_max_arclen = poly_max_arclen
 | 
					        self.poly_max_arclen = poly_max_arclen
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]:
 | 
					    def  __deepcopy__(self, memo: Dict = None) -> 'Arc':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = copy.copy(self).unlock()
 | 
				
			||||||
 | 
					        new._offset = self._offset.copy()
 | 
				
			||||||
 | 
					        new._radii = self._radii.copy()
 | 
				
			||||||
 | 
					        new._angles = self._angles.copy()
 | 
				
			||||||
 | 
					        new.locked = self.locked
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_polygons(self,
 | 
				
			||||||
 | 
					                    poly_num_points: Optional[int] = None,
 | 
				
			||||||
 | 
					                    poly_max_arclen: Optional[float] = None,
 | 
				
			||||||
 | 
					                    ) -> List[Polygon]:
 | 
				
			||||||
        if poly_num_points is None:
 | 
					        if poly_num_points is None:
 | 
				
			||||||
            poly_num_points = self.poly_num_points
 | 
					            poly_num_points = self.poly_num_points
 | 
				
			||||||
        if poly_max_arclen is None:
 | 
					        if poly_max_arclen is None:
 | 
				
			||||||
@ -209,12 +234,12 @@ class Arc(Shape):
 | 
				
			|||||||
    def get_bounds(self) -> numpy.ndarray:
 | 
					    def get_bounds(self) -> numpy.ndarray:
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        Equation for rotated ellipse is
 | 
					        Equation for rotated ellipse is
 | 
				
			||||||
            x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)
 | 
					            `x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)`
 | 
				
			||||||
            y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)
 | 
					            `y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)`
 | 
				
			||||||
          where t is our parameter.
 | 
					          where `t` is our parameter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Differentiating and solving for 0 slope wrt. t, we find
 | 
					        Differentiating and solving for 0 slope wrt. `t`, we find
 | 
				
			||||||
            tan(t) = -+ b/a cot(phi)
 | 
					            `tan(t) = -+ b/a cot(phi)`
 | 
				
			||||||
          where -+ is for x, y cases, so that's where the extrema are.
 | 
					          where -+ is for x, y cases, so that's where the extrema are.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
 | 
					        If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
 | 
				
			||||||
@ -308,13 +333,16 @@ class Arc(Shape):
 | 
				
			|||||||
        width = self.width
 | 
					        width = self.width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (type(self), radii, angles, width/norm_value, self.layer), \
 | 
					        return (type(self), radii, angles, width/norm_value, self.layer), \
 | 
				
			||||||
               (self.offset, scale/norm_value, rotation, self.dose), \
 | 
					               (self.offset, scale/norm_value, rotation, False, self.dose), \
 | 
				
			||||||
               lambda: Arc(radii=radii*norm_value, angles=angles, width=width*norm_value, layer=self.layer)
 | 
					               lambda: Arc(radii=radii*norm_value, angles=angles, width=width*norm_value, layer=self.layer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_cap_edges(self) -> numpy.ndarray:
 | 
					    def get_cap_edges(self) -> numpy.ndarray:
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        :returns: [[[x0, y0], [x1, y1]],   array of 4 points, specifying the two cuts which
 | 
					        Returns:
 | 
				
			||||||
                   [[x2, y2], [x3, y3]]],    would create this arc from its corresponding ellipse.
 | 
					            ```
 | 
				
			||||||
 | 
					            [[[x0, y0], [x1, y1]],   array of 4 points, specifying the two cuts which
 | 
				
			||||||
 | 
					             [[x2, y2], [x3, y3]]],    would create this arc from its corresponding ellipse.
 | 
				
			||||||
 | 
					            ```
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        a_ranges = self._angles_to_parameters()
 | 
					        a_ranges = self._angles_to_parameters()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -340,8 +368,9 @@ class Arc(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def _angles_to_parameters(self) -> numpy.ndarray:
 | 
					    def _angles_to_parameters(self) -> numpy.ndarray:
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        :return: "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
 | 
					        Returns:
 | 
				
			||||||
                   [[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]
 | 
					            "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
 | 
				
			||||||
 | 
					                   `[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        a = []
 | 
					        a = []
 | 
				
			||||||
        for sgn in (-1, +1):
 | 
					        for sgn in (-1, +1):
 | 
				
			||||||
@ -357,3 +386,22 @@ class Arc(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            a.append((a0, a1))
 | 
					            a.append((a0, a1))
 | 
				
			||||||
        return numpy.array(a)
 | 
					        return numpy.array(a)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self) -> 'Arc':
 | 
				
			||||||
 | 
					        self.radii.flags.writeable = False
 | 
				
			||||||
 | 
					        self.angles.flags.writeable = False
 | 
				
			||||||
 | 
					        Shape.lock(self)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self) -> 'Arc':
 | 
				
			||||||
 | 
					        Shape.unlock(self)
 | 
				
			||||||
 | 
					        self.radii.flags.writeable = True
 | 
				
			||||||
 | 
					        self.angles.flags.writeable = True
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        angles = f' a°{self.angles*180/pi}'
 | 
				
			||||||
 | 
					        rotation = f' r°{self.rotation*180/pi:g}' if self.rotation != 0 else ''
 | 
				
			||||||
 | 
					        dose = f' d{self.dose:g}' if self.dose != 1 else ''
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}{dose}{locked}>'
 | 
				
			||||||
 | 
				
			|||||||
@ -1,33 +1,32 @@
 | 
				
			|||||||
from typing import List
 | 
					from typing import List, Dict, Optional
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
from numpy import pi
 | 
					from numpy import pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
 | 
					from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
 | 
				
			||||||
from .. import PatternError
 | 
					from .. import PatternError
 | 
				
			||||||
from ..utils import is_scalar, vector2
 | 
					from ..utils import is_scalar, vector2, layer_t
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Circle(Shape):
 | 
					class Circle(Shape):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    A circle, which has a position and radius.
 | 
					    A circle, which has a position and radius.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen')
 | 
				
			||||||
 | 
					    _radius: float
 | 
				
			||||||
 | 
					    """ Circle radius """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _radius = None                              # type: float
 | 
					    poly_num_points: Optional[int]
 | 
				
			||||||
 | 
					    """ Sets the default number of points for `.polygonize()` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Defaults for to_polygons
 | 
					    poly_max_arclen: Optional[float]
 | 
				
			||||||
    poly_num_points = DEFAULT_POLY_NUM_POINTS   # type: int
 | 
					    """ Sets the default max segement length for `.polygonize()` """
 | 
				
			||||||
    poly_max_arclen = None                      # type: float
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # radius property
 | 
					    # radius property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def radius(self) -> float:
 | 
					    def radius(self) -> float:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Circle's radius (float, >= 0)
 | 
					        Circle's radius (float, >= 0)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: radius
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._radius
 | 
					        return self._radius
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -41,19 +40,33 @@ class Circle(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __init__(self,
 | 
					    def __init__(self,
 | 
				
			||||||
                 radius: float,
 | 
					                 radius: float,
 | 
				
			||||||
                 poly_num_points: int=DEFAULT_POLY_NUM_POINTS,
 | 
					                 poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
 | 
				
			||||||
                 poly_max_arclen: float=None,
 | 
					                 poly_max_arclen: Optional[float] = None,
 | 
				
			||||||
                 offset: vector2=(0.0, 0.0),
 | 
					                 offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                 layer: int=0,
 | 
					                 layer: layer_t = 0,
 | 
				
			||||||
                 dose: float=1.0):
 | 
					                 dose: float = 1.0,
 | 
				
			||||||
 | 
					                 locked: bool = False):
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.identifier = ()
 | 
				
			||||||
        self.offset = numpy.array(offset, dtype=float)
 | 
					        self.offset = numpy.array(offset, dtype=float)
 | 
				
			||||||
        self.layer = layer
 | 
					        self.layer = layer
 | 
				
			||||||
        self.dose = dose
 | 
					        self.dose = dose
 | 
				
			||||||
        self.radius = radius
 | 
					        self.radius = radius
 | 
				
			||||||
        self.poly_num_points = poly_num_points
 | 
					        self.poly_num_points = poly_num_points
 | 
				
			||||||
        self.poly_max_arclen = poly_max_arclen
 | 
					        self.poly_max_arclen = poly_max_arclen
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]:
 | 
					    def  __deepcopy__(self, memo: Dict = None) -> 'Circle':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = copy.copy(self).unlock()
 | 
				
			||||||
 | 
					        new._offset = self._offset.copy()
 | 
				
			||||||
 | 
					        new.locked = self.locked
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_polygons(self,
 | 
				
			||||||
 | 
					                    poly_num_points: Optional[int] = None,
 | 
				
			||||||
 | 
					                    poly_max_arclen: Optional[float] = None,
 | 
				
			||||||
 | 
					                    ) -> List[Polygon]:
 | 
				
			||||||
        if poly_num_points is None:
 | 
					        if poly_num_points is None:
 | 
				
			||||||
            poly_num_points = self.poly_num_points
 | 
					            poly_num_points = self.poly_num_points
 | 
				
			||||||
        if poly_max_arclen is None:
 | 
					        if poly_max_arclen is None:
 | 
				
			||||||
@ -94,6 +107,10 @@ class Circle(Shape):
 | 
				
			|||||||
        rotation = 0.0
 | 
					        rotation = 0.0
 | 
				
			||||||
        magnitude = self.radius / norm_value
 | 
					        magnitude = self.radius / norm_value
 | 
				
			||||||
        return (type(self), self.layer), \
 | 
					        return (type(self), self.layer), \
 | 
				
			||||||
               (self.offset, magnitude, rotation, self.dose), \
 | 
					               (self.offset, magnitude, rotation, False, self.dose), \
 | 
				
			||||||
               lambda: Circle(radius=norm_value, layer=self.layer)
 | 
					               lambda: Circle(radius=norm_value, layer=self.layer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        dose = f' d{self.dose:g}' if self.dose != 1 else ''
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}{dose}{locked}>'
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,12 @@
 | 
				
			|||||||
from typing import List, Tuple
 | 
					from typing import List, Tuple, Dict, Sequence, Optional
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
import math
 | 
					import math
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
from numpy import pi
 | 
					from numpy import pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
 | 
					from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
 | 
				
			||||||
from .. import PatternError
 | 
					from .. import PatternError
 | 
				
			||||||
from ..utils import is_scalar, rotation_matrix_2d, vector2
 | 
					from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Ellipse(Shape):
 | 
					class Ellipse(Shape):
 | 
				
			||||||
@ -16,21 +14,25 @@ class Ellipse(Shape):
 | 
				
			|||||||
    An ellipse, which has a position, two radii, and a rotation.
 | 
					    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.
 | 
					    The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('_radii', '_rotation',
 | 
				
			||||||
 | 
					                 'poly_num_points', 'poly_max_arclen')
 | 
				
			||||||
 | 
					    _radii: numpy.ndarray
 | 
				
			||||||
 | 
					    """ Ellipse radii """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _radii = None       # type: numpy.ndarray
 | 
					    _rotation: float
 | 
				
			||||||
    _rotation = 0.0     # type: float
 | 
					    """ Angle from x-axis to first radius (ccw, radians) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Defaults for to_polygons
 | 
					    poly_num_points: Optional[int]
 | 
				
			||||||
    poly_num_points = DEFAULT_POLY_NUM_POINTS   # type: int
 | 
					    """ Sets the default number of points for `.polygonize()` """
 | 
				
			||||||
    poly_max_arclen = None                      # type: float
 | 
					
 | 
				
			||||||
 | 
					    poly_max_arclen: Optional[float]
 | 
				
			||||||
 | 
					    """ Sets the default max segement length for `.polygonize()` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # radius properties
 | 
					    # radius properties
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def radii(self) -> numpy.ndarray:
 | 
					    def radii(self) -> numpy.ndarray:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return the radii [rx, ry]
 | 
					        Return the radii `[rx, ry]`
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: [rx, ry]
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._radii
 | 
					        return self._radii
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -70,7 +72,8 @@ class Ellipse(Shape):
 | 
				
			|||||||
        Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise
 | 
					        Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise
 | 
				
			||||||
         is positive)
 | 
					         is positive)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: counterclockwise rotation in radians
 | 
					        Returns:
 | 
				
			||||||
 | 
					            counterclockwise rotation in radians
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._rotation
 | 
					        return self._rotation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -82,13 +85,16 @@ class Ellipse(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __init__(self,
 | 
					    def __init__(self,
 | 
				
			||||||
                 radii: vector2,
 | 
					                 radii: vector2,
 | 
				
			||||||
                 poly_num_points: int=DEFAULT_POLY_NUM_POINTS,
 | 
					                 poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS,
 | 
				
			||||||
                 poly_max_arclen: float=None,
 | 
					                 poly_max_arclen: Optional[float] = None,
 | 
				
			||||||
                 offset: vector2=(0.0, 0.0),
 | 
					                 offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                 rotation: float=0,
 | 
					                 rotation: float = 0,
 | 
				
			||||||
                 mirrored: Tuple[bool] = (False, False),
 | 
					                 mirrored: Sequence[bool] = (False, False),
 | 
				
			||||||
                 layer: int=0,
 | 
					                 layer: layer_t = 0,
 | 
				
			||||||
                 dose: float=1.0):
 | 
					                 dose: float = 1.0,
 | 
				
			||||||
 | 
					                 locked: bool = False):
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.identifier = ()
 | 
				
			||||||
        self.radii = radii
 | 
					        self.radii = radii
 | 
				
			||||||
        self.offset = offset
 | 
					        self.offset = offset
 | 
				
			||||||
        self.rotation = rotation
 | 
					        self.rotation = rotation
 | 
				
			||||||
@ -97,10 +103,19 @@ class Ellipse(Shape):
 | 
				
			|||||||
        self.dose = dose
 | 
					        self.dose = dose
 | 
				
			||||||
        self.poly_num_points = poly_num_points
 | 
					        self.poly_num_points = poly_num_points
 | 
				
			||||||
        self.poly_max_arclen = poly_max_arclen
 | 
					        self.poly_max_arclen = poly_max_arclen
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __deepcopy__(self, memo: Dict = None) -> 'Ellipse':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = copy.copy(self).unlock()
 | 
				
			||||||
 | 
					        new._offset = self._offset.copy()
 | 
				
			||||||
 | 
					        new._radii = self._radii.copy()
 | 
				
			||||||
 | 
					        new.locked = self.locked
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_polygons(self,
 | 
					    def to_polygons(self,
 | 
				
			||||||
                    poly_num_points: int=None,
 | 
					                    poly_num_points: Optional[int] = None,
 | 
				
			||||||
                    poly_max_arclen: float=None
 | 
					                    poly_max_arclen: Optional[float] = None,
 | 
				
			||||||
                    ) -> List[Polygon]:
 | 
					                    ) -> List[Polygon]:
 | 
				
			||||||
        if poly_num_points is None:
 | 
					        if poly_num_points is None:
 | 
				
			||||||
            poly_num_points = self.poly_num_points
 | 
					            poly_num_points = self.poly_num_points
 | 
				
			||||||
@ -162,6 +177,21 @@ class Ellipse(Shape):
 | 
				
			|||||||
            scale = self.radius_y
 | 
					            scale = self.radius_y
 | 
				
			||||||
            angle = (self.rotation + pi / 2) % pi
 | 
					            angle = (self.rotation + pi / 2) % pi
 | 
				
			||||||
        return (type(self), radii, self.layer), \
 | 
					        return (type(self), radii, self.layer), \
 | 
				
			||||||
               (self.offset, scale/norm_value, angle, self.dose), \
 | 
					               (self.offset, scale/norm_value, angle, False, self.dose), \
 | 
				
			||||||
               lambda: Ellipse(radii=radii*norm_value, layer=self.layer)
 | 
					               lambda: Ellipse(radii=radii*norm_value, layer=self.layer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self) -> 'Ellipse':
 | 
				
			||||||
 | 
					        self.radii.flags.writeable = False
 | 
				
			||||||
 | 
					        Shape.lock(self)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self) -> 'Ellipse':
 | 
				
			||||||
 | 
					        Shape.unlock(self)
 | 
				
			||||||
 | 
					        self.radii.flags.writeable = True
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
 | 
				
			||||||
 | 
					        dose = f' d{self.dose:g}' if self.dose != 1 else ''
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}{dose}{locked}>'
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,21 @@
 | 
				
			|||||||
from typing import List, Tuple
 | 
					from typing import List, Tuple, Dict, Optional, Sequence
 | 
				
			||||||
import copy
 | 
					import copy
 | 
				
			||||||
from enum import Enum
 | 
					from enum import Enum
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
from numpy import pi
 | 
					from numpy import pi, inf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import Shape, normalized_shape_tuple
 | 
					from . import Shape, normalized_shape_tuple, Polygon, Circle
 | 
				
			||||||
from .. import PatternError
 | 
					from .. import PatternError
 | 
				
			||||||
from ..utils import is_scalar, rotation_matrix_2d, vector2
 | 
					from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
 | 
				
			||||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices
 | 
					from ..utils import remove_colinear_vertices, remove_duplicate_vertices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					
 | 
				
			||||||
 | 
					class PathCap(Enum):
 | 
				
			||||||
 | 
					    Flush = 0       # Path ends at final vertices
 | 
				
			||||||
 | 
					    Circle = 1      # Path extends past final vertices with a semicircle of radius width/2
 | 
				
			||||||
 | 
					    Square = 2      # Path extends past final vertices with a width-by-width/2 rectangle
 | 
				
			||||||
 | 
					    SquareCustom = 4  # Path extends past final vertices with a rectangle of length
 | 
				
			||||||
 | 
					                      #     defined by path.cap_extensions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Path(Shape):
 | 
					class Path(Shape):
 | 
				
			||||||
@ -19,23 +25,19 @@ class Path(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    A normalized_form(...) is available, but can be quite slow with lots of vertices.
 | 
					    A normalized_form(...) is available, but can be quite slow with lots of vertices.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    _vertices = None        # type: numpy.ndarray
 | 
					    __slots__ = ('_vertices', '_width', '_cap', '_cap_extensions')
 | 
				
			||||||
    _width = None           # type: float
 | 
					    _vertices: numpy.ndarray
 | 
				
			||||||
    _cap = None             # type: Path.Cap
 | 
					    _width: float
 | 
				
			||||||
 | 
					    _cap: PathCap
 | 
				
			||||||
    class Cap(Enum):
 | 
					    _cap_extensions: Optional[numpy.ndarray]
 | 
				
			||||||
        Flush = 0
 | 
					 | 
				
			||||||
        Circle = 1
 | 
					 | 
				
			||||||
        Square = 2
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Cap = PathCap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # width property
 | 
					    # width property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def width(self) -> float:
 | 
					    def width(self) -> float:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Path width (float, >= 0)
 | 
					        Path width (float, >= 0)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: width
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._width
 | 
					        return self._width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -49,31 +51,56 @@ class Path(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # cap property
 | 
					    # cap property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def cap(self) -> 'Path.Cap':
 | 
					    def cap(self) -> PathCap:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Path end-cap
 | 
					        Path end-cap
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: Path.Cap enum
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._cap
 | 
					        return self._cap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cap.setter
 | 
					    @cap.setter
 | 
				
			||||||
    def cap(self, val: 'Path.Cap'):
 | 
					    def cap(self, val: PathCap):
 | 
				
			||||||
        self._cap = Path.Cap(val)
 | 
					        # TODO: Document that setting cap can change cap_extensions
 | 
				
			||||||
 | 
					        self._cap = PathCap(val)
 | 
				
			||||||
 | 
					        if self.cap != PathCap.SquareCustom:
 | 
				
			||||||
 | 
					            self.cap_extensions = None
 | 
				
			||||||
 | 
					        elif self.cap_extensions is None:
 | 
				
			||||||
 | 
					            # just got set to SquareCustom
 | 
				
			||||||
 | 
					            self.cap_extensions = numpy.zeros(2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # cap_extensions property
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def cap_extensions(self) -> Optional[numpy.ndarray]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Path end-cap extension
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            2-element ndarray or `None`
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self._cap_extensions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @cap_extensions.setter
 | 
				
			||||||
 | 
					    def cap_extensions(self, vals: Optional[numpy.ndarray]):
 | 
				
			||||||
 | 
					        custom_caps = (PathCap.SquareCustom,)
 | 
				
			||||||
 | 
					        if self.cap in custom_caps:
 | 
				
			||||||
 | 
					            if vals is None:
 | 
				
			||||||
 | 
					                raise Exception('Tried to set cap extensions to None on path with custom cap type')
 | 
				
			||||||
 | 
					            self._cap_extensions = numpy.array(vals, dtype=float)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            if vals is not None:
 | 
				
			||||||
 | 
					                raise Exception('Tried to set custom cap extensions on path with non-custom cap type')
 | 
				
			||||||
 | 
					            self._cap_extensions = vals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # vertices property
 | 
					    # vertices property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def vertices(self) -> numpy.ndarray:
 | 
					    def vertices(self) -> numpy.ndarray:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Vertices of the path (Nx2 ndarray: [[x0, y0], [x1, y1], ...]
 | 
					        Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: vertices
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._vertices
 | 
					        return self._vertices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @vertices.setter
 | 
					    @vertices.setter
 | 
				
			||||||
    def vertices(self, val: numpy.ndarray):
 | 
					    def vertices(self, val: numpy.ndarray):
 | 
				
			||||||
        val = numpy.array(val, dtype=float)
 | 
					        val = numpy.array(val, dtype=float)                 #TODO document that these might not be copied
 | 
				
			||||||
        if len(val.shape) < 2 or val.shape[1] != 2:
 | 
					        if len(val.shape) < 2 or val.shape[1] != 2:
 | 
				
			||||||
            raise PatternError('Vertices must be an Nx2 array')
 | 
					            raise PatternError('Vertices must be an Nx2 array')
 | 
				
			||||||
        if val.shape[0] < 2:
 | 
					        if val.shape[0] < 2:
 | 
				
			||||||
@ -113,42 +140,77 @@ class Path(Shape):
 | 
				
			|||||||
    def __init__(self,
 | 
					    def __init__(self,
 | 
				
			||||||
                 vertices: numpy.ndarray,
 | 
					                 vertices: numpy.ndarray,
 | 
				
			||||||
                 width: float = 0.0,
 | 
					                 width: float = 0.0,
 | 
				
			||||||
                 cap: 'Path.Cap' = Path.Cap.Flush,
 | 
					                 cap: PathCap = PathCap.Flush,
 | 
				
			||||||
                 offset: vector2=(0.0, 0.0),
 | 
					                 cap_extensions: numpy.ndarray = None,
 | 
				
			||||||
                 rotation: float = 0
 | 
					                 offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                 mirrored: Tuple[bool] = (False, False),
 | 
					                 rotation: float = 0,
 | 
				
			||||||
                 layer: int=0,
 | 
					                 mirrored: Sequence[bool] = (False, False),
 | 
				
			||||||
                 dose: float=1.0,
 | 
					                 layer: layer_t = 0,
 | 
				
			||||||
                 ) -> 'Path':
 | 
					                 dose: float = 1.0,
 | 
				
			||||||
 | 
					                 locked: bool = False,
 | 
				
			||||||
 | 
					                 ):
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self._cap_extensions = None     # Since .cap setter might access it
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.identifier = ()
 | 
				
			||||||
        self.offset = offset
 | 
					        self.offset = offset
 | 
				
			||||||
        self.layer = layer
 | 
					        self.layer = layer
 | 
				
			||||||
        self.dose = dose
 | 
					        self.dose = dose
 | 
				
			||||||
        self.vertices = vertices
 | 
					        self.vertices = vertices
 | 
				
			||||||
        self.width = width
 | 
					        self.width = width
 | 
				
			||||||
        self.cap = cap
 | 
					        self.cap = cap
 | 
				
			||||||
 | 
					        if cap_extensions is not None:
 | 
				
			||||||
 | 
					            self.cap_extensions = cap_extensions
 | 
				
			||||||
        self.rotate(rotation)
 | 
					        self.rotate(rotation)
 | 
				
			||||||
        [self.mirror(a) for a, do in enumerate(mirrored) if do]
 | 
					        [self.mirror(a) for a, do in enumerate(mirrored) if do]
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __deepcopy__(self, memo: Dict = None) -> 'Path':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = copy.copy(self).unlock()
 | 
				
			||||||
 | 
					        new._offset = self._offset.copy()
 | 
				
			||||||
 | 
					        new._vertices = self._vertices.copy()
 | 
				
			||||||
 | 
					        new._cap = copy.deepcopy(self._cap, memo)
 | 
				
			||||||
 | 
					        new._cap_extensions = copy.deepcopy(self._cap_extensions, memo)
 | 
				
			||||||
 | 
					        new.locked = self.locked
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def travel(travel_pairs: Tuple[Tuple[float, float]],
 | 
					    def travel(travel_pairs: Tuple[Tuple[float, float]],
 | 
				
			||||||
               width: float = 0.0,
 | 
					               width: float = 0.0,
 | 
				
			||||||
               cap: 'Path.Cap' = Path.Cap.Flush,
 | 
					               cap: PathCap = PathCap.Flush,
 | 
				
			||||||
               offset: vector2=(0.0, 0.0),
 | 
					               cap_extensions = None,
 | 
				
			||||||
               rotation: float = 0
 | 
					               offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
               mirrored: Tuple[bool] = (False, False),
 | 
					               rotation: float = 0,
 | 
				
			||||||
               layer: int=0,
 | 
					               mirrored: Sequence[bool] = (False, False),
 | 
				
			||||||
               dose: float=1.0,
 | 
					               layer: layer_t = 0,
 | 
				
			||||||
 | 
					               dose: float = 1.0,
 | 
				
			||||||
               ) -> 'Path':
 | 
					               ) -> 'Path':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        TODO
 | 
					        Build a path by specifying the turn angles and travel distances
 | 
				
			||||||
 | 
					          rather than setting the distances directly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            travel_pairs: A list of (angle, distance) pairs that define
 | 
				
			||||||
 | 
					                the path. Angles are counterclockwise, in radians, and are relative
 | 
				
			||||||
 | 
					                to the previous segment's direction (the initial angle is relative
 | 
				
			||||||
 | 
					                to the +x axis).
 | 
				
			||||||
 | 
					            width: Path width, default `0`
 | 
				
			||||||
 | 
					            cap: End-cap type, default `Path.Cap.Flush` (no end-cap)
 | 
				
			||||||
 | 
					            cap_extensions: End-cap extension distances, when using `Path.Cap.CustomSquare`.
 | 
				
			||||||
 | 
					                Default `(0, 0)` or `None`, depending on cap type
 | 
				
			||||||
 | 
					            offset: Offset, default `(0, 0)`
 | 
				
			||||||
 | 
					            rotation: Rotation counterclockwise, in radians. Default `0`
 | 
				
			||||||
 | 
					            mirrored: Whether to mirror across the x or y axes. For example,
 | 
				
			||||||
 | 
					                `mirrored=(True, False)` results in a reflection across the x-axis,
 | 
				
			||||||
 | 
					                multiplying the path's y-coordinates by -1. Default `(False, False)`
 | 
				
			||||||
 | 
					            layer: Layer, default `0`
 | 
				
			||||||
 | 
					            dose: Dose, default `1.0`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param offset: Offset, default (0, 0)
 | 
					        Returns:
 | 
				
			||||||
        :param rotation: Rotation counterclockwise, in radians
 | 
					            The resulting Path object
 | 
				
			||||||
        :param layer: Layer, default 0
 | 
					 | 
				
			||||||
        :param dose: Dose, default 1.0
 | 
					 | 
				
			||||||
        :return: The resulting Path object
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        #TODO: needs testing
 | 
				
			||||||
        direction = numpy.array([1, 0])
 | 
					        direction = numpy.array([1, 0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        verts = [[0, 0]]
 | 
					        verts = [[0, 0]]
 | 
				
			||||||
@ -156,22 +218,17 @@ class Path(Shape):
 | 
				
			|||||||
            direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T
 | 
					            direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T
 | 
				
			||||||
            verts.append(verts[-1] + direction * distance)
 | 
					            verts.append(verts[-1] + direction * distance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Path(vertices=verts, width=width, cap=cap,
 | 
					        return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions,
 | 
				
			||||||
                    offset=offset, rotation=rotation, mirrored=mirrored,
 | 
					                    offset=offset, rotation=rotation, mirrored=mirrored,
 | 
				
			||||||
                    layer=layer, dose=dose)
 | 
					                    layer=layer, dose=dose)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_polygons(self,
 | 
					    def to_polygons(self,
 | 
				
			||||||
                    poly_num_points: int=None,
 | 
					                    poly_num_points: int = None,
 | 
				
			||||||
                    poly_max_arclen: float=None,
 | 
					                    poly_max_arclen: float = None,
 | 
				
			||||||
                    ) -> List['Polygon']:
 | 
					                    ) -> List['Polygon']:
 | 
				
			||||||
        if self.cap in (Path.Cap.Flush, Path.Cap.Circle
 | 
					        extensions = self._calculate_cap_extensions()
 | 
				
			||||||
            extension = 0.0
 | 
					 | 
				
			||||||
        elif self.cap == Path.Cap.Square:
 | 
					 | 
				
			||||||
            extension = self.width / 2
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            raise PatternError('Unrecognized path endcap: {}'.format(self.cap))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        v = remove_colinear_vertices(numpy.array(element.xy, dtype=float), closed_path=False)
 | 
					        v = remove_colinear_vertices(self.vertices, closed_path=False)
 | 
				
			||||||
        dv = numpy.diff(v, axis=0)
 | 
					        dv = numpy.diff(v, axis=0)
 | 
				
			||||||
        dvdir = dv / numpy.sqrt((dv * dv).sum(axis=1))[:, None]
 | 
					        dvdir = dv / numpy.sqrt((dv * dv).sum(axis=1))[:, None]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -181,13 +238,12 @@ class Path(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
 | 
					        perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # add extension
 | 
					        # add extensions
 | 
				
			||||||
        if extension != 0
 | 
					        if (extensions != 0).any():
 | 
				
			||||||
            v[0] -= dvdir[0] * extension
 | 
					            v[0] -= dvdir[0] * extensions[0]
 | 
				
			||||||
            v[-1] += dvdir[-1] * extension
 | 
					            v[-1] += dvdir[-1] * extensions[1]
 | 
				
			||||||
            dv = numpy.diff(v, axis=0)      # recalculate dv; dvdir and perp should stay the same
 | 
					            dv = numpy.diff(v, axis=0)      # recalculate dv; dvdir and perp should stay the same
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Find intersections of expanded sides
 | 
					        # Find intersections of expanded sides
 | 
				
			||||||
        As = numpy.stack((dv[:-1], -dv[1:]), axis=2)
 | 
					        As = numpy.stack((dv[:-1], -dv[1:]), axis=2)
 | 
				
			||||||
        bs = v[1:-1] - v[:-2] + perp[1:] - perp[:-1]
 | 
					        bs = v[1:-1] - v[:-2] + perp[1:] - perp[:-1]
 | 
				
			||||||
@ -210,18 +266,18 @@ class Path(Shape):
 | 
				
			|||||||
            if towards_perp[i]:
 | 
					            if towards_perp[i]:
 | 
				
			||||||
                o0.append(intersection_p[i])
 | 
					                o0.append(intersection_p[i])
 | 
				
			||||||
                if acute[i]:
 | 
					                if acute[i]:
 | 
				
			||||||
                    o1.append(intersection_n[i])
 | 
					 | 
				
			||||||
                else:
 | 
					 | 
				
			||||||
                    # Opposite is >270
 | 
					                    # Opposite is >270
 | 
				
			||||||
                    pt0 = v[i + 1] - perp[i + 0] + dvdir[i + 0] * element.width / 2
 | 
					                    pt0 = v[i + 1] - perp[i + 0] + dvdir[i + 0] * self.width / 2
 | 
				
			||||||
                    pt1 = v[i + 1] - perp[i + 1] - dvdir[i + 1] * element.width / 2
 | 
					                    pt1 = v[i + 1] - perp[i + 1] - dvdir[i + 1] * self.width / 2
 | 
				
			||||||
                    o1 += [pt0, pt1]
 | 
					                    o1 += [pt0, pt1]
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    o1.append(intersection_n[i])
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                o1.append(intersection_n[i])
 | 
					                o1.append(intersection_n[i])
 | 
				
			||||||
                if acute[i]:
 | 
					                if acute[i]:
 | 
				
			||||||
                    # > 270
 | 
					                    # > 270
 | 
				
			||||||
                    pt0 = v[i + 1] + perp[i + 0] + dvdir[i + 0] * element.width / 2
 | 
					                    pt0 = v[i + 1] + perp[i + 0] + dvdir[i + 0] * self.width / 2
 | 
				
			||||||
                    pt1 = v[i + 1] + perp[i + 1] - dvdir[i + 1] * element.width / 2
 | 
					                    pt1 = v[i + 1] + perp[i + 1] - dvdir[i + 1] * self.width / 2
 | 
				
			||||||
                    o0 += [pt0, pt1]
 | 
					                    o0 += [pt0, pt1]
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    o0.append(intersection_p[i])
 | 
					                    o0.append(intersection_p[i])
 | 
				
			||||||
@ -231,34 +287,27 @@ class Path(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        polys = [Polygon(offset=self.offset, vertices=verts, dose=self.dose, layer=self.layer)]
 | 
					        polys = [Polygon(offset=self.offset, vertices=verts, dose=self.dose, layer=self.layer)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.cap == Path.Cap.Circle:
 | 
					        if self.cap == PathCap.Circle:
 | 
				
			||||||
            for vert in v:
 | 
					            #for vert in v:         # not sure if every vertex, or just ends?
 | 
				
			||||||
 | 
					            for vert in [v[0], v[-1]]:
 | 
				
			||||||
                circ = Circle(offset=vert, radius=self.width / 2, dose=self.dose, layer=self.layer)
 | 
					                circ = Circle(offset=vert, radius=self.width / 2, dose=self.dose, layer=self.layer)
 | 
				
			||||||
                polys += circ.to_polygons(poly_num_points=poly_num_points, poly_max_arclen=poly_max_arclen)
 | 
					                polys += circ.to_polygons(poly_num_points=poly_num_points, poly_max_arclen=poly_max_arclen)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return polys
 | 
					        return polys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_bounds(self) -> numpy.ndarray:
 | 
					    def get_bounds(self) -> numpy.ndarray:
 | 
				
			||||||
        if self.cap == Path.Cap.Circle:
 | 
					        if self.cap == PathCap.Circle:
 | 
				
			||||||
            bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
 | 
					            bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
 | 
				
			||||||
                                                 numpy.max(self.vertices, axis=0) + self.width / 2))
 | 
					                                                 numpy.max(self.vertices, axis=0) + self.width / 2))
 | 
				
			||||||
        elif self.cap in (Path.Cap.Flush,
 | 
					        elif self.cap in (PathCap.Flush,
 | 
				
			||||||
                          Path.Cap.Square):
 | 
					                          PathCap.Square,
 | 
				
			||||||
            if self.cap == Path.Cap.Flush:
 | 
					                          PathCap.SquareCustom):
 | 
				
			||||||
                extension = 0
 | 
					            bounds = numpy.array([[+inf, +inf], [-inf, -inf]])
 | 
				
			||||||
            elif self.cap == Path.Cap.Square:
 | 
					            polys = self.to_polygons()
 | 
				
			||||||
                extension = element.width / 2
 | 
					            for poly in polys:
 | 
				
			||||||
 | 
					                poly_bounds = poly.get_bounds()
 | 
				
			||||||
            v = remove_colinear_vertices(self.vertices, closed_path=False)
 | 
					                bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :])
 | 
				
			||||||
            dv = numpy.diff(v, axis=0)
 | 
					                bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :])
 | 
				
			||||||
            dvdir = dv / numpy.sqrt((dv * dv).sum(axis=1))[:, None]
 | 
					 | 
				
			||||||
            perp = dvdir[:, ::-1] * [[1, -1]] * element.width / 2
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            v[0] -= dvdir * extension
 | 
					 | 
				
			||||||
            v[-1] += dvdir * extension
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            bounds = self.offset + numpy.vstack((numpy.min(v - numpy.abs(perp), axis=0),
 | 
					 | 
				
			||||||
                                                 numpy.max(v + numpy.abs(perp), axis=0)))
 | 
					 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            raise PatternError('get_bounds() not implemented for endcaps: {}'.format(self.cap))
 | 
					            raise PatternError('get_bounds() not implemented for endcaps: {}'.format(self.cap))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -301,15 +350,16 @@ class Path(Shape):
 | 
				
			|||||||
        width0 = self.width / norm_value
 | 
					        width0 = self.width / norm_value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer), \
 | 
					        return (type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer), \
 | 
				
			||||||
               (offset, scale/norm_value, rotation, self.dose), \
 | 
					               (offset, scale/norm_value, rotation, False, self.dose), \
 | 
				
			||||||
               lambda: Polygon(reordered_vertices*norm_value, width=self.width*norm_value,
 | 
					               lambda: Path(reordered_vertices*norm_value, width=self.width*norm_value,
 | 
				
			||||||
                               cap=self.cap, layer=self.layer)
 | 
					                            cap=self.cap, layer=self.layer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean_vertices(self) -> 'Path':
 | 
					    def clean_vertices(self) -> 'Path':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Removes duplicate, co-linear and otherwise redundant vertices.
 | 
					        Removes duplicate, co-linear and otherwise redundant vertices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: self
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.remove_colinear_vertices()
 | 
					        self.remove_colinear_vertices()
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@ -318,7 +368,8 @@ class Path(Shape):
 | 
				
			|||||||
        '''
 | 
					        '''
 | 
				
			||||||
        Removes all consecutive duplicate (repeated) vertices.
 | 
					        Removes all consecutive duplicate (repeated) vertices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: self
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False)
 | 
					        self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@ -327,7 +378,38 @@ class Path(Shape):
 | 
				
			|||||||
        '''
 | 
					        '''
 | 
				
			||||||
        Removes consecutive co-linear vertices.
 | 
					        Removes consecutive co-linear vertices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: self
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        self.vertices = remove_colinear_vertices(self.vertices, closed_path=False)
 | 
					        self.vertices = remove_colinear_vertices(self.vertices, closed_path=False)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _calculate_cap_extensions(self) -> numpy.ndarray:
 | 
				
			||||||
 | 
					        if self.cap == PathCap.Square:
 | 
				
			||||||
 | 
					            extensions = numpy.full(2, self.width / 2)
 | 
				
			||||||
 | 
					        elif self.cap == PathCap.SquareCustom:
 | 
				
			||||||
 | 
					            extensions =  self.cap_extensions
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Flush or Circle
 | 
				
			||||||
 | 
					            extensions = numpy.zeros(2)
 | 
				
			||||||
 | 
					        return extensions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self) -> 'Path':
 | 
				
			||||||
 | 
					        self.vertices.flags.writeable = False
 | 
				
			||||||
 | 
					        if self.cap_extensions is not None:
 | 
				
			||||||
 | 
					            self.cap_extensions.flags.writeable = False
 | 
				
			||||||
 | 
					        Shape.lock(self)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self) -> 'Path':
 | 
				
			||||||
 | 
					        Shape.unlock(self)
 | 
				
			||||||
 | 
					        self.vertices.flags.writeable = True
 | 
				
			||||||
 | 
					        if self.cap_extensions is not None:
 | 
				
			||||||
 | 
					            self.cap_extensions.flags.writeable = True
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        centroid = self.offset + self.vertices.mean(axis=0)
 | 
				
			||||||
 | 
					        dose = f' d{self.dose:g}' if self.dose != 1 else ''
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}{dose}{locked}>'
 | 
				
			||||||
 | 
				
			|||||||
@ -1,38 +1,36 @@
 | 
				
			|||||||
from typing import List, Tuple
 | 
					from typing import List, Tuple, Dict, Optional, Sequence
 | 
				
			||||||
import copy
 | 
					import copy
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
from numpy import pi
 | 
					from numpy import pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import Shape, normalized_shape_tuple
 | 
					from . import Shape, normalized_shape_tuple
 | 
				
			||||||
from .. import PatternError
 | 
					from .. import PatternError
 | 
				
			||||||
from ..utils import is_scalar, rotation_matrix_2d, vector2
 | 
					from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
 | 
				
			||||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices
 | 
					from ..utils import remove_colinear_vertices, remove_duplicate_vertices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Polygon(Shape):
 | 
					class Polygon(Shape):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
 | 
					    A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
 | 
				
			||||||
       implicitly-closed boundary, and an offset.
 | 
					       implicitly-closed boundary, and an offset.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    A normalized_form(...) is available, but can be quite slow with lots of vertices.
 | 
					    A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    _vertices = None        # type: numpy.ndarray
 | 
					    __slots__ = ('_vertices',)
 | 
				
			||||||
 | 
					    _vertices: numpy.ndarray
 | 
				
			||||||
 | 
					    """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # vertices property
 | 
					    # vertices property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def vertices(self) -> numpy.ndarray:
 | 
					    def vertices(self) -> numpy.ndarray:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Vertices of the polygon (Nx2 ndarray: [[x0, y0], [x1, y1], ...]
 | 
					        Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: vertices
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._vertices
 | 
					        return self._vertices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @vertices.setter
 | 
					    @vertices.setter
 | 
				
			||||||
    def vertices(self, val: numpy.ndarray):
 | 
					    def vertices(self, val: numpy.ndarray):
 | 
				
			||||||
        val = numpy.array(val, dtype=float)
 | 
					        val = numpy.array(val, dtype=float)                  #TODO document that these might not be copied
 | 
				
			||||||
        if len(val.shape) < 2 or val.shape[1] != 2:
 | 
					        if len(val.shape) < 2 or val.shape[1] != 2:
 | 
				
			||||||
            raise PatternError('Vertices must be an Nx2 array')
 | 
					            raise PatternError('Vertices must be an Nx2 array')
 | 
				
			||||||
        if val.shape[0] < 3:
 | 
					        if val.shape[0] < 3:
 | 
				
			||||||
@ -71,83 +69,101 @@ class Polygon(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __init__(self,
 | 
					    def __init__(self,
 | 
				
			||||||
                 vertices: numpy.ndarray,
 | 
					                 vertices: numpy.ndarray,
 | 
				
			||||||
                 offset: vector2=(0.0, 0.0),
 | 
					                 offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                 rotation: float=0.0,
 | 
					                 rotation: float = 0.0,
 | 
				
			||||||
                 mirrored: Tuple[bool] = (False, False),
 | 
					                 mirrored: Sequence[bool] = (False, False),
 | 
				
			||||||
                 layer: int=0,
 | 
					                 layer: layer_t = 0,
 | 
				
			||||||
                 dose: float=1.0,
 | 
					                 dose: float = 1.0,
 | 
				
			||||||
 | 
					                 locked: bool = False,
 | 
				
			||||||
                 ):
 | 
					                 ):
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.identifier = ()
 | 
				
			||||||
        self.layer = layer
 | 
					        self.layer = layer
 | 
				
			||||||
        self.dose = dose
 | 
					        self.dose = dose
 | 
				
			||||||
        self.vertices = vertices
 | 
					        self.vertices = vertices
 | 
				
			||||||
        self.offset = offset
 | 
					        self.offset = offset
 | 
				
			||||||
        self.rotate(rotation)
 | 
					        self.rotate(rotation)
 | 
				
			||||||
        [self.mirror(a) for a, do in enumerate(mirrored) if do]
 | 
					        [self.mirror(a) for a, do in enumerate(mirrored) if do]
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = copy.copy(self).unlock()
 | 
				
			||||||
 | 
					        new._offset = self._offset.copy()
 | 
				
			||||||
 | 
					        new._vertices = self._vertices.copy()
 | 
				
			||||||
 | 
					        new.locked = self.locked
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def square(side_length: float,
 | 
					    def square(side_length: float,
 | 
				
			||||||
               rotation: float=0.0,
 | 
					               rotation: float = 0.0,
 | 
				
			||||||
               offset: vector2=(0.0, 0.0),
 | 
					               offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
               layer: int=0,
 | 
					               layer: layer_t = 0,
 | 
				
			||||||
               dose: float=1.0
 | 
					               dose: float = 1.0,
 | 
				
			||||||
               ) -> 'Polygon':
 | 
					               ) -> 'Polygon':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Draw a square given side_length, centered on the origin.
 | 
					        Draw a square given side_length, centered on the origin.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param side_length: Length of one side
 | 
					        Args:
 | 
				
			||||||
        :param rotation: Rotation counterclockwise, in radians
 | 
					            side_length: Length of one side
 | 
				
			||||||
        :param offset: Offset, default (0, 0)
 | 
					            rotation: Rotation counterclockwise, in radians
 | 
				
			||||||
        :param layer: Layer, default 0
 | 
					            offset: Offset, default `(0, 0)`
 | 
				
			||||||
        :param dose: Dose, default 1.0
 | 
					            layer: Layer, default `0`
 | 
				
			||||||
        :return: A Polygon object containing the requested square
 | 
					            dose: Dose, default `1.0`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            A Polygon object containing the requested square
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        norm_square = numpy.array([[-1, -1],
 | 
					        norm_square = numpy.array([[-1, -1],
 | 
				
			||||||
                                   [-1, +1],
 | 
					                                   [-1, +1],
 | 
				
			||||||
                                   [+1, +1],
 | 
					                                   [+1, +1],
 | 
				
			||||||
                                   [+1, -1]], dtype=float)
 | 
					                                   [+1, -1]], dtype=float)
 | 
				
			||||||
        vertices = 0.5 * side_length * norm_square
 | 
					        vertices = 0.5 * side_length * norm_square
 | 
				
			||||||
        poly = Polygon(vertices, offset, layer, dose)
 | 
					        poly = Polygon(vertices, offset=offset, layer=layer, dose=dose)
 | 
				
			||||||
        poly.rotate(rotation)
 | 
					        poly.rotate(rotation)
 | 
				
			||||||
        return poly
 | 
					        return poly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def rectangle(lx: float,
 | 
					    def rectangle(lx: float,
 | 
				
			||||||
                  ly: float,
 | 
					                  ly: float,
 | 
				
			||||||
                  rotation: float=0,
 | 
					                  rotation: float = 0,
 | 
				
			||||||
                  offset: vector2=(0.0, 0.0),
 | 
					                  offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                  layer: int=0,
 | 
					                  layer: layer_t = 0,
 | 
				
			||||||
                  dose: float=1.0
 | 
					                  dose: float = 1.0,
 | 
				
			||||||
                  ) -> 'Polygon':
 | 
					                  ) -> 'Polygon':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Draw a rectangle with side lengths lx and ly, centered on the origin.
 | 
					        Draw a rectangle with side lengths lx and ly, centered on the origin.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param lx: Length along x (before rotation)
 | 
					        Args:
 | 
				
			||||||
        :param ly: Length along y (before rotation)
 | 
					            lx: Length along x (before rotation)
 | 
				
			||||||
        :param rotation: Rotation counterclockwise, in radians
 | 
					            ly: Length along y (before rotation)
 | 
				
			||||||
        :param offset: Offset, default (0, 0)
 | 
					            rotation: Rotation counterclockwise, in radians
 | 
				
			||||||
        :param layer: Layer, default 0
 | 
					            offset: Offset, default `(0, 0)`
 | 
				
			||||||
        :param dose: Dose, default 1.0
 | 
					            layer: Layer, default `0`
 | 
				
			||||||
        :return: A Polygon object containing the requested rectangle
 | 
					            dose: Dose, default `1.0`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            A Polygon object containing the requested rectangle
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        vertices = 0.5 * numpy.array([[-lx, -ly],
 | 
					        vertices = 0.5 * numpy.array([[-lx, -ly],
 | 
				
			||||||
                                      [-lx, +ly],
 | 
					                                      [-lx, +ly],
 | 
				
			||||||
                                      [+lx, +ly],
 | 
					                                      [+lx, +ly],
 | 
				
			||||||
                                      [+lx, -ly]], dtype=float)
 | 
					                                      [+lx, -ly]], dtype=float)
 | 
				
			||||||
        poly = Polygon(vertices, offset, layer, dose)
 | 
					        poly = Polygon(vertices, offset=offset, layer=layer, dose=dose)
 | 
				
			||||||
        poly.rotate(rotation)
 | 
					        poly.rotate(rotation)
 | 
				
			||||||
        return poly
 | 
					        return poly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def rect(xmin: float = None,
 | 
					    def rect(xmin: Optional[float] = None,
 | 
				
			||||||
             xctr: float = None,
 | 
					             xctr: Optional[float] = None,
 | 
				
			||||||
             xmax: float = None,
 | 
					             xmax: Optional[float] = None,
 | 
				
			||||||
             lx: float = None,
 | 
					             lx: Optional[float] = None,
 | 
				
			||||||
             ymin: float = None,
 | 
					             ymin: Optional[float] = None,
 | 
				
			||||||
             yctr: float = None,
 | 
					             yctr: Optional[float] = None,
 | 
				
			||||||
             ymax: float = None,
 | 
					             ymax: Optional[float] = None,
 | 
				
			||||||
             ly: float = None,
 | 
					             ly: Optional[float] = None,
 | 
				
			||||||
             layer: int = 0,
 | 
					             layer: layer_t = 0,
 | 
				
			||||||
             dose: float = 1.0
 | 
					             dose: float = 1.0,
 | 
				
			||||||
             ) -> 'Polygon':
 | 
					             ) -> 'Polygon':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Draw a rectangle by specifying side/center positions.
 | 
					        Draw a rectangle by specifying side/center positions.
 | 
				
			||||||
@ -155,25 +171,34 @@ class Polygon(Shape):
 | 
				
			|||||||
        Must provide 2 of (xmin, xctr, xmax, lx),
 | 
					        Must provide 2 of (xmin, xctr, xmax, lx),
 | 
				
			||||||
        and 2 of (ymin, yctr, ymax, ly).
 | 
					        and 2 of (ymin, yctr, ymax, ly).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param xmin: Minimum x coordinate
 | 
					        Args:
 | 
				
			||||||
        :param xctr: Center x coordinate
 | 
					            xmin: Minimum x coordinate
 | 
				
			||||||
        :param xmax: Maximum x coordinate
 | 
					            xctr: Center x coordinate
 | 
				
			||||||
        :param lx: Length along x direction
 | 
					            xmax: Maximum x coordinate
 | 
				
			||||||
        :param ymin: Minimum y coordinate
 | 
					            lx: Length along x direction
 | 
				
			||||||
        :param yctr: Center y coordinate
 | 
					            ymin: Minimum y coordinate
 | 
				
			||||||
        :param ymax: Maximum y coordinate
 | 
					            yctr: Center y coordinate
 | 
				
			||||||
        :param ly: Length along y direction
 | 
					            ymax: Maximum y coordinate
 | 
				
			||||||
        :param layer: Layer, default 0
 | 
					            ly: Length along y direction
 | 
				
			||||||
        :param dose: Dose, default 1.0
 | 
					            layer: Layer, default `0`
 | 
				
			||||||
        :return: A Polygon object containing the requested rectangle
 | 
					            dose: Dose, default `1.0`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            A Polygon object containing the requested rectangle
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if lx is None:
 | 
					        if lx is None:
 | 
				
			||||||
            if xctr is None:
 | 
					            if xctr is None:
 | 
				
			||||||
 | 
					                assert(xmin is not None)
 | 
				
			||||||
 | 
					                assert(xmax is not None)
 | 
				
			||||||
                xctr = 0.5 * (xmax + xmin)
 | 
					                xctr = 0.5 * (xmax + xmin)
 | 
				
			||||||
                lx = xmax - xmin
 | 
					                lx = xmax - xmin
 | 
				
			||||||
            elif xmax is None:
 | 
					            elif xmax is None:
 | 
				
			||||||
 | 
					                assert(xmin is not None)
 | 
				
			||||||
 | 
					                assert(xctr is not None)
 | 
				
			||||||
                lx = 2 * (xctr - xmin)
 | 
					                lx = 2 * (xctr - xmin)
 | 
				
			||||||
            elif xmin is None:
 | 
					            elif xmin is None:
 | 
				
			||||||
 | 
					                assert(xctr is not None)
 | 
				
			||||||
 | 
					                assert(xmax is not None)
 | 
				
			||||||
                lx = 2 * (xmax - xctr)
 | 
					                lx = 2 * (xmax - xctr)
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
 | 
					                raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
 | 
				
			||||||
@ -181,19 +206,29 @@ class Polygon(Shape):
 | 
				
			|||||||
            if xctr is not None:
 | 
					            if xctr is not None:
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
            elif xmax is None:
 | 
					            elif xmax is None:
 | 
				
			||||||
 | 
					                assert(xmin is not None)
 | 
				
			||||||
 | 
					                assert(lx is not None)
 | 
				
			||||||
                xctr = xmin + 0.5 * lx
 | 
					                xctr = xmin + 0.5 * lx
 | 
				
			||||||
            elif xmin is None:
 | 
					            elif xmin is None:
 | 
				
			||||||
 | 
					                assert(xmax is not None)
 | 
				
			||||||
 | 
					                assert(lx is not None)
 | 
				
			||||||
                xctr = xmax - 0.5 * lx
 | 
					                xctr = xmax - 0.5 * lx
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
 | 
					                raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ly is None:
 | 
					        if ly is None:
 | 
				
			||||||
            if yctr is None:
 | 
					            if yctr is None:
 | 
				
			||||||
 | 
					                assert(ymin is not None)
 | 
				
			||||||
 | 
					                assert(ymax is not None)
 | 
				
			||||||
                yctr = 0.5 * (ymax + ymin)
 | 
					                yctr = 0.5 * (ymax + ymin)
 | 
				
			||||||
                ly = ymax - ymin
 | 
					                ly = ymax - ymin
 | 
				
			||||||
            elif ymax is None:
 | 
					            elif ymax is None:
 | 
				
			||||||
 | 
					                assert(ymin is not None)
 | 
				
			||||||
 | 
					                assert(yctr is not None)
 | 
				
			||||||
                ly = 2 * (yctr - ymin)
 | 
					                ly = 2 * (yctr - ymin)
 | 
				
			||||||
            elif ymin is None:
 | 
					            elif ymin is None:
 | 
				
			||||||
 | 
					                assert(yctr is not None)
 | 
				
			||||||
 | 
					                assert(ymax is not None)
 | 
				
			||||||
                ly = 2 * (ymax - yctr)
 | 
					                ly = 2 * (ymax - yctr)
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
 | 
					                raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
 | 
				
			||||||
@ -201,8 +236,12 @@ class Polygon(Shape):
 | 
				
			|||||||
            if yctr is not None:
 | 
					            if yctr is not None:
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
            elif ymax is None:
 | 
					            elif ymax is None:
 | 
				
			||||||
 | 
					                assert(ymin is not None)
 | 
				
			||||||
 | 
					                assert(ly is not None)
 | 
				
			||||||
                yctr = ymin + 0.5 * ly
 | 
					                yctr = ymin + 0.5 * ly
 | 
				
			||||||
            elif ymin is None:
 | 
					            elif ymin is None:
 | 
				
			||||||
 | 
					                assert(ly is not None)
 | 
				
			||||||
 | 
					                assert(ymax is not None)
 | 
				
			||||||
                yctr = ymax - 0.5 * ly
 | 
					                yctr = ymax - 0.5 * ly
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
 | 
					                raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
 | 
				
			||||||
@ -213,8 +252,8 @@ class Polygon(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_polygons(self,
 | 
					    def to_polygons(self,
 | 
				
			||||||
                    _poly_num_points: int=None,
 | 
					                    poly_num_points: int = None,        # unused
 | 
				
			||||||
                    _poly_max_arclen: float=None,
 | 
					                    poly_max_arclen: float = None,      # unused
 | 
				
			||||||
                    ) -> List['Polygon']:
 | 
					                    ) -> List['Polygon']:
 | 
				
			||||||
        return [copy.deepcopy(self)]
 | 
					        return [copy.deepcopy(self)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -255,15 +294,18 @@ class Polygon(Shape):
 | 
				
			|||||||
            x_min = x_min[y_min]
 | 
					            x_min = x_min[y_min]
 | 
				
			||||||
        reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
 | 
					        reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO: normalize mirroring?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (type(self), reordered_vertices.data.tobytes(), self.layer), \
 | 
					        return (type(self), reordered_vertices.data.tobytes(), self.layer), \
 | 
				
			||||||
               (offset, scale/norm_value, rotation, self.dose), \
 | 
					               (offset, scale/norm_value, rotation, False, self.dose), \
 | 
				
			||||||
               lambda: Polygon(reordered_vertices*norm_value, layer=self.layer)
 | 
					               lambda: Polygon(reordered_vertices*norm_value, layer=self.layer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean_vertices(self) -> 'Polygon':
 | 
					    def clean_vertices(self) -> 'Polygon':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Removes duplicate, co-linear and otherwise redundant vertices.
 | 
					        Removes duplicate, co-linear and otherwise redundant vertices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: self
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.remove_colinear_vertices()
 | 
					        self.remove_colinear_vertices()
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@ -272,7 +314,8 @@ class Polygon(Shape):
 | 
				
			|||||||
        '''
 | 
					        '''
 | 
				
			||||||
        Removes all consecutive duplicate (repeated) vertices.
 | 
					        Removes all consecutive duplicate (repeated) vertices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: self
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True)
 | 
					        self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@ -281,7 +324,24 @@ class Polygon(Shape):
 | 
				
			|||||||
        '''
 | 
					        '''
 | 
				
			||||||
        Removes consecutive co-linear vertices.
 | 
					        Removes consecutive co-linear vertices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :returns: self
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
 | 
					        self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self) -> 'Polygon':
 | 
				
			||||||
 | 
					        self.vertices.flags.writeable = False
 | 
				
			||||||
 | 
					        Shape.lock(self)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self) -> 'Polygon':
 | 
				
			||||||
 | 
					        Shape.unlock(self)
 | 
				
			||||||
 | 
					        self.vertices.flags.writeable = True
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        centroid = self.offset + self.vertices.mean(axis=0)
 | 
				
			||||||
 | 
					        dose = f' d{self.dose:g}' if self.dose != 1 else ''
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}{dose}{locked}>'
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,18 @@
 | 
				
			|||||||
from typing import List, Tuple, Callable
 | 
					from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING
 | 
				
			||||||
from abc import ABCMeta, abstractmethod
 | 
					from abc import ABCMeta, abstractmethod
 | 
				
			||||||
import copy
 | 
					import copy
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .. import PatternError
 | 
					from ..error import PatternError, PatternLockedError
 | 
				
			||||||
from ..utils import is_scalar, rotation_matrix_2d, vector2
 | 
					from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					    from . import Polygon
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Type definitions
 | 
					# Type definitions
 | 
				
			||||||
normalized_shape_tuple = Tuple[Tuple,
 | 
					normalized_shape_tuple = Tuple[Tuple,
 | 
				
			||||||
                               Tuple[numpy.ndarray, float, float, float],
 | 
					                               Tuple[numpy.ndarray, float, float, bool, float],
 | 
				
			||||||
                               Callable[[], 'Shape']]
 | 
					                               Callable[[], 'Shape']]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ## Module-wide defaults
 | 
					# ## Module-wide defaults
 | 
				
			||||||
@ -20,103 +20,144 @@ normalized_shape_tuple = Tuple[Tuple,
 | 
				
			|||||||
DEFAULT_POLY_NUM_POINTS = 24
 | 
					DEFAULT_POLY_NUM_POINTS = 24
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					T = TypeVar('T', bound='Shape')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Shape(metaclass=ABCMeta):
 | 
					class Shape(metaclass=ABCMeta):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Abstract class specifying functions common to all shapes.
 | 
					    Abstract class specifying functions common to all shapes.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # [x_offset, y_offset]
 | 
					    _offset: numpy.ndarray
 | 
				
			||||||
    _offset = numpy.array([0.0, 0.0])   # type: numpy.ndarray
 | 
					    """ `[x_offset, y_offset]` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Layer (integer >= 0 or tuple)
 | 
					    _layer: layer_t
 | 
				
			||||||
    _layer = 0                          # type: int or Tuple
 | 
					    """ Layer (integer >= 0 or tuple) """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Dose
 | 
					    _dose: float
 | 
				
			||||||
    _dose = 1.0                         # type: float
 | 
					    """ Dose """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # --- Abstract methods
 | 
					    identifier: Tuple
 | 
				
			||||||
 | 
					    """ An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    locked: bool
 | 
				
			||||||
 | 
					    """ If `True`, any changes to the shape will raise a `PatternLockedError` """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __setattr__(self, name, value):
 | 
				
			||||||
 | 
					        if self.locked and name != 'locked':
 | 
				
			||||||
 | 
					            raise PatternLockedError()
 | 
				
			||||||
 | 
					        object.__setattr__(self, name, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __copy__(self) -> 'Shape':
 | 
				
			||||||
 | 
					        cls = self.__class__
 | 
				
			||||||
 | 
					        new = cls.__new__(cls)
 | 
				
			||||||
 | 
					        for name in Shape.__slots__ + self.__slots__:
 | 
				
			||||||
 | 
					            object.__setattr__(new, name, getattr(self, name))
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    --- Abstract methods
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
    @abstractmethod
 | 
					    @abstractmethod
 | 
				
			||||||
    def to_polygons(self, num_vertices: int, max_arclen: float) -> List['Polygon']:
 | 
					    def to_polygons(self,
 | 
				
			||||||
 | 
					                    num_vertices: Optional[int] = None,
 | 
				
			||||||
 | 
					                    max_arclen: Optional[float] = None,
 | 
				
			||||||
 | 
					                    ) -> List['Polygon']:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns a list of polygons which approximate the shape.
 | 
					        Returns a list of polygons which approximate the shape.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param num_vertices: Number of points to use for each polygon. Can be overridden by
 | 
					        Args:
 | 
				
			||||||
                     max_arclen if that results in more points. Optional, defaults to shapes'
 | 
					            num_vertices: Number of points to use for each polygon. Can be overridden by
 | 
				
			||||||
                      internal defaults.
 | 
					                  max_arclen if that results in more points. Optional, defaults to shapes'
 | 
				
			||||||
        :param max_arclen: Maximum arclength which can be approximated by a single line
 | 
					                  internal defaults.
 | 
				
			||||||
                     segment. Optional, defaults to shapes' internal defaults.
 | 
					            max_arclen: Maximum arclength which can be approximated by a single line
 | 
				
			||||||
        :return: List of polygons equivalent to the shape
 | 
					                  segment. Optional, defaults to shapes' internal defaults.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            List of polygons equivalent to the shape
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @abstractmethod
 | 
					    @abstractmethod
 | 
				
			||||||
    def get_bounds(self) -> numpy.ndarray:
 | 
					    def get_bounds(self) -> numpy.ndarray:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns [[x_min, y_min], [x_max, y_max]] which specify a minimal bounding box for the shape.
 | 
					        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
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @abstractmethod
 | 
					    @abstractmethod
 | 
				
			||||||
    def rotate(self, theta: float) -> 'Shape':
 | 
					    def rotate(self: T, theta: float) -> T:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate the shape around its center (0, 0), ignoring its offset.
 | 
					        Rotate the shape around its origin (0, 0), ignoring its offset.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param theta: Angle to rotate by (counterclockwise, radians)
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            theta: Angle to rotate by (counterclockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @abstractmethod
 | 
					    @abstractmethod
 | 
				
			||||||
    def mirror(self, axis: int) -> 'Shape':
 | 
					    def mirror(self: T, axis: int) -> T:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Mirror the shape across an axis.
 | 
					        Mirror the shape across an axis.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param axis: Axis to mirror across.
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            axis: Axis to mirror across.
 | 
				
			||||||
 | 
					                (0: mirror across x axis, 1: mirror across y axis)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @abstractmethod
 | 
					    @abstractmethod
 | 
				
			||||||
    def scale_by(self, c: float) -> 'Shape':
 | 
					    def scale_by(self: T, c: float) -> T:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Scale the shape's size (eg. radius, for a circle) by a constant factor.
 | 
					        Scale the shape's size (eg. radius, for a circle) by a constant factor.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param c: Factor to scale by
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            c: Factor to scale by
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @abstractmethod
 | 
					    @abstractmethod
 | 
				
			||||||
    def normalized_form(self, norm_value: int) -> normalized_shape_tuple:
 | 
					    def normalized_form(self: T, norm_value: int) -> normalized_shape_tuple:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Writes the shape in a standardized notation, with offset, scale, rotation, and dose
 | 
					        Writes the shape in a standardized notation, with offset, scale, rotation, and dose
 | 
				
			||||||
         information separated out from the remaining values.
 | 
					         information separated out from the remaining values.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param norm_value: This value is used to normalize lengths intrinsic to the shape;
 | 
					        Args:
 | 
				
			||||||
 | 
					            norm_value: This value is used to normalize lengths intrinsic to the shape;
 | 
				
			||||||
                eg. for a circle, the returned intrinsic radius value will be (radius / norm_value), and
 | 
					                eg. for a circle, the returned intrinsic radius value will be (radius / norm_value), and
 | 
				
			||||||
                the returned callable will create a Circle(radius=norm_value, ...). This is useful
 | 
					                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
 | 
					                when you find it important for quantities to remain in a certain range, eg. for
 | 
				
			||||||
                GDSII where vertex locations are stored as integers.
 | 
					                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:
 | 
					        Returns:
 | 
				
			||||||
                intrinsic: A tuple of basic types containing all information about the instance that
 | 
					            The returned information takes the form of a 3-element tuple,
 | 
				
			||||||
                            is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self).
 | 
					              `(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
 | 
				
			||||||
                constructor: A callable (no arguments) which returns an instance of type(self) with
 | 
					                         is not contained in 'extrinsic'. Usually, `intrinsic[0] == type(self)`.
 | 
				
			||||||
                            internal state equivalent to 'intrinsic'.
 | 
					              `extrinsic`: `([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose)`
 | 
				
			||||||
 | 
					              `constructor`: A callable (no arguments) which returns an instance of `type(self)` with
 | 
				
			||||||
 | 
					                           internal state equivalent to `intrinsic`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ---- Non-abstract properties
 | 
					    '''
 | 
				
			||||||
 | 
					    ---- Non-abstract properties
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
    # offset property
 | 
					    # offset property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def offset(self) -> numpy.ndarray:
 | 
					    def offset(self) -> numpy.ndarray:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        [x, y] offset
 | 
					        [x, y] offset
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: [x_offset, y_offset]
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._offset
 | 
					        return self._offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,16 +172,14 @@ class Shape(metaclass=ABCMeta):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # layer property
 | 
					    # layer property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def layer(self) -> int or Tuple[int]:
 | 
					    def layer(self) -> layer_t:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Layer number (int or tuple of ints)
 | 
					        Layer number or name (int, tuple of ints, or string)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: Layer
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._layer
 | 
					        return self._layer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @layer.setter
 | 
					    @layer.setter
 | 
				
			||||||
    def layer(self, val: int or List[int]):
 | 
					    def layer(self, val: layer_t):
 | 
				
			||||||
        self._layer = val
 | 
					        self._layer = val
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # dose property
 | 
					    # dose property
 | 
				
			||||||
@ -148,8 +187,6 @@ class Shape(metaclass=ABCMeta):
 | 
				
			|||||||
    def dose(self) -> float:
 | 
					    def dose(self) -> float:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Dose (float >= 0)
 | 
					        Dose (float >= 0)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: Dose value
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self._dose
 | 
					        return self._dose
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -161,32 +198,41 @@ class Shape(metaclass=ABCMeta):
 | 
				
			|||||||
            raise PatternError('Dose must be non-negative')
 | 
					            raise PatternError('Dose must be non-negative')
 | 
				
			||||||
        self._dose = val
 | 
					        self._dose = val
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ---- Non-abstract methods
 | 
					    '''
 | 
				
			||||||
    def copy(self) -> 'Shape':
 | 
					    ---- Non-abstract methods
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    def copy(self: T) -> T:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns a deep copy of the shape.
 | 
					        Returns a deep copy of the shape.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: Deep copy of self
 | 
					        Returns:
 | 
				
			||||||
 | 
					            copy.deepcopy(self)
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return copy.deepcopy(self)
 | 
					        return copy.deepcopy(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def translate(self, offset: vector2) -> 'Shape':
 | 
					    def translate(self: T, offset: vector2) -> T:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Translate the shape by the given offset
 | 
					        Translate the shape by the given offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param offset: [x_offset, y,offset]
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            offset: [x_offset, y,offset]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.offset += offset
 | 
					        self.offset += offset
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def rotate_around(self, pivot: vector2, rotation: float) -> 'Shape':
 | 
					    def rotate_around(self: T, pivot: vector2, rotation: float) -> T:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate the shape around a point.
 | 
					        Rotate the shape around a point.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param pivot: Point (x, y) to rotate around
 | 
					        Args:
 | 
				
			||||||
        :param rotation: Angle to rotate by (counterclockwise, radians)
 | 
					            pivot: Point (x, y) to rotate around
 | 
				
			||||||
        :return: self
 | 
					            rotation: Angle to rotate by (counterclockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pivot = numpy.array(pivot, dtype=float)
 | 
					        pivot = numpy.array(pivot, dtype=float)
 | 
				
			||||||
        self.translate(-pivot)
 | 
					        self.translate(-pivot)
 | 
				
			||||||
@ -195,19 +241,25 @@ class Shape(metaclass=ABCMeta):
 | 
				
			|||||||
        self.translate(+pivot)
 | 
					        self.translate(+pivot)
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def manhattanize_fast(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']:
 | 
					    def manhattanize_fast(self,
 | 
				
			||||||
 | 
					                          grid_x: numpy.ndarray,
 | 
				
			||||||
 | 
					                          grid_y: numpy.ndarray,
 | 
				
			||||||
 | 
					                          ) -> List['Polygon']:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape.
 | 
					        Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        This function works by
 | 
					        This function works by
 | 
				
			||||||
            1) Converting the shape to polygons using .to_polygons()
 | 
					            1) Converting the shape to polygons using `.to_polygons()`
 | 
				
			||||||
            2) Approximating each edge with an equivalent Manhattan edge
 | 
					            2) Approximating each edge with an equivalent Manhattan edge
 | 
				
			||||||
        This process results in a reasonable Manhattan representation of the shape, but is
 | 
					        This process results in a reasonable Manhattan representation of the shape, but is
 | 
				
			||||||
          imprecise near non-Manhattan or off-grid corners.
 | 
					          imprecise near non-Manhattan or off-grid corners.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
 | 
					        Args:
 | 
				
			||||||
        :param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
 | 
					            grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
 | 
				
			||||||
        :return: List of Polygon objects with grid-aligned edges.
 | 
					            grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            List of `Polygon` objects with grid-aligned edges.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from . import Polygon
 | 
					        from . import Polygon
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -297,12 +349,15 @@ class Shape(metaclass=ABCMeta):
 | 
				
			|||||||
        return manhattan_polygons
 | 
					        return manhattan_polygons
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def manhattanize(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']:
 | 
					    def manhattanize(self,
 | 
				
			||||||
 | 
					                     grid_x: numpy.ndarray,
 | 
				
			||||||
 | 
					                     grid_y: numpy.ndarray
 | 
				
			||||||
 | 
					                     ) -> List['Polygon']:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape.
 | 
					        Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        This function works by
 | 
					        This function works by
 | 
				
			||||||
            1) Converting the shape to polygons using .to_polygons()
 | 
					            1) Converting the shape to polygons using `.to_polygons()`
 | 
				
			||||||
            2) Accurately rasterizing each polygon on a grid,
 | 
					            2) Accurately rasterizing each polygon on a grid,
 | 
				
			||||||
                where the edges of each grid cell correspond to the allowed coordinates
 | 
					                where the edges of each grid cell correspond to the allowed coordinates
 | 
				
			||||||
            3) Thresholding the (anti-aliased) rasterized image
 | 
					            3) Thresholding the (anti-aliased) rasterized image
 | 
				
			||||||
@ -311,7 +366,7 @@ class Shape(metaclass=ABCMeta):
 | 
				
			|||||||
          caveats include:
 | 
					          caveats include:
 | 
				
			||||||
            a) If high accuracy is important, perform any polygonization and clipping operations
 | 
					            a) If high accuracy is important, perform any polygonization and clipping operations
 | 
				
			||||||
                prior to calling this function. This allows you to specify any arguments you may
 | 
					                prior to calling this function. This allows you to specify any arguments you may
 | 
				
			||||||
                need for .to_polygons(), and also avoids calling .manhattanize() multiple times for
 | 
					                need for `.to_polygons()`, and also avoids calling `.manhattanize()` multiple times for
 | 
				
			||||||
                the same grid location (which causes inaccuracies in the final representation).
 | 
					                the same grid location (which causes inaccuracies in the final representation).
 | 
				
			||||||
            b) If the shape is very large or the grid very fine, memory requirements can be reduced
 | 
					            b) If the shape is very large or the grid very fine, memory requirements can be reduced
 | 
				
			||||||
                by breaking the shape apart into multiple, smaller shapes.
 | 
					                by breaking the shape apart into multiple, smaller shapes.
 | 
				
			||||||
@ -319,19 +374,22 @@ class Shape(metaclass=ABCMeta):
 | 
				
			|||||||
                equidistant from allowed edge location.
 | 
					                equidistant from allowed edge location.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Implementation notes:
 | 
					        Implementation notes:
 | 
				
			||||||
            i) Rasterization is performed using float_raster, giving a high-precision anti-aliased
 | 
					            i) Rasterization is performed using `float_raster`, giving a high-precision anti-aliased
 | 
				
			||||||
                rasterized image.
 | 
					                rasterized image.
 | 
				
			||||||
            ii) To find the exact polygon edges, the thresholded rasterized image is supersampled
 | 
					            ii) To find the exact polygon edges, the thresholded rasterized image is supersampled
 | 
				
			||||||
                  prior to calling skimage.measure.find_contours(), which uses marching squares
 | 
					                  prior to calling `skimage.measure.find_contours()`, which uses marching squares
 | 
				
			||||||
                  to find the contours. This is done because find_contours() performs interpolation,
 | 
					                  to find the contours. This is done because `find_contours()` performs interpolation,
 | 
				
			||||||
                  which has to be undone in order to regain the axis-aligned contours. A targetted
 | 
					                  which has to be undone in order to regain the axis-aligned contours. A targetted
 | 
				
			||||||
                  rewrite of find_contours() for this specific application, or use of a different
 | 
					                  rewrite of `find_contours()` for this specific application, or use of a different
 | 
				
			||||||
                  boundary tracing method could remove this requirement, but for now this seems to
 | 
					                  boundary tracing method could remove this requirement, but for now this seems to
 | 
				
			||||||
                  be the most performant approach.
 | 
					                  be the most performant approach.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
 | 
					        Args:
 | 
				
			||||||
        :param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
 | 
					            grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
 | 
				
			||||||
        :return: List of Polygon objects with grid-aligned edges.
 | 
					            grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            List of `Polygon` objects with grid-aligned edges.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        from . import Polygon
 | 
					        from . import Polygon
 | 
				
			||||||
        import skimage.measure
 | 
					        import skimage.measure
 | 
				
			||||||
@ -384,3 +442,37 @@ class Shape(metaclass=ABCMeta):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return manhattan_polygons
 | 
					        return manhattan_polygons
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def set_layer(self: T, layer: layer_t) -> T:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Chainable method for changing the layer.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            layer: new value for self.layer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.layer = layer
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self: T) -> T:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Lock the Shape, disallowing further changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = False
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', True)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self: T) -> T:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Unlock the Shape
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = True
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
				
			|||||||
@ -1,25 +1,28 @@
 | 
				
			|||||||
from typing import List, Tuple
 | 
					from typing import List, Tuple, Dict, Sequence, Optional, MutableSequence
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
from numpy import pi, inf
 | 
					from numpy import pi, inf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import Shape, Polygon, normalized_shape_tuple
 | 
					from . import Shape, Polygon, normalized_shape_tuple
 | 
				
			||||||
from .. import PatternError
 | 
					from .. import PatternError
 | 
				
			||||||
from ..utils import is_scalar, vector2, get_bit
 | 
					from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Loaded on use:
 | 
					# Loaded on use:
 | 
				
			||||||
# from freetype import Face
 | 
					# from freetype import Face
 | 
				
			||||||
# from matplotlib.path import Path
 | 
					# from matplotlib.path import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Text(Shape):
 | 
					class Text(Shape):
 | 
				
			||||||
    _string = ''
 | 
					    """
 | 
				
			||||||
    _height = 1.0
 | 
					    Text (to be printed e.g. as a set of polygons).
 | 
				
			||||||
    _rotation = 0.0
 | 
					    This is distinct from non-printed Label objects.
 | 
				
			||||||
    _mirrored = None
 | 
					    """
 | 
				
			||||||
    font_path = ''
 | 
					    __slots__ = ('_string', '_height', '_rotation', '_mirrored', 'font_path')
 | 
				
			||||||
 | 
					    _string: str
 | 
				
			||||||
 | 
					    _height: float
 | 
				
			||||||
 | 
					    _rotation: float
 | 
				
			||||||
 | 
					    _mirrored: numpy.ndarray        #ndarray[bool]
 | 
				
			||||||
 | 
					    font_path: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # vertices property
 | 
					    # vertices property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
@ -54,24 +57,28 @@ class Text(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Mirrored property
 | 
					    # Mirrored property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def mirrored(self) -> List[bool]:
 | 
					    def mirrored(self) -> numpy.ndarray:        #ndarray[bool]
 | 
				
			||||||
        return self._mirrored
 | 
					        return self._mirrored
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @mirrored.setter
 | 
					    @mirrored.setter
 | 
				
			||||||
    def mirrored(self, val: List[bool]):
 | 
					    def mirrored(self, val: Sequence[bool]):
 | 
				
			||||||
        if is_scalar(val):
 | 
					        if is_scalar(val):
 | 
				
			||||||
            raise PatternError('Mirrored must be a 2-element list of booleans')
 | 
					            raise PatternError('Mirrored must be a 2-element list of booleans')
 | 
				
			||||||
        self._mirrored = list(val)
 | 
					        self._mirrored = numpy.ndarray(val, dtype=bool, copy=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self,
 | 
					    def __init__(self,
 | 
				
			||||||
                 string: str,
 | 
					                 string: str,
 | 
				
			||||||
                 height: float,
 | 
					                 height: float,
 | 
				
			||||||
                 font_path: str,
 | 
					                 font_path: str,
 | 
				
			||||||
                 offset: vector2=(0.0, 0.0),
 | 
					                 offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                 rotation: float=0.0,
 | 
					                 rotation: float = 0.0,
 | 
				
			||||||
                 mirrored: Tuple[bool]=(False, False),
 | 
					                 mirrored: Tuple[bool, bool] = (False, False),
 | 
				
			||||||
                 layer: int=0,
 | 
					                 layer: layer_t = 0,
 | 
				
			||||||
                 dose: float=1.0):
 | 
					                 dose: float = 1.0,
 | 
				
			||||||
 | 
					                 locked: bool = False,
 | 
				
			||||||
 | 
					                 ):
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.identifier = ()
 | 
				
			||||||
        self.offset = offset
 | 
					        self.offset = offset
 | 
				
			||||||
        self.layer = layer
 | 
					        self.layer = layer
 | 
				
			||||||
        self.dose = dose
 | 
					        self.dose = dose
 | 
				
			||||||
@ -80,13 +87,22 @@ class Text(Shape):
 | 
				
			|||||||
        self.rotation = rotation
 | 
					        self.rotation = rotation
 | 
				
			||||||
        self.font_path = font_path
 | 
					        self.font_path = font_path
 | 
				
			||||||
        self.mirrored = mirrored
 | 
					        self.mirrored = mirrored
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __deepcopy__(self, memo: Dict = None) -> 'Text':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = copy.copy(self).unlock()
 | 
				
			||||||
 | 
					        new._offset = self._offset.copy()
 | 
				
			||||||
 | 
					        new._mirrored = copy.deepcopy(self._mirrored, memo)
 | 
				
			||||||
 | 
					        new.locked = self.locked
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_polygons(self,
 | 
					    def to_polygons(self,
 | 
				
			||||||
                    _poly_num_points: int=None,
 | 
					                    poly_num_points: Optional[int] = None,        # unused
 | 
				
			||||||
                    _poly_max_arclen: float=None
 | 
					                    poly_max_arclen: Optional[float] = None,      # unused
 | 
				
			||||||
                    ) -> List[Polygon]:
 | 
					                    ) -> List[Polygon]:
 | 
				
			||||||
        all_polygons = []
 | 
					        all_polygons = []
 | 
				
			||||||
        total_advance = 0
 | 
					        total_advance = 0.0
 | 
				
			||||||
        for char in self.string:
 | 
					        for char in self.string:
 | 
				
			||||||
            raw_polys, advance = get_char_as_polygons(self.font_path, char)
 | 
					            raw_polys, advance = get_char_as_polygons(self.font_path, char)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -117,18 +133,22 @@ class Text(Shape):
 | 
				
			|||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
 | 
					    def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
 | 
				
			||||||
        return (type(self), self.string, self.font_path, self.mirrored, self.layer), \
 | 
					        mirror_x, rotation = normalize_mirror(self.mirrored)
 | 
				
			||||||
               (self.offset, self.height / norm_value, self.rotation, self.dose), \
 | 
					        rotation += self.rotation
 | 
				
			||||||
 | 
					        rotation %= 2 * pi
 | 
				
			||||||
 | 
					        return (type(self), self.string, self.font_path, self.layer), \
 | 
				
			||||||
 | 
					               (self.offset, self.height / norm_value, rotation, mirror_x, self.dose), \
 | 
				
			||||||
               lambda: Text(string=self.string,
 | 
					               lambda: Text(string=self.string,
 | 
				
			||||||
                            height=self.height * norm_value,
 | 
					                            height=self.height * norm_value,
 | 
				
			||||||
                            font_path=self.font_path,
 | 
					                            font_path=self.font_path,
 | 
				
			||||||
                            mirrored=self.mirrored,
 | 
					                            rotation=rotation,
 | 
				
			||||||
 | 
					                            mirrored=(mirror_x, False),
 | 
				
			||||||
                            layer=self.layer)
 | 
					                            layer=self.layer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_bounds(self) -> numpy.ndarray:
 | 
					    def get_bounds(self) -> numpy.ndarray:
 | 
				
			||||||
        # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so
 | 
					        # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so
 | 
				
			||||||
        #  just convert to polygons instead
 | 
					        #  just convert to polygons instead
 | 
				
			||||||
        bounds = [[+inf, +inf], [-inf, -inf]]
 | 
					        bounds = numpy.array([[+inf, +inf], [-inf, -inf]])
 | 
				
			||||||
        polys = self.to_polygons()
 | 
					        polys = self.to_polygons()
 | 
				
			||||||
        for poly in polys:
 | 
					        for poly in polys:
 | 
				
			||||||
            poly_bounds = poly.get_bounds()
 | 
					            poly_bounds = poly.get_bounds()
 | 
				
			||||||
@ -140,7 +160,7 @@ class Text(Shape):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def get_char_as_polygons(font_path: str,
 | 
					def get_char_as_polygons(font_path: str,
 | 
				
			||||||
                         char: str,
 | 
					                         char: str,
 | 
				
			||||||
                         resolution: float=48*64,
 | 
					                         resolution: float = 48*64,
 | 
				
			||||||
                         ) -> Tuple[List[List[List[float]]], float]:
 | 
					                         ) -> Tuple[List[List[List[float]]], float]:
 | 
				
			||||||
    from freetype import Face
 | 
					    from freetype import Face
 | 
				
			||||||
    from matplotlib.path import Path
 | 
					    from matplotlib.path import Path
 | 
				
			||||||
@ -150,12 +170,15 @@ def get_char_as_polygons(font_path: str,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    The output is normalized so that the font size is 1 unit.
 | 
					    The output is normalized so that the font size is 1 unit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param font_path: File path specifying a font loadable by freetype
 | 
					    Args:
 | 
				
			||||||
    :param char: Character to convert to polygons
 | 
					        font_path: File path specifying a font loadable by freetype
 | 
				
			||||||
    :param resolution: Internal resolution setting (used for freetype
 | 
					        char: Character to convert to polygons
 | 
				
			||||||
            Face.set_font_size(resolution)). Modify at your own peril!
 | 
					        resolution: Internal resolution setting (used for freetype
 | 
				
			||||||
    :return: List of polygons [[[x0, y0], [x1, y1], ...], ...] and 'advance' distance (distance
 | 
					            `Face.set_font_size(resolution))`. Modify at your own peril!
 | 
				
			||||||
            from the start of this glyph to the start of the next one)
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        List of polygons `[[[x0, y0], [x1, y1], ...], ...]` and
 | 
				
			||||||
 | 
					        'advance' distance (distance from the start of this glyph to the start of the next one)
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if len(char) != 1:
 | 
					    if len(char) != 1:
 | 
				
			||||||
        raise Exception('get_char_as_polygons called with non-char')
 | 
					        raise Exception('get_char_as_polygons called with non-char')
 | 
				
			||||||
@ -175,7 +198,7 @@ def get_char_as_polygons(font_path: str,
 | 
				
			|||||||
        tags = outline.tags[start:end + 1]
 | 
					        tags = outline.tags[start:end + 1]
 | 
				
			||||||
        tags.append(tags[0])
 | 
					        tags.append(tags[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        segments = []
 | 
					        segments: List[List[List[float]]] = []
 | 
				
			||||||
        for j, point in enumerate(points):
 | 
					        for j, point in enumerate(points):
 | 
				
			||||||
            # If we already have a segment, add this point to it
 | 
					            # If we already have a segment, add this point to it
 | 
				
			||||||
            if j > 0:
 | 
					            if j > 0:
 | 
				
			||||||
@ -220,3 +243,20 @@ def get_char_as_polygons(font_path: str,
 | 
				
			|||||||
        polygons = path.to_polygons()
 | 
					        polygons = path.to_polygons()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return polygons, advance
 | 
					    return polygons, advance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self) -> 'Text':
 | 
				
			||||||
 | 
					        self.mirrored.flags.writeable = False
 | 
				
			||||||
 | 
					        Shape.lock(self)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self) -> 'Text':
 | 
				
			||||||
 | 
					        Shape.unlock(self)
 | 
				
			||||||
 | 
					        self.mirrored.flags.writeable = True
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        rotation = f' r°{self.rotation*180/pi:g}' if self.rotation != 0 else ''
 | 
				
			||||||
 | 
					        dose = f' d{self.dose:g}' if self.dose != 1 else ''
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
 | 
				
			||||||
 | 
					        return f'<TextShape "{self.string}" l{self.layer} o{self.offset} h{self.height:g}{rotation}{mirrored}{dose}{locked}>'
 | 
				
			||||||
 | 
				
			|||||||
@ -2,18 +2,21 @@
 | 
				
			|||||||
 SubPattern provides basic support for nesting Pattern objects within each other, by adding
 | 
					 SubPattern provides basic support for nesting Pattern objects within each other, by adding
 | 
				
			||||||
  offset, rotation, scaling, and other such properties to the reference.
 | 
					  offset, rotation, scaling, and other such properties to the reference.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					#TODO more top-level documentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from typing import Union, List
 | 
					from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any
 | 
				
			||||||
import copy
 | 
					import copy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
from numpy import pi
 | 
					from numpy import pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .error import PatternError
 | 
					from .error import PatternError, PatternLockedError
 | 
				
			||||||
from .utils import is_scalar, rotation_matrix_2d, vector2
 | 
					from .utils import is_scalar, rotation_matrix_2d, vector2
 | 
				
			||||||
 | 
					from .repetition import GridRepetition
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__author__ = 'Jan Petykiewicz'
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
 | 
					    from . import Pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SubPattern:
 | 
					class SubPattern:
 | 
				
			||||||
@ -21,21 +24,61 @@ class SubPattern:
 | 
				
			|||||||
    SubPattern provides basic support for nesting Pattern objects within each other, by adding
 | 
					    SubPattern provides basic support for nesting Pattern objects within each other, by adding
 | 
				
			||||||
     offset, rotation, scaling, and associated methods.
 | 
					     offset, rotation, scaling, and associated methods.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    __slots__ = ('_pattern',
 | 
				
			||||||
 | 
					                 '_offset',
 | 
				
			||||||
 | 
					                 '_rotation',
 | 
				
			||||||
 | 
					                 '_dose',
 | 
				
			||||||
 | 
					                 '_scale',
 | 
				
			||||||
 | 
					                 '_mirrored',
 | 
				
			||||||
 | 
					                 'identifier',
 | 
				
			||||||
 | 
					                 'locked')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pattern = None          # type: Pattern
 | 
					    _pattern: Optional['Pattern']
 | 
				
			||||||
    _offset = (0.0, 0.0)    # type: numpy.ndarray
 | 
					    """ The `Pattern` being instanced """
 | 
				
			||||||
    _rotation = 0.0         # type: float
 | 
					
 | 
				
			||||||
    _dose = 1.0             # type: float
 | 
					    _offset: numpy.ndarray
 | 
				
			||||||
    _scale = 1.0            # type: float
 | 
					    """ (x, y) offset for the instance """
 | 
				
			||||||
    _mirrored = None        # type: List[bool]
 | 
					
 | 
				
			||||||
 | 
					    _rotation: float
 | 
				
			||||||
 | 
					    """ rotation for the instance, radians counterclockwise """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _dose: float
 | 
				
			||||||
 | 
					    """ dose factor for the instance """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _scale: float
 | 
				
			||||||
 | 
					    """ scale factor for the instance """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _mirrored: numpy.ndarray        # ndarray[bool]
 | 
				
			||||||
 | 
					    """ Whether to mirror the instanc across the x and/or y axes. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    identifier: Tuple[Any, ...]
 | 
				
			||||||
 | 
					    """ Arbitrary identifier, used internally by some `masque` functions. """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    locked: bool
 | 
				
			||||||
 | 
					    """ If `True`, disallows changes to the GridRepetition """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self,
 | 
					    def __init__(self,
 | 
				
			||||||
                 pattern: 'Pattern',
 | 
					                 pattern: Optional['Pattern'],
 | 
				
			||||||
                 offset: vector2=(0.0, 0.0),
 | 
					                 offset: vector2 = (0.0, 0.0),
 | 
				
			||||||
                 rotation: float=0.0,
 | 
					                 rotation: float = 0.0,
 | 
				
			||||||
                 mirrored: List[bool]=None,
 | 
					                 mirrored: Optional[Sequence[bool]] = None,
 | 
				
			||||||
                 dose: float=1.0,
 | 
					                 dose: float = 1.0,
 | 
				
			||||||
                 scale: float=1.0):
 | 
					                 scale: float = 1.0,
 | 
				
			||||||
 | 
					                 locked: bool = False,
 | 
				
			||||||
 | 
					                 identifier: Tuple[Any, ...] = ()):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            pattern: Pattern to reference.
 | 
				
			||||||
 | 
					            offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc.
 | 
				
			||||||
 | 
					            rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0).
 | 
				
			||||||
 | 
					            mirrored: Whether to mirror the referenced pattern across its x and y axes.
 | 
				
			||||||
 | 
					            dose: Scaling factor applied to the dose.
 | 
				
			||||||
 | 
					            scale: Scaling factor applied to the pattern's geometry.
 | 
				
			||||||
 | 
					            locked: Whether the `SubPattern` is locked after initialization.
 | 
				
			||||||
 | 
					            identifier: Arbitrary tuple, used internally by some `masque` functions.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        self.identifier = identifier
 | 
				
			||||||
        self.pattern = pattern
 | 
					        self.pattern = pattern
 | 
				
			||||||
        self.offset = offset
 | 
					        self.offset = offset
 | 
				
			||||||
        self.rotation = rotation
 | 
					        self.rotation = rotation
 | 
				
			||||||
@ -44,6 +87,41 @@ class SubPattern:
 | 
				
			|||||||
        if mirrored is None:
 | 
					        if mirrored is None:
 | 
				
			||||||
            mirrored = [False, False]
 | 
					            mirrored = [False, False]
 | 
				
			||||||
        self.mirrored = mirrored
 | 
					        self.mirrored = mirrored
 | 
				
			||||||
 | 
					        self.locked = locked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __setattr__(self, name, value):
 | 
				
			||||||
 | 
					        if self.locked and name != 'locked':
 | 
				
			||||||
 | 
					            raise PatternLockedError()
 | 
				
			||||||
 | 
					        object.__setattr__(self, name, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __copy__(self) -> 'SubPattern':
 | 
				
			||||||
 | 
					        new = SubPattern(pattern=self.pattern,
 | 
				
			||||||
 | 
					                         offset=self.offset.copy(),
 | 
				
			||||||
 | 
					                         rotation=self.rotation,
 | 
				
			||||||
 | 
					                         dose=self.dose,
 | 
				
			||||||
 | 
					                         scale=self.scale,
 | 
				
			||||||
 | 
					                         mirrored=self.mirrored.copy(),
 | 
				
			||||||
 | 
					                         locked=self.locked)
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def  __deepcopy__(self, memo: Dict = None) -> 'SubPattern':
 | 
				
			||||||
 | 
					        memo = {} if memo is None else memo
 | 
				
			||||||
 | 
					        new = copy.copy(self).unlock()
 | 
				
			||||||
 | 
					        new.pattern = copy.deepcopy(self.pattern, memo)
 | 
				
			||||||
 | 
					        new.locked = self.locked
 | 
				
			||||||
 | 
					        return new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # pattern property
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def pattern(self) -> Optional['Pattern']:
 | 
				
			||||||
 | 
					        return self._pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @pattern.setter
 | 
				
			||||||
 | 
					    def pattern(self, val: Optional['Pattern']):
 | 
				
			||||||
 | 
					        from .pattern import Pattern
 | 
				
			||||||
 | 
					        if val is not None and not isinstance(val, Pattern):
 | 
				
			||||||
 | 
					            raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val))
 | 
				
			||||||
 | 
					        self._pattern = val
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # offset property
 | 
					    # offset property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
@ -98,22 +176,23 @@ class SubPattern:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Mirrored property
 | 
					    # Mirrored property
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def mirrored(self) -> List[bool]:
 | 
					    def mirrored(self) -> numpy.ndarray:        # ndarray[bool]
 | 
				
			||||||
        return self._mirrored
 | 
					        return self._mirrored
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @mirrored.setter
 | 
					    @mirrored.setter
 | 
				
			||||||
    def mirrored(self, val: List[bool]):
 | 
					    def mirrored(self, val: Sequence[bool]):
 | 
				
			||||||
        if is_scalar(val):
 | 
					        if is_scalar(val):
 | 
				
			||||||
            raise PatternError('Mirrored must be a 2-element list of booleans')
 | 
					            raise PatternError('Mirrored must be a 2-element list of booleans')
 | 
				
			||||||
        self._mirrored = val
 | 
					        self._mirrored = numpy.array(val, dtype=bool, copy=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def as_pattern(self) -> 'Pattern':
 | 
					    def as_pattern(self) -> 'Pattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns a copy of self.pattern which has been scaled, rotated, etc. according to this
 | 
					        Returns:
 | 
				
			||||||
         SubPattern's properties.
 | 
					            A copy of self.pattern which has been scaled, rotated, etc. according to this
 | 
				
			||||||
        :return: Copy of self.pattern that has been altered to reflect the SubPattern's properties.
 | 
					             `SubPattern`'s properties.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pattern = self.pattern.deepcopy()
 | 
					        assert(self.pattern is not None)
 | 
				
			||||||
 | 
					        pattern = self.pattern.deepcopy().deepunlock()
 | 
				
			||||||
        pattern.scale_by(self.scale)
 | 
					        pattern.scale_by(self.scale)
 | 
				
			||||||
        [pattern.mirror(ax) for ax, do in enumerate(self.mirrored) if do]
 | 
					        [pattern.mirror(ax) for ax, do in enumerate(self.mirrored) if do]
 | 
				
			||||||
        pattern.rotate_around((0.0, 0.0), self.rotation)
 | 
					        pattern.rotate_around((0.0, 0.0), self.rotation)
 | 
				
			||||||
@ -125,8 +204,11 @@ class SubPattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Translate by the given offset
 | 
					        Translate by the given offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param offset: Translate by this offset
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            offset: Offset `[x, y]` to translate by
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.offset += offset
 | 
					        self.offset += offset
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@ -135,9 +217,12 @@ class SubPattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate around a point
 | 
					        Rotate around a point
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param pivot: Point to rotate around
 | 
					        Args:
 | 
				
			||||||
        :param rotation: Angle to rotate by (counterclockwise, radians)
 | 
					            pivot: Point `[x, y]` to rotate around
 | 
				
			||||||
        :return: self
 | 
					            rotation: Angle to rotate by (counterclockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        pivot = numpy.array(pivot, dtype=float)
 | 
					        pivot = numpy.array(pivot, dtype=float)
 | 
				
			||||||
        self.translate(-pivot)
 | 
					        self.translate(-pivot)
 | 
				
			||||||
@ -148,10 +233,13 @@ class SubPattern:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def rotate(self, rotation: float) -> 'SubPattern':
 | 
					    def rotate(self, rotation: float) -> 'SubPattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Rotate around (0, 0)
 | 
					        Rotate the instance around it's origin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param rotation: Angle to rotate by (counterclockwise, radians)
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            rotation: Angle to rotate by (counterclockwise, radians)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.rotation += rotation
 | 
					        self.rotation += rotation
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@ -160,27 +248,38 @@ class SubPattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Mirror the subpattern across an axis.
 | 
					        Mirror the subpattern across an axis.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param axis: Axis to mirror across.
 | 
					        Args:
 | 
				
			||||||
        :return: self
 | 
					            axis: Axis to mirror across.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.mirrored[axis] = not self.mirrored[axis]
 | 
					        self.mirrored[axis] = not self.mirrored[axis]
 | 
				
			||||||
 | 
					        self.rotation *= -1
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_bounds(self) -> numpy.ndarray or None:
 | 
					    def get_bounds(self) -> Optional[numpy.ndarray]:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the
 | 
					        Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
 | 
				
			||||||
         extent of the SubPattern in each dimension.
 | 
					         extent of the `SubPattern` in each dimension.
 | 
				
			||||||
        Returns None if the contained Pattern is empty.
 | 
					        Returns `None` if the contained `Pattern` is empty.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: [[x_min, y_min], [x_max, y_max]] or None
 | 
					        Returns:
 | 
				
			||||||
 | 
					            `[[x_min, y_min], [x_max, y_max]]` or `None`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        if self.pattern is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
        return self.as_pattern().get_bounds()
 | 
					        return self.as_pattern().get_bounds()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def scale_by(self, c: float) -> 'SubPattern':
 | 
					    def scale_by(self, c: float) -> 'SubPattern':
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Scale the subpattern by a factor
 | 
					        Scale the subpattern by a factor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param c: scaling factor
 | 
					        Args:
 | 
				
			||||||
 | 
					            c: scaling factor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.scale *= c
 | 
					        self.scale *= c
 | 
				
			||||||
        return self
 | 
					        return self
 | 
				
			||||||
@ -189,7 +288,8 @@ class SubPattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a shallow copy of the subpattern.
 | 
					        Return a shallow copy of the subpattern.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: copy.copy(self)
 | 
					        Returns:
 | 
				
			||||||
 | 
					            `copy.copy(self)`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return copy.copy(self)
 | 
					        return copy.copy(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -197,6 +297,70 @@ class SubPattern:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a deep copy of the subpattern.
 | 
					        Return a deep copy of the subpattern.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: copy.copy(self)
 | 
					        Returns:
 | 
				
			||||||
 | 
					            `copy.deepcopy(self)`
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return copy.deepcopy(self)
 | 
					        return copy.deepcopy(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lock(self) -> 'SubPattern':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Lock the SubPattern, disallowing changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = False
 | 
				
			||||||
 | 
					        self.mirrored.flags.writeable = False
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', True)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def unlock(self) -> 'SubPattern':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Unlock the SubPattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.offset.flags.writeable = True
 | 
				
			||||||
 | 
					        self.mirrored.flags.writeable = True
 | 
				
			||||||
 | 
					        object.__setattr__(self, 'locked', False)
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deeplock(self) -> 'SubPattern':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Recursively lock the SubPattern and its contained pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        assert(self.pattern is not None)
 | 
				
			||||||
 | 
					        self.lock()
 | 
				
			||||||
 | 
					        self.pattern.deeplock()
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deepunlock(self) -> 'SubPattern':
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Recursively unlock the SubPattern and its contained pattern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This is dangerous unless you have just performed a deepcopy, since
 | 
				
			||||||
 | 
					        the subpattern and its components may be used in more than one once!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        assert(self.pattern is not None)
 | 
				
			||||||
 | 
					        self.unlock()
 | 
				
			||||||
 | 
					        self.pattern.deepunlock()
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        name = self.pattern.name if self.pattern is not None else None
 | 
				
			||||||
 | 
					        rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
 | 
				
			||||||
 | 
					        scale = f' d{self.scale:g}' if self.scale != 1 else ''
 | 
				
			||||||
 | 
					        mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
 | 
				
			||||||
 | 
					        dose = f' d{self.dose:g}' if self.dose != 1 else ''
 | 
				
			||||||
 | 
					        locked = ' L' if self.locked else ''
 | 
				
			||||||
 | 
					        return f'<SubPattern "{name}" at {self.offset}{rotation}{scale}{mirrored}{dose}{locked}>'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					subpattern_t = Union[SubPattern, GridRepetition]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										116
									
								
								masque/utils.py
									
									
									
									
									
								
							
							
						
						
									
										116
									
								
								masque/utils.py
									
									
									
									
									
								
							@ -2,42 +2,50 @@
 | 
				
			|||||||
Various helper functions
 | 
					Various helper functions
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from typing import Any, Union, Tuple
 | 
					from typing import Any, Union, Tuple, Sequence
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import numpy
 | 
					import numpy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Type definitions
 | 
					# Type definitions
 | 
				
			||||||
vector2 = Union[numpy.ndarray, Tuple[float, float]]
 | 
					vector2 = Union[numpy.ndarray, Tuple[float, float], Sequence[float]]
 | 
				
			||||||
 | 
					layer_t = Union[int, Tuple[int, int], str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def is_scalar(var: Any) -> bool:
 | 
					def is_scalar(var: Any) -> bool:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Alias for 'not hasattr(var, "__len__")'
 | 
					    Alias for 'not hasattr(var, "__len__")'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param var: Checks if var has a length.
 | 
					    Args:
 | 
				
			||||||
 | 
					        var: Checks if `var` has a length.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    return not hasattr(var, "__len__")
 | 
					    return not hasattr(var, "__len__")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_bit(bit_string: Any, bit_id: int) -> bool:
 | 
					def get_bit(bit_string: Any, bit_id: int) -> bool:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Returns true iff bit number 'bit_id' from the right of 'bit_string' is 1
 | 
					    Interprets bit number `bit_id` from the right (lsb) of `bit_string` as a boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param bit_string: Bit string to test
 | 
					    Args:
 | 
				
			||||||
    :param bit_id: Bit number, 0-indexed from the right (lsb)
 | 
					        bit_string: Bit string to test
 | 
				
			||||||
    :return: value of the requested bit (bool)
 | 
					        bit_id: Bit number, 0-indexed from the right (lsb)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        Boolean value of the requested bit
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    return bit_string & (1 << bit_id) != 0
 | 
					    return bit_string & (1 << bit_id) != 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def set_bit(bit_string: Any, bit_id: int, value: bool) -> Any:
 | 
					def set_bit(bit_string: Any, bit_id: int, value: bool) -> Any:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Returns 'bit_string' with bit number 'bit_id' set to 'value'.
 | 
					    Returns `bit_string`, with bit number `bit_id` set to boolean `value`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param bit_string: Bit string to alter
 | 
					    Args:
 | 
				
			||||||
    :param bit_id: Bit number, 0-indexed from right (lsb)
 | 
					        bit_string: Bit string to alter
 | 
				
			||||||
    :param value: Boolean value to set bit to
 | 
					        bit_id: Bit number, 0-indexed from right (lsb)
 | 
				
			||||||
    :return: Altered 'bit_string'
 | 
					        value: Boolean value to set bit to
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        Altered `bit_string`
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    mask = (1 << bit_id)
 | 
					    mask = (1 << bit_id)
 | 
				
			||||||
    bit_string &= ~mask
 | 
					    bit_string &= ~mask
 | 
				
			||||||
@ -50,40 +58,78 @@ def rotation_matrix_2d(theta: float) -> numpy.ndarray:
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    2D rotation matrix for rotating counterclockwise around the origin.
 | 
					    2D rotation matrix for rotating counterclockwise around the origin.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param theta: Angle to rotate, in radians
 | 
					    Args:
 | 
				
			||||||
    :return: rotation matrix
 | 
					        theta: Angle to rotate, in radians
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        rotation matrix
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    return numpy.array([[numpy.cos(theta), -numpy.sin(theta)],
 | 
					    return numpy.array([[numpy.cos(theta), -numpy.sin(theta)],
 | 
				
			||||||
                        [numpy.sin(theta), +numpy.cos(theta)]])
 | 
					                        [numpy.sin(theta), +numpy.cos(theta)]])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def normalize_mirror(mirrored: Sequence[bool]) -> Tuple[bool, float]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Converts 0-2 mirror operations `(mirror_across_x_axis, mirror_across_y_axis)`
 | 
				
			||||||
 | 
					    into 0-1 mirror operations and a rotation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        mirrored: `(mirror_across_x_axis, mirror_across_y_axis)`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        `mirror_across_x_axis` (bool) and
 | 
				
			||||||
 | 
					        `angle_to_rotate` in radians
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mirrored_x, mirrored_y = mirrored
 | 
				
			||||||
 | 
					    mirror_x = (mirrored_x != mirrored_y) #XOR
 | 
				
			||||||
 | 
					    angle = numpy.pi if mirrored_y else 0
 | 
				
			||||||
 | 
					    return mirror_x, angle
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def remove_duplicate_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray:
 | 
					def remove_duplicate_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray:
 | 
				
			||||||
        duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
 | 
					    """
 | 
				
			||||||
        if not closed_path:
 | 
					    Given a list of vertices, remove any consecutive duplicates.
 | 
				
			||||||
            duplicates[0] = False
 | 
					
 | 
				
			||||||
        return vertices[~duplicates]
 | 
					    Args:
 | 
				
			||||||
 | 
					        vertices: `[[x0, y0], [x1, y1], ...]`
 | 
				
			||||||
 | 
					        closed_path: If True, `vertices` is interpreted as an implicity-closed path
 | 
				
			||||||
 | 
					            (i.e. the last vertex will be removed if it is the same as the first)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        `vertices` with no consecutive duplicates.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
 | 
				
			||||||
 | 
					    if not closed_path:
 | 
				
			||||||
 | 
					        duplicates[0] = False
 | 
				
			||||||
 | 
					    return vertices[~duplicates]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray:
 | 
					def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray:
 | 
				
			||||||
        '''
 | 
					    """
 | 
				
			||||||
        Given a list of vertices, remove any superflous vertices (i.e.
 | 
					    Given a list of vertices, remove any superflous vertices (i.e.
 | 
				
			||||||
            those which lie along the line formed by their neighbors)
 | 
					        those which lie along the line formed by their neighbors)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param vertices: Nx2 ndarray of vertices
 | 
					    Args:
 | 
				
			||||||
        :param closed_path: If True, the vertices are assumed to represent an implicitly
 | 
					        vertices: Nx2 ndarray of vertices
 | 
				
			||||||
            closed path. If False, the path is assumed to be open. Default True.
 | 
					        closed_path: If `True`, the vertices are assumed to represent an implicitly
 | 
				
			||||||
        :return:
 | 
					           closed path. If `False`, the path is assumed to be open. Default `True`.
 | 
				
			||||||
        '''
 | 
					 | 
				
			||||||
        # Check for dx0/dy0 == dx1/dy1
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dv = numpy.roll(vertices, 1, axis=0) - vertices #[y0 - yn1, y1-y0, ...]
 | 
					    Returns:
 | 
				
			||||||
        dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] # [[dx1*dy0, dx1*dy0], ...]
 | 
					        `vertices` with colinear (superflous) vertices removed.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    vertices = numpy.array(vertices)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
 | 
					    # Check for dx0/dy0 == dx1/dy1
 | 
				
			||||||
        err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        slopes_equal = (dxdy_diff / err_mult) < 1e-15
 | 
					    dv = numpy.roll(vertices, -1, axis=0) - vertices #       [y1-y0, y2-y1, ...]
 | 
				
			||||||
        if not closed_path:
 | 
					    dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1]   #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]]
 | 
				
			||||||
            slopes_equal[[0, -1]] = False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return vertices[~slopes_equal]
 | 
					    dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
 | 
				
			||||||
 | 
					    err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    slopes_equal = (dxdy_diff / err_mult) < 1e-15
 | 
				
			||||||
 | 
					    if not closed_path:
 | 
				
			||||||
 | 
					        slopes_equal[[0, -1]] = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return vertices[~slopes_equal]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										9
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								setup.py
									
									
									
									
									
								
							@ -1,13 +1,15 @@
 | 
				
			|||||||
#!/usr/bin/env python3
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from setuptools import setup, find_packages
 | 
					from setuptools import setup, find_packages
 | 
				
			||||||
import masque
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
with open('README.md', 'r') as f:
 | 
					with open('README.md', 'r') as f:
 | 
				
			||||||
    long_description = f.read()
 | 
					    long_description = f.read()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					with open('masque/VERSION', 'r') as f:
 | 
				
			||||||
 | 
					    version = f.read().strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
setup(name='masque',
 | 
					setup(name='masque',
 | 
				
			||||||
      version=masque.version,
 | 
					      version=version,
 | 
				
			||||||
      description='Lithography mask library',
 | 
					      description='Lithography mask library',
 | 
				
			||||||
      long_description=long_description,
 | 
					      long_description=long_description,
 | 
				
			||||||
      long_description_content_type='text/markdown',
 | 
					      long_description_content_type='text/markdown',
 | 
				
			||||||
@ -15,6 +17,9 @@ setup(name='masque',
 | 
				
			|||||||
      author_email='anewusername@gmail.com',
 | 
					      author_email='anewusername@gmail.com',
 | 
				
			||||||
      url='https://mpxd.net/code/jan/masque',
 | 
					      url='https://mpxd.net/code/jan/masque',
 | 
				
			||||||
      packages=find_packages(),
 | 
					      packages=find_packages(),
 | 
				
			||||||
 | 
					      package_data={
 | 
				
			||||||
 | 
					          'masque': ['VERSION']
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      install_requires=[
 | 
					      install_requires=[
 | 
				
			||||||
            'numpy',
 | 
					            'numpy',
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user