diff --git a/README.md b/README.md index 140bd2c..070cee3 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/examples/phc.py b/examples/phc.py index f214a5a..1f829b0 100644 --- a/examples/phc.py +++ b/examples/phc.py @@ -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 = { diff --git a/examples/tutorial/devices.py b/examples/tutorial/devices.py index 5cae36e..762c17b 100644 --- a/examples/tutorial/devices.py +++ b/examples/tutorial/devices.py @@ -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) diff --git a/masque/__init__.py b/masque/__init__.py index e8f8d8d..c8460bd 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -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 diff --git a/masque/file/svg.py b/masque/file/svg.py index f3f0448..99b3d6c 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -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) diff --git a/masque/file/utils.py b/masque/file/utils.py index b475b47..c478bf8 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -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) diff --git a/masque/library.py b/masque/library.py index 08a982f..6990529 100644 --- a/masque/library.py +++ b/masque/library.py @@ -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. diff --git a/masque/pattern.py b/masque/pattern.py index bdf9360..156c436 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -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 diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 6554946..b33427e 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -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' a°{numpy.rad2deg(self.angles)}' rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' - dose = f' d{self.dose:g}' if self.dose != 1 else '' - return f'' + return f'' diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index a9beb19..7854044 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -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'' + return f'' diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 2a51e8e..5cd532e 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -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'' + return f'' diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 2539c5b..dafe523 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -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'' + return f'' diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 05df53b..20d5b47 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -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'' + return f'' diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index a36281e..9eae34d 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -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 diff --git a/masque/shapes/text.py b/masque/shapes/text.py index b820bc8..9fad29a 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -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' r°{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'' + return f'' diff --git a/masque/subpattern.py b/masque/subpattern.py index 6c18396..49168e1 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -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'' + return f'' diff --git a/masque/traits/__init__.py b/masque/traits/__init__.py index 8ae8a7c..996b313 100644 --- a/masque/traits/__init__.py +++ b/masque/traits/__init__.py @@ -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 diff --git a/masque/traits/doseable.py b/masque/traits/doseable.py deleted file mode 100644 index 989ea0b..0000000 --- a/masque/traits/doseable.py +++ /dev/null @@ -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