Merge branch 'master' of mpxd.net:jan/masque
This commit is contained in:
commit
0fd0e259aa
@ -9,8 +9,9 @@ E-beam doses, and the ability to output to multiple formats.
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
* python 3 (written and tested with 3.5)
|
* python >= 3.5 (written and tested with 3.6)
|
||||||
* numpy
|
* numpy
|
||||||
|
* pyclipper
|
||||||
* 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)
|
||||||
@ -27,5 +28,5 @@ pip install git+https://mpxd.net/gogs/jan/masque.git@release
|
|||||||
* Polygon de-embedding
|
* Polygon de-embedding
|
||||||
### Maybe
|
### Maybe
|
||||||
* Construct from bitmap
|
* Construct from bitmap
|
||||||
* Boolean operations on polygons (eg. using pyclipper)
|
* Boolean operations on polygons (using pyclipper)
|
||||||
* Output to OASIS
|
* Output to OASIS (using fatamorgana)
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
Dependencies:
|
Dependencies:
|
||||||
- numpy
|
- numpy
|
||||||
|
- pyclipper
|
||||||
- matplotlib [Pattern.visualize(...)]
|
- matplotlib [Pattern.visualize(...)]
|
||||||
- python-gdsii [masque.file.gdsii]
|
- python-gdsii [masque.file.gdsii]
|
||||||
- svgwrite [masque.file.svgwrite]
|
- svgwrite [masque.file.svgwrite]
|
||||||
|
@ -2,6 +2,8 @@ from typing import List
|
|||||||
import copy
|
import copy
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
import pyclipper
|
||||||
|
from pyclipper import scale_to_clipper, scale_from_clipper
|
||||||
|
|
||||||
from . import Shape, normalized_shape_tuple
|
from . import Shape, normalized_shape_tuple
|
||||||
from .. import PatternError
|
from .. import PatternError
|
||||||
@ -173,75 +175,17 @@ class Polygon(Shape):
|
|||||||
(offset, scale/norm_value, rotation, self.dose), \
|
(offset, scale/norm_value, rotation, self.dose), \
|
||||||
lambda: Polygon(reordered_vertices*norm_value, layer=self.layer)
|
lambda: Polygon(reordered_vertices*norm_value, layer=self.layer)
|
||||||
|
|
||||||
def cut(self,
|
def clean_vertices(self) -> 'Polygon':
|
||||||
cut_xs: numpy.ndarray = None,
|
|
||||||
cut_ys: numpy.ndarray = None
|
|
||||||
) -> List['Polygon']:
|
|
||||||
"""
|
"""
|
||||||
Decomposes the polygon into a list of constituents by cutting along the
|
Removes duplicate, co-linear and otherwise redundant vertices.
|
||||||
specified x and/or y coordinates.
|
|
||||||
|
|
||||||
:param cut_xs: list of x-coordinates to cut along (e.g., [1, 1.4, 6])
|
:returns: self
|
||||||
:param cut_ys: list of y-coordinates to cut along (e.g., [1, 3, 5.4])
|
|
||||||
:return: List of Polygon objects
|
|
||||||
"""
|
"""
|
||||||
import float_raster
|
self.vertices = scale_from_clipper(
|
||||||
xy_complex = self.vertices[:, 0] + 1j * self.vertices[:, 1]
|
pyclipper.CleanPolygon(
|
||||||
xy_cleaned = _clean_complex_vertices(xy_complex)
|
scale_to_clipper(
|
||||||
xy = numpy.vstack((numpy.real(xy_cleaned)[None, :],
|
self.vertices
|
||||||
numpy.imag(xy_cleaned)[None, :]))
|
)))
|
||||||
|
return self
|
||||||
if cut_xs is None:
|
|
||||||
cut_xs = tuple()
|
|
||||||
if cut_ys is None:
|
|
||||||
cut_ys = tuple()
|
|
||||||
|
|
||||||
mins, maxs = self.get_bounds()
|
|
||||||
dx, dy = maxs - mins
|
|
||||||
|
|
||||||
cx = numpy.hstack((min(tuple(cut_xs) + (mins[0],)) - dx, cut_xs, max((maxs[0],) + tuple(cut_xs)) + dx))
|
|
||||||
cy = numpy.hstack((min(tuple(cut_ys) + (mins[1],)) - dy, cut_ys, max((maxs[1],) + tuple(cut_ys)) + dy))
|
|
||||||
|
|
||||||
all_verts = float_raster.create_vertices(xy, cx, cy)
|
|
||||||
|
|
||||||
polygons = []
|
|
||||||
for cx_min, cx_max in zip(cx, cx[1:]):
|
|
||||||
for cy_min, cy_max in zip(cy, cy[1:]):
|
|
||||||
clipped_verts = (numpy.real(all_verts).clip(cx_min, cx_max) + 1j *
|
|
||||||
numpy.imag(all_verts).clip(cy_min, cy_max))
|
|
||||||
|
|
||||||
cleaned_verts = _clean_complex_vertices(clipped_verts)
|
|
||||||
if len(cleaned_verts) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
final_verts = numpy.hstack((numpy.real(cleaned_verts)[:, None],
|
|
||||||
numpy.imag(cleaned_verts)[:, None]))
|
|
||||||
polygons.append(Polygon(
|
|
||||||
vertices=final_verts,
|
|
||||||
layer=self.layer,
|
|
||||||
dose=self.dose))
|
|
||||||
return polygons
|
|
||||||
|
|
||||||
|
|
||||||
def _clean_complex_vertices(vertices: numpy.ndarray) -> numpy.ndarray:
|
|
||||||
eps = numpy.finfo(vertices.dtype).eps
|
|
||||||
|
|
||||||
def cleanup(v):
|
|
||||||
# Remove duplicate points
|
|
||||||
dv = v - numpy.roll(v, 1)
|
|
||||||
v = v[numpy.abs(dv) > eps]
|
|
||||||
|
|
||||||
# Remove colinear points
|
|
||||||
dv = v - numpy.roll(v, 1)
|
|
||||||
m = numpy.angle(dv) % pi
|
|
||||||
diff_m = m - numpy.roll(m, -1)
|
|
||||||
return v[numpy.abs(diff_m) > eps]
|
|
||||||
|
|
||||||
n = len(vertices)
|
|
||||||
cleaned = cleanup(vertices)
|
|
||||||
while n != len(cleaned):
|
|
||||||
n = len(cleaned)
|
|
||||||
cleaned = cleanup(cleaned)
|
|
||||||
|
|
||||||
return cleaned
|
|
||||||
|
|
||||||
|
@ -3,6 +3,9 @@ from abc import ABCMeta, abstractmethod
|
|||||||
import copy
|
import copy
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
import pyclipper
|
||||||
|
from pyclipper import scale_to_clipper, scale_from_clipper
|
||||||
|
|
||||||
from .. import PatternError
|
from .. import PatternError
|
||||||
from ..utils import is_scalar, rotation_matrix_2d, vector2
|
from ..utils import is_scalar, rotation_matrix_2d, vector2
|
||||||
|
|
||||||
@ -249,7 +252,7 @@ class Shape(metaclass=ABCMeta):
|
|||||||
numpy.where(keep_y)[0][0])
|
numpy.where(keep_y)[0][0])
|
||||||
|
|
||||||
rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy)
|
rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy)
|
||||||
binary_rastered = (rastered >= 0.5)
|
binary_rastered = (numpy.abs(rastered) >= 0.5)
|
||||||
supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1)
|
supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1)
|
||||||
|
|
||||||
contours = skimage.measure.find_contours(supersampled, 0.5)
|
contours = skimage.measure.find_contours(supersampled, 0.5)
|
||||||
@ -271,3 +274,49 @@ class Shape(metaclass=ABCMeta):
|
|||||||
|
|
||||||
return manhattan_polygons
|
return manhattan_polygons
|
||||||
|
|
||||||
|
def cut(self,
|
||||||
|
cut_xs: numpy.ndarray = None,
|
||||||
|
cut_ys: numpy.ndarray = None
|
||||||
|
) -> List['Polygon']:
|
||||||
|
"""
|
||||||
|
Decomposes the shape into a list of constituent polygons by polygonizing and
|
||||||
|
then cutting along the specified x and/or y coordinates.
|
||||||
|
|
||||||
|
:param cut_xs: list of x-coordinates to cut along (e.g., [1, 1.4, 6])
|
||||||
|
:param cut_ys: list of y-coordinates to cut along (e.g., [1, 3, 5.4])
|
||||||
|
:return: List of Polygon objects
|
||||||
|
"""
|
||||||
|
from . import Polygon
|
||||||
|
|
||||||
|
clipped_shapes = []
|
||||||
|
for polygon in self.to_polygons():
|
||||||
|
min_x, min_y = numpy.min(polygon.vertices, axis=0)
|
||||||
|
max_x, max_y = numpy.max(polygon.vertices, axis=0)
|
||||||
|
range_x = max_x - min_x
|
||||||
|
range_y = max_y - min_y
|
||||||
|
|
||||||
|
edge_xs = (min_x - range_x - 1,) + tuple(cut_xs) + (max_x + range_x + 1,)
|
||||||
|
edge_ys = (min_y - range_y - 1,) + tuple(cut_ys) + (max_y + range_y + 1,)
|
||||||
|
|
||||||
|
for i in range(2):
|
||||||
|
for j in range(2):
|
||||||
|
clipper = pyclipper.Pyclipper()
|
||||||
|
clipper.AddPath(scale_to_clipper(polygon.vertices), pyclipper.PT_SUBJECT, True)
|
||||||
|
|
||||||
|
for start_x, stop_x in zip(edge_xs[i::2], edge_xs[(i+1)::2]):
|
||||||
|
for start_y, stop_y in zip(edge_ys[j::2], edge_ys[(j+1)::2]):
|
||||||
|
clipper.AddPath(scale_to_clipper((
|
||||||
|
(start_x, start_y),
|
||||||
|
(start_x, stop_y),
|
||||||
|
(stop_x, stop_y),
|
||||||
|
(stop_x, start_y),
|
||||||
|
)), pyclipper.PT_CLIP, True)
|
||||||
|
|
||||||
|
clipped_parts = scale_from_clipper(clipper.Execute(pyclipper.CT_INTERSECTION,
|
||||||
|
pyclipper.PFT_EVENODD,
|
||||||
|
pyclipper.PFT_EVENODD))
|
||||||
|
for part in clipped_parts:
|
||||||
|
poly = polygon.copy()
|
||||||
|
poly.vertices = part
|
||||||
|
clipped_shapes.append(poly)
|
||||||
|
return clipped_shapes
|
||||||
|
@ -156,4 +156,3 @@ class SubPattern:
|
|||||||
"""
|
"""
|
||||||
self.scale *= c
|
self.scale *= c
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
7
setup.py
7
setup.py
@ -3,20 +3,21 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(name='masque',
|
setup(name='masque',
|
||||||
version='0.2',
|
version='0.3',
|
||||||
description='Lithography mask library',
|
description='Lithography mask library',
|
||||||
author='Jan Petykiewicz',
|
author='Jan Petykiewicz',
|
||||||
author_email='anewusername@gmail.com',
|
author_email='anewusername@gmail.com',
|
||||||
url='https://mpxd.net/gogs/jan/masque',
|
url='https://mpxd.net/gogs/jan/masque',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'numpy'
|
'numpy',
|
||||||
|
'pyclipper',
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'visualization': ['matplotlib'],
|
'visualization': ['matplotlib'],
|
||||||
'gdsii': ['python-gdsii'],
|
'gdsii': ['python-gdsii'],
|
||||||
'svg': ['svgwrite'],
|
'svg': ['svgwrite'],
|
||||||
'text': ['freetype-py', 'matplotlib']
|
'text': ['freetype-py', 'matplotlib'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user