Remove support for dose

Since there isn't GDS/OASIS level support for dose, this can be mostly
handled by using arbitrary layers/dtypes directly. Dose scaling isn't
handled as nicely that way, but it corresponds more directly to what
gets written to file.
This commit is contained in:
Jan Petykiewicz 2023-01-18 18:14:33 -08:00 committed by jan
parent f7a2edfe23
commit c7f3e7ee52
18 changed files with 57 additions and 340 deletions

View File

@ -3,8 +3,8 @@
Masque is a Python module for designing lithography masks.
The general idea is to implement something resembling the GDSII file-format, but
with some vectorized element types (eg. circles, not just polygons), better support for
E-beam doses, and the ability to output to multiple formats.
with some vectorized element types (eg. circles, not just polygons) and the ability
to output to multiple formats.
- [Source repository](https://mpxd.net/code/jan/masque)
- [PyPI](https://pypi.org/project/masque)

View File

@ -32,14 +32,13 @@ def hole(layer: layer_t,
Pattern, named `'hole'`
"""
pat = Pattern('hole', shapes=[
Circle(radius=radius, offset=(0, 0), layer=layer, dose=1.0)
Circle(radius=radius, offset=(0, 0), layer=layer)
])
return pat
def perturbed_l3(lattice_constant: float,
hole: Pattern,
trench_dose: float = 1.0,
trench_layer: layer_t = (1, 0),
shifts_a: Sequence[float] = (0.15, 0, 0.075),
shifts_r: Sequence[float] = (1.0, 1.0, 1.0),
@ -53,7 +52,6 @@ def perturbed_l3(lattice_constant: float,
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
trench_dose: Dose for the trenches. Default 1.0. (Hole dose is 1.0.)
trench_layer: Layer for the trenches, default `(1, 0)`.
shifts_a: passed to `pcgen.l3_shift`; specifies lattice constant
(1 - multiplicative factor) for shifting holes adjacent to
@ -85,10 +83,8 @@ def perturbed_l3(lattice_constant: float,
trench_dx = max_xy[0] - min_xy[0]
pat.shapes += [
Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
layer=trench_layer, dose=trench_dose),
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
layer=trench_layer, dose=trench_dose),
Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width, layer=trench_layer),
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width, layer=trench_layer),
]
ports = {

View File

@ -36,7 +36,6 @@ def pat2dev(pat: Pattern) -> Device:
def perturbed_l3(
lattice_constant: float,
hole: Pattern,
trench_dose: float = 1.0,
trench_layer: layer_t = (1, 0),
shifts_a: Sequence[float] = (0.15, 0, 0.075),
shifts_r: Sequence[float] = (1.0, 1.0, 1.0),
@ -50,7 +49,6 @@ def perturbed_l3(
Args:
lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole
trench_dose: Dose for the trenches. Default 1.0. (Hole dose is 1.0.)
trench_layer: Layer for the trenches, default `(1, 0)`.
shifts_a: passed to `pcgen.l3_shift`; specifies lattice constant
(1 - multiplicative factor) for shifting holes adjacent to
@ -87,10 +85,8 @@ def perturbed_l3(
trench_dx = max_xy[0] - min_xy[0]
pat.shapes += [
Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
layer=trench_layer, dose=trench_dose),
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
layer=trench_layer, dose=trench_dose),
Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width, layer=trench_layer),
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width, layer=trench_layer),
]
# Ports are at outer extents of the device (with y=0)

View File

@ -3,8 +3,8 @@
masque is an attempt to make a relatively small library for designing lithography
masks. The general idea is to implement something resembling the GDSII and OASIS file-formats,
but with some additional vectorized element types (eg. ellipses, not just polygons), better
support for E-beam doses, and the ability to interface with multiple file formats.
but with some additional vectorized element types (eg. ellipses, not just polygons), and the
ability to interface with multiple file formats.
`Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape`
objects, a list of `Label` objects, and a list of references to other `Patterns` (using

View File

@ -26,8 +26,8 @@ def writefile(
Note that this function modifies the Pattern.
If `custom_attributes` is `True`, non-standard `pattern_layer` and `pattern_dose` attributes
are written to the relevant elements.
If `custom_attributes` is `True`, a non-standard `pattern_layer` attribute
is written to the relevant elements.
It is often a good idea to run `pattern.subpatternize()` on pattern prior to
calling this function, especially if calling `.polygonize()` will result in very
@ -39,8 +39,8 @@ def writefile(
Args:
pattern: Pattern to write to file. Modified by this function.
filename: Filename to write to.
custom_attributes: Whether to write non-standard `pattern_layer` and
`pattern_dose` attributes to the SVG elements.
custom_attributes: Whether to write non-standard `pattern_layer` attribute to the
SVG elements.
"""
pattern = library[top]
@ -61,8 +61,7 @@ def writefile(
svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string,
debug=(not custom_attributes))
# Now create a group for each row in sd_table (ie, each pattern + dose combination)
# and add in any Boundary and Use elements
# Now create a group for each pattern and add in any Boundary and Use elements
for name, pat in library.items():
svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red')
@ -73,7 +72,6 @@ def writefile(
path = svg.path(d=path_spec)
if custom_attributes:
path['pattern_layer'] = polygon.layer
path['pattern_dose'] = polygon.dose
svg_group.add(path)
@ -82,8 +80,6 @@ def writefile(
continue
transform = f'scale({subpat.scale:g}) rotate({subpat.rotation:g}) translate({subpat.offset[0]:g},{subpat.offset[1]:g})'
use = svg.use(href='#' + mangle_name(subpat.target), transform=transform)
if custom_attributes:
use['pattern_dose'] = subpat.dose
svg_group.add(use)
svg.defs.add(svg_group)

View File

@ -15,23 +15,18 @@ from ..shapes import Polygon, Path
logger = logging.getLogger(__name__)
def mangle_name(name: str, dose_multiplier: float = 1.0) -> str:
def mangle_name(name: str) -> str:
"""
Create a new name using `name` and the `dose_multiplier`.
Sanitize a name.
Args:
name: Name we want to mangle.
dose_multiplier: Dose multiplier to mangle with.
Returns:
Mangled name.
"""
if dose_multiplier == 1:
full_name = name
else:
full_name = f'{name}_dm{dose_multiplier}'
expression = re.compile(r'[^A-Za-z0-9_\?\$]')
sanitized_name = expression.sub('_', full_name)
sanitized_name = expression.sub('_', name)
return sanitized_name
@ -59,134 +54,6 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
return pat
def make_dose_table(
top_names: Iterable[str],
library: Mapping[str, Pattern],
dose_multiplier: float = 1.0,
) -> Set[Tuple[str, float]]:
"""
Create a set containing `(name, written_dose)` for each pattern (including subpatterns)
Args:
top_names: Names of all topcells
pattern: Source Patterns.
dose_multiplier: Multiplier for all written_dose entries.
Returns:
`{(name, written_dose), ...}`
"""
dose_table = {(top_name, dose_multiplier) for top_name in top_names}
for name, pattern in library.items():
for subpat in pattern.subpatterns:
if subpat.target is None:
continue
subpat_dose_entry = (subpat.target, subpat.dose * dose_multiplier)
if subpat_dose_entry not in dose_table:
subpat_dose_table = make_dose_table(subpat.target, library, subpat.dose * dose_multiplier)
dose_table = dose_table.union(subpat_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(
library: Mapping[str, 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).
"""
logger.warning('TODO: dose2dtype() needs to be tested!')
if not isinstance(library, Library):
library = WrapROLibrary(library)
# Get a table of (id(pat), written_dose) for each pattern and subpattern
sd_table = make_dose_table(library.find_toplevel(), library)
# 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 name, pat_dose in sd_table:
pat = library[name]
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_names = {} # {(old name, dose): new name} mapping
new_lib = {} # {new_name: new_pattern} mapping
for name, pat_dose in sd_table:
mangled_name = mangle_name(name, pat_dose)
new_names[(name, pat_dose)] = mangled_name
old_pat = library[name]
if pat_dose == 1:
new_lib[mangled_name] = old_pat
continue
pat = old_pat.deepcopy()
if len(mangled_name) == 0:
raise PatternError(f'Zero-length name after mangle, originally "{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_lib[mangled_name] = pat
return new_lib, dose_vals_list
def is_gzipped(path: pathlib.Path) -> bool:
with open(path, 'rb') as stream:
magic_bytes = stream.read(2)

View File

@ -409,7 +409,7 @@ class MutableLibrary(Library, metaclass=ABCMeta):
"""
Iterates through all `Pattern`s. Within each `Pattern`, it iterates
over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-,
offset-, dose-, and rotation-independent form. Each shape whose normalized form appears
offset-, and rotation-independent form. Each shape whose normalized form appears
more than once is removed and re-added using subpattern objects referencing a newly-created
`Pattern` containing only the normalized form of the shape.
@ -468,7 +468,7 @@ class MutableLibrary(Library, metaclass=ABCMeta):
for pat in tuple(self.values()):
# Store `[(index_in_shapes, values_from_normalized_form), ...]` for all shapes which
# are to be replaced.
# The `values` are `(offset, scale, rotation, dose)`.
# The `values` are `(offset, scale, rotation)`.
shape_table: MutableMapping[Tuple, List] = defaultdict(list)
for i, shape in enumerate(pat.shapes):
@ -489,9 +489,9 @@ class MutableLibrary(Library, metaclass=ABCMeta):
for label in shape_table:
target = label2name(label)
for i, values in shape_table[label]:
offset, scale, rotation, mirror_x, dose = values
offset, scale, rotation, mirror_x = values
pat.addsp(target=target, offset=offset, scale=scale,
rotation=rotation, dose=dose, mirrored=(mirror_x, False))
rotation=rotation, mirrored=(mirror_x, False))
shapes_to_remove.append(i)
# Remove any shapes for which we have created subpatterns.

View File

@ -415,20 +415,6 @@ class Pattern(AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
self.mirror_element_centers(axis)
return self
def scale_element_doses(self: P, c: float) -> P:
"""
Multiply all shape and subpattern doses by a factor
Args:
c: Factor to multiply doses by
Return:
self
"""
for entry in chain(self.shapes, self.subpatterns):
entry.dose *= c
return self
def copy(self: P) -> P:
"""
Return a copy of the Pattern, deep-copying shapes and copying subpattern

View File

@ -162,7 +162,6 @@ class Arc(Shape, metaclass=AutoSlots):
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
raw: bool = False,
@ -179,7 +178,6 @@ class Arc(Shape, metaclass=AutoSlots):
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
self._dose = dose
else:
self.radii = radii
self.angles = angles
@ -189,7 +187,6 @@ class Arc(Shape, metaclass=AutoSlots):
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
self.dose = dose
self.poly_num_points = poly_num_points
self.poly_max_arclen = poly_max_arclen
[self.mirror(a) for a, do in enumerate(mirrored) if do]
@ -256,7 +253,7 @@ class Arc(Shape, metaclass=AutoSlots):
ys = numpy.hstack((ys1, ys2))
xys = numpy.vstack((xs, ys)).T
poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation)
poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation)
return [poly]
def get_bounds(self) -> NDArray[numpy.float64]:
@ -368,7 +365,7 @@ class Arc(Shape, metaclass=AutoSlots):
width = self.width
return ((type(self), radii, angles, width / norm_value, self.layer),
(self.offset, scale / norm_value, rotation, False, self.dose),
(self.offset, scale / norm_value, rotation, False),
lambda: Arc(radii=radii * norm_value, angles=angles, width=width * norm_value, layer=self.layer))
def get_cap_edges(self) -> NDArray[numpy.float64]:
@ -425,5 +422,4 @@ class Arc(Shape, metaclass=AutoSlots):
def __repr__(self) -> str:
angles = f'{numpy.rad2deg(self.angles)}'
rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
dose = f' d{self.dose:g}' if self.dose != 1 else ''
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}{dose}>'
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'

View File

@ -50,7 +50,6 @@ class Circle(Shape, metaclass=AutoSlots):
poly_max_arclen: Optional[float] = None,
offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
raw: bool = False,
@ -62,14 +61,12 @@ class Circle(Shape, metaclass=AutoSlots):
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
self._dose = dose
else:
self.radius = radius
self.offset = offset
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
self.dose = dose
self.poly_num_points = poly_num_points
self.poly_max_arclen = poly_max_arclen
@ -105,7 +102,7 @@ class Circle(Shape, metaclass=AutoSlots):
ys = numpy.sin(thetas) * self.radius
xys = numpy.vstack((xs, ys)).T
return [Polygon(xys, offset=self.offset, dose=self.dose, layer=self.layer)]
return [Polygon(xys, offset=self.offset, layer=self.layer)]
def get_bounds(self) -> NDArray[numpy.float64]:
return numpy.vstack((self.offset - self.radius,
@ -126,9 +123,8 @@ class Circle(Shape, metaclass=AutoSlots):
rotation = 0.0
magnitude = self.radius / norm_value
return ((type(self), self.layer),
(self.offset, magnitude, rotation, False, self.dose),
(self.offset, magnitude, rotation, False),
lambda: Circle(radius=norm_value, layer=self.layer))
def __repr__(self) -> str:
dose = f' d{self.dose:g}' if self.dose != 1 else ''
return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}{dose}>'
return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}>'

View File

@ -97,7 +97,6 @@ class Ellipse(Shape, metaclass=AutoSlots):
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
raw: bool = False,
@ -111,7 +110,6 @@ class Ellipse(Shape, metaclass=AutoSlots):
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
self._dose = dose
else:
self.radii = radii
self.offset = offset
@ -119,7 +117,6 @@ class Ellipse(Shape, metaclass=AutoSlots):
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
self.dose = dose
[self.mirror(a) for a, do in enumerate(mirrored) if do]
self.poly_num_points = poly_num_points
self.poly_max_arclen = poly_max_arclen
@ -167,7 +164,7 @@ class Ellipse(Shape, metaclass=AutoSlots):
ys = r1 * sin_th
xys = numpy.vstack((xs, ys)).T
poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation)
poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation)
return [poly]
def get_bounds(self) -> NDArray[numpy.float64]:
@ -199,10 +196,9 @@ class Ellipse(Shape, metaclass=AutoSlots):
scale = self.radius_y
angle = (self.rotation + pi / 2) % pi
return ((type(self), radii, self.layer),
(self.offset, scale / norm_value, angle, False, self.dose),
(self.offset, scale / norm_value, angle, False),
lambda: Ellipse(radii=radii * norm_value, layer=self.layer))
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 ''
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}{dose}>'
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}>'

View File

@ -151,7 +151,6 @@ class Path(Shape, metaclass=AutoSlots):
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
raw: bool = False,
@ -167,7 +166,6 @@ class Path(Shape, metaclass=AutoSlots):
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
self._dose = dose
self._width = width
self._cap = cap
self._cap_extensions = cap_extensions
@ -177,7 +175,6 @@ class Path(Shape, metaclass=AutoSlots):
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
self.dose = dose
self.width = width
self.cap = cap
self.cap_extensions = cap_extensions
@ -204,7 +201,6 @@ class Path(Shape, metaclass=AutoSlots):
rotation: float = 0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
dose: float = 1.0,
) -> 'Path':
"""
Build a path by specifying the turn angles and travel distances
@ -225,7 +221,6 @@ class Path(Shape, metaclass=AutoSlots):
`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`
Returns:
The resulting Path object
@ -240,7 +235,7 @@ class Path(Shape, metaclass=AutoSlots):
return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions,
offset=offset, rotation=rotation, mirrored=mirrored,
layer=layer, dose=dose)
layer=layer)
def to_polygons(
self,
@ -255,7 +250,7 @@ class Path(Shape, metaclass=AutoSlots):
if self.width == 0:
verts = numpy.vstack((v, v[::-1]))
return [Polygon(offset=self.offset, vertices=verts, dose=self.dose, layer=self.layer)]
return [Polygon(offset=self.offset, vertices=verts, layer=self.layer)]
perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
@ -306,12 +301,12 @@ class Path(Shape, metaclass=AutoSlots):
o1.append(v[-1] - perp[-1])
verts = numpy.vstack((o0, o1[::-1]))
polys = [Polygon(offset=self.offset, vertices=verts, dose=self.dose, layer=self.layer)]
polys = [Polygon(offset=self.offset, vertices=verts, layer=self.layer)]
if self.cap == PathCap.Circle:
#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, layer=self.layer)
polys += circ.to_polygons(poly_num_points=poly_num_points, poly_max_arclen=poly_max_arclen)
return polys
@ -372,7 +367,7 @@ class Path(Shape, metaclass=AutoSlots):
width0 = self.width / norm_value
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer),
(offset, scale / norm_value, rotation, False, self.dose),
(offset, scale / norm_value, rotation, False),
lambda: Path(reordered_vertices * norm_value, width=self.width * norm_value,
cap=self.cap, layer=self.layer))
@ -419,5 +414,4 @@ class Path(Shape, metaclass=AutoSlots):
def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0)
dose = f' d{self.dose:g}' if self.dose != 1 else ''
return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}{dose}>'
return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'

View File

@ -79,7 +79,6 @@ class Polygon(Shape, metaclass=AutoSlots):
rotation: float = 0.0,
mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
raw: bool = False,
@ -92,14 +91,12 @@ class Polygon(Shape, metaclass=AutoSlots):
self._repetition = repetition
self._annotations = annotations if annotations is not None else {}
self._layer = layer
self._dose = dose
else:
self.vertices = vertices
self.offset = offset
self.repetition = repetition
self.annotations = annotations if annotations is not None else {}
self.layer = layer
self.dose = dose
self.rotate(rotation)
[self.mirror(a) for a, do in enumerate(mirrored) if do]
@ -118,7 +115,6 @@ class Polygon(Shape, metaclass=AutoSlots):
rotation: float = 0.0,
offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
) -> 'Polygon':
"""
@ -129,7 +125,6 @@ class Polygon(Shape, metaclass=AutoSlots):
rotation: Rotation counterclockwise, in radians
offset: Offset, default `(0, 0)`
layer: Layer, default `0`
dose: Dose, default `1.0`
repetition: `Repetition` object, default `None`
Returns:
@ -140,8 +135,7 @@ class Polygon(Shape, metaclass=AutoSlots):
[+1, +1],
[+1, -1]], dtype=float)
vertices = 0.5 * side_length * norm_square
poly = Polygon(vertices, offset=offset, layer=layer, dose=dose,
repetition=repetition)
poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition)
poly.rotate(rotation)
return poly
@ -153,7 +147,6 @@ class Polygon(Shape, metaclass=AutoSlots):
rotation: float = 0,
offset: ArrayLike = (0.0, 0.0),
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
) -> 'Polygon':
"""
@ -165,7 +158,6 @@ class Polygon(Shape, metaclass=AutoSlots):
rotation: Rotation counterclockwise, in radians
offset: Offset, default `(0, 0)`
layer: Layer, default `0`
dose: Dose, default `1.0`
repetition: `Repetition` object, default `None`
Returns:
@ -175,8 +167,7 @@ class Polygon(Shape, metaclass=AutoSlots):
[-lx, +ly],
[+lx, +ly],
[+lx, -ly]], dtype=float)
poly = Polygon(vertices, offset=offset, layer=layer, dose=dose,
repetition=repetition)
poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition)
poly.rotate(rotation)
return poly
@ -192,7 +183,6 @@ class Polygon(Shape, metaclass=AutoSlots):
ymax: Optional[float] = None,
ly: Optional[float] = None,
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
) -> 'Polygon':
"""
@ -211,7 +201,6 @@ class Polygon(Shape, metaclass=AutoSlots):
ymax: Maximum y coordinate
ly: Length along y direction
layer: Layer, default `0`
dose: Dose, default `1.0`
repetition: `Repetition` object, default `None`
Returns:
@ -277,8 +266,7 @@ class Polygon(Shape, metaclass=AutoSlots):
else:
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr),
layer=layer, dose=dose, repetition=repetition)
poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), layer=layer, repetition=repetition)
return poly
@staticmethod
@ -290,7 +278,6 @@ class Polygon(Shape, metaclass=AutoSlots):
center: ArrayLike = (0.0, 0.0),
rotation: float = 0.0,
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
) -> 'Polygon':
"""
@ -310,7 +297,6 @@ class Polygon(Shape, metaclass=AutoSlots):
`0` results in four axis-aligned sides (the long sides of the
irregular octagon).
layer: Layer, default `0`
dose: Dose, default `1.0`
repetition: `Repetition` object, default `None`
Returns:
@ -337,7 +323,7 @@ class Polygon(Shape, metaclass=AutoSlots):
side_length = 2 * inner_radius / s
vertices = 0.5 * side_length * norm_oct
poly = Polygon(vertices, offset=center, layer=layer, dose=dose, repetition=repetition)
poly = Polygon(vertices, offset=center, layer=layer, repetition=repetition)
poly.rotate(rotation)
return poly
@ -390,7 +376,7 @@ class Polygon(Shape, metaclass=AutoSlots):
# TODO: normalize mirroring?
return ((type(self), reordered_vertices.data.tobytes(), self.layer),
(offset, scale / norm_value, rotation, False, self.dose),
(offset, scale / norm_value, rotation, False),
lambda: Polygon(reordered_vertices * norm_value, layer=self.layer))
def clean_vertices(self) -> 'Polygon':
@ -425,5 +411,4 @@ class Polygon(Shape, metaclass=AutoSlots):
def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0)
dose = f' d{self.dose:g}' if self.dose != 1 else ''
return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}{dose}>'
return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}>'

View File

@ -5,8 +5,8 @@ import numpy
from numpy.typing import NDArray, ArrayLike
from ..traits import (
PositionableImpl, LayerableImpl, DoseableImpl,
Rotatable, Mirrorable, Copyable, Scalable,
PositionableImpl, LayerableImpl,
PivotableImpl, RepeatableImpl, AnnotatableImpl,
)
@ -27,7 +27,7 @@ DEFAULT_POLY_NUM_POINTS = 24
T = TypeVar('T', bound='Shape')
class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable,
class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Scalable,
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
"""
Abstract class specifying functions common to all shapes.
@ -68,7 +68,7 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
@abstractmethod
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, and rotation
information separated out from the remaining values.
Args:
@ -83,7 +83,7 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
`(intrinsic, extrinsic, constructor)`. These are further broken down as:
`intrinsic`: A tuple of basic types containing all information about the instance that
is not contained in 'extrinsic'. Usually, `intrinsic[0] == type(self)`.
`extrinsic`: `([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose)`
`extrinsic`: `([x_offset, y_offset], scale, rotation, mirror_across_x_axis)`
`constructor`: A callable (no arguments) which returns an instance of `type(self)` with
internal state equivalent to `intrinsic`.
"""
@ -195,12 +195,10 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
vertex_lists.append(vlist)
polygon_contours.append(numpy.vstack(vertex_lists))
manhattan_polygons = []
for contour in polygon_contours:
manhattan_polygons.append(Polygon(
vertices=contour,
layer=self.layer,
dose=self.dose))
manhattan_polygons = [
Polygon(vertices=contour, layer=self.layer)
for contour in polygon_contours
]
return manhattan_polygons
@ -298,6 +296,6 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable
manhattan_polygons.append(Polygon(
vertices=vertices,
layer=self.layer,
dose=self.dose))
))
return manhattan_polygons

View File

@ -70,7 +70,6 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
rotation: float = 0.0,
mirrored: ArrayLike = (False, False),
layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
raw: bool = False,
@ -80,7 +79,6 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
assert(isinstance(mirrored, numpy.ndarray))
self._offset = offset
self._layer = layer
self._dose = dose
self._string = string
self._height = height
self._rotation = rotation
@ -90,7 +88,6 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
else:
self.offset = offset
self.layer = layer
self.dose = dose
self.string = string
self.height = height
self.rotation = rotation
@ -119,7 +116,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
# Move these polygons to the right of the previous letter
for xys in raw_polys:
poly = Polygon(xys, dose=self.dose, layer=self.layer)
poly = Polygon(xys, layer=self.layer)
poly.mirror2d(self.mirrored)
poly.scale_by(self.height)
poly.offset = self.offset + [total_advance, 0]
@ -144,7 +141,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
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),
(self.offset, self.height / norm_value, rotation, mirror_x),
lambda: Text(string=self.string,
height=self.height * norm_value,
font_path=self.font_path,
@ -254,6 +251,5 @@ def get_char_as_polygons(
def __repr__(self) -> str:
rotation = f'{self.rotation*180/pi:g}' if self.rotation != 0 else ''
dose = f' d{self.dose:g}' if self.dose != 1 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}>'
return f'<TextShape "{self.string}" l{self.layer} o{self.offset} h{self.height:g}{rotation}{mirrored}>'

View File

@ -15,7 +15,7 @@ from .error import PatternError
from .utils import is_scalar, AutoSlots, annotations_t
from .repetition import Repetition
from .traits import (
PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
PositionableImpl, RotatableImpl, ScalableImpl,
Mirrorable, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
)
@ -27,7 +27,7 @@ if TYPE_CHECKING:
S = TypeVar('S', bound='SubPattern')
class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable,
class SubPattern(PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
metaclass=AutoSlots):
"""
@ -49,7 +49,6 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0,
mirrored: Optional[Sequence[bool]] = None,
dose: float = 1.0,
scale: float = 1.0,
repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None,
@ -60,14 +59,12 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
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.
repetition: `Repetition` object, default `None`
"""
self.target = target
self.offset = offset
self.rotation = rotation
self.dose = dose
self.scale = scale
if mirrored is None:
mirrored = (False, False)
@ -80,7 +77,6 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
target=self.target,
offset=self.offset.copy(),
rotation=self.rotation,
dose=self.dose,
scale=self.scale,
mirrored=self.mirrored.copy(),
repetition=copy.deepcopy(self.repetition),
@ -150,8 +146,6 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
pattern.rotate_around((0.0, 0.0), self.rotation)
if numpy.any(self.offset):
pattern.translate_elements(self.offset)
if self.dose != 1:
pattern.scale_element_doses(self.dose)
if self.repetition is not None:
combined = type(pattern)()
@ -204,5 +198,4 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
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 ''
return f'<SubPattern {name} at {self.offset}{rotation}{scale}{mirrored}{dose}>'
return f'<SubPattern {name} at {self.offset}{rotation}{scale}{mirrored}>'

View File

@ -3,7 +3,6 @@ Traits (mixins) and default implementations
"""
from .positionable import Positionable, PositionableImpl
from .layerable import Layerable, LayerableImpl
from .doseable import Doseable, DoseableImpl
from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl
from .repeatable import Repeatable, RepeatableImpl
from .scalable import Scalable, ScalableImpl

View File

@ -1,77 +0,0 @@
from typing import TypeVar
from abc import ABCMeta, abstractmethod
from ..error import MasqueError
T = TypeVar('T', bound='Doseable')
I = TypeVar('I', bound='DoseableImpl')
class Doseable(metaclass=ABCMeta):
"""
Abstract class for all doseable entities
"""
__slots__ = ()
'''
---- Properties
'''
@property
@abstractmethod
def dose(self) -> float:
"""
Dose (float >= 0)
"""
pass
# @dose.setter
# @abstractmethod
# def dose(self, val: float):
# pass
'''
---- Methods
'''
@abstractmethod
def set_dose(self: T, dose: float) -> T:
"""
Set the dose
Args:
dose: new value for dose
Returns:
self
"""
pass
class DoseableImpl(Doseable, metaclass=ABCMeta):
"""
Simple implementation of Doseable
"""
__slots__ = ()
_dose: float
""" Dose """
'''
---- Non-abstract properties
'''
@property
def dose(self) -> float:
return self._dose
@dose.setter
def dose(self, val: float) -> None:
if not val >= 0:
raise MasqueError('Dose must be non-negative')
self._dose = val
'''
---- Non-abstract methods
'''
def set_dose(self: I, dose: float) -> I:
self.dose = dose
return self