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

View File

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

View File

@ -36,7 +36,6 @@ def pat2dev(pat: Pattern) -> Device:
def perturbed_l3( def perturbed_l3(
lattice_constant: float, lattice_constant: float,
hole: Pattern, hole: Pattern,
trench_dose: float = 1.0,
trench_layer: layer_t = (1, 0), trench_layer: layer_t = (1, 0),
shifts_a: Sequence[float] = (0.15, 0, 0.075), shifts_a: Sequence[float] = (0.15, 0, 0.075),
shifts_r: Sequence[float] = (1.0, 1.0, 1.0), shifts_r: Sequence[float] = (1.0, 1.0, 1.0),
@ -50,7 +49,6 @@ def perturbed_l3(
Args: Args:
lattice_constant: Distance between nearest neighbor holes lattice_constant: Distance between nearest neighbor holes
hole: `Pattern` object containing a single hole 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)`. trench_layer: Layer for the trenches, default `(1, 0)`.
shifts_a: passed to `pcgen.l3_shift`; specifies lattice constant shifts_a: passed to `pcgen.l3_shift`; specifies lattice constant
(1 - multiplicative factor) for shifting holes adjacent to (1 - multiplicative factor) for shifting holes adjacent to
@ -87,10 +85,8 @@ def perturbed_l3(
trench_dx = max_xy[0] - min_xy[0] trench_dx = max_xy[0] - min_xy[0]
pat.shapes += [ pat.shapes += [
Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width, Polygon.rect(ymin=max_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width, layer=trench_layer),
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),
Polygon.rect(ymax=min_xy[1], xmin=min_xy[0], lx=trench_dx, ly=trench_width,
layer=trench_layer, dose=trench_dose),
] ]
# Ports are at outer extents of the device (with y=0) # 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 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, 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 but with some additional vectorized element types (eg. ellipses, not just polygons), and the
support for E-beam doses, and the ability to interface with multiple file formats. ability to interface with multiple file 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, a list of `Label` objects, and a list of references to other `Patterns` (using 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. 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`, a non-standard `pattern_layer` attribute
are written to the relevant elements. is 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
@ -39,8 +39,8 @@ def writefile(
Args: Args:
pattern: Pattern to write to file. Modified by this function. pattern: Pattern to write to file. Modified by this function.
filename: Filename to write to. filename: Filename to write to.
custom_attributes: Whether to write non-standard `pattern_layer` and custom_attributes: Whether to write non-standard `pattern_layer` attribute to the
`pattern_dose` attributes to the SVG elements. SVG elements.
""" """
pattern = library[top] pattern = library[top]
@ -61,8 +61,7 @@ def writefile(
svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string, svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string,
debug=(not custom_attributes)) debug=(not custom_attributes))
# Now create a group for each row in sd_table (ie, each pattern + dose combination) # Now create a group for each pattern and add in any Boundary and Use elements
# and add in any Boundary and Use elements
for name, pat in library.items(): for name, pat in library.items():
svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red') svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red')
@ -73,7 +72,6 @@ def writefile(
path = svg.path(d=path_spec) path = svg.path(d=path_spec)
if custom_attributes: if custom_attributes:
path['pattern_layer'] = polygon.layer path['pattern_layer'] = polygon.layer
path['pattern_dose'] = polygon.dose
svg_group.add(path) svg_group.add(path)
@ -82,8 +80,6 @@ def writefile(
continue continue
transform = f'scale({subpat.scale:g}) rotate({subpat.rotation:g}) translate({subpat.offset[0]:g},{subpat.offset[1]:g})' 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) use = svg.use(href='#' + mangle_name(subpat.target), transform=transform)
if custom_attributes:
use['pattern_dose'] = subpat.dose
svg_group.add(use) svg_group.add(use)
svg.defs.add(svg_group) svg.defs.add(svg_group)

View File

@ -15,23 +15,18 @@ from ..shapes import Polygon, Path
logger = logging.getLogger(__name__) 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: Args:
name: Name we want to mangle. name: Name we want to mangle.
dose_multiplier: Dose multiplier to mangle with.
Returns: Returns:
Mangled name. 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_\?\$]') expression = re.compile(r'[^A-Za-z0-9_\?\$]')
sanitized_name = expression.sub('_', full_name) sanitized_name = expression.sub('_', name)
return sanitized_name return sanitized_name
@ -59,134 +54,6 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
return pat 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: def is_gzipped(path: pathlib.Path) -> bool:
with open(path, 'rb') as stream: with open(path, 'rb') as stream:
magic_bytes = stream.read(2) 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 Iterates through all `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-, 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.
@ -468,7 +468,7 @@ class MutableLibrary(Library, metaclass=ABCMeta):
for pat in tuple(self.values()): for pat in tuple(self.values()):
# Store `[(index_in_shapes, values_from_normalized_form), ...]` for all shapes which # Store `[(index_in_shapes, values_from_normalized_form), ...]` for all shapes which
# are to be replaced. # are to be replaced.
# The `values` are `(offset, scale, rotation, dose)`. # The `values` are `(offset, scale, rotation)`.
shape_table: MutableMapping[Tuple, List] = defaultdict(list) shape_table: MutableMapping[Tuple, List] = defaultdict(list)
for i, shape in enumerate(pat.shapes): for i, shape in enumerate(pat.shapes):
@ -489,9 +489,9 @@ class MutableLibrary(Library, metaclass=ABCMeta):
for label in shape_table: for label in shape_table:
target = label2name(label) target = label2name(label)
for i, values in shape_table[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, 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) shapes_to_remove.append(i)
# Remove any shapes for which we have created subpatterns. # 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) self.mirror_element_centers(axis)
return self 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: def copy(self: P) -> P:
""" """
Return a copy of the Pattern, deep-copying shapes and copying subpattern 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, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
raw: bool = False, raw: bool = False,
@ -179,7 +178,6 @@ class Arc(Shape, metaclass=AutoSlots):
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {} self._annotations = annotations if annotations is not None else {}
self._layer = layer self._layer = layer
self._dose = dose
else: else:
self.radii = radii self.radii = radii
self.angles = angles self.angles = angles
@ -189,7 +187,6 @@ class Arc(Shape, metaclass=AutoSlots):
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
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.mirror(a) for a, do in enumerate(mirrored) if do] [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)) ys = numpy.hstack((ys1, ys2))
xys = numpy.vstack((xs, ys)).T 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] return [poly]
def get_bounds(self) -> NDArray[numpy.float64]: def get_bounds(self) -> NDArray[numpy.float64]:
@ -368,7 +365,7 @@ class Arc(Shape, metaclass=AutoSlots):
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, 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)) lambda: Arc(radii=radii * norm_value, angles=angles, width=width * norm_value, layer=self.layer))
def get_cap_edges(self) -> NDArray[numpy.float64]: def get_cap_edges(self) -> NDArray[numpy.float64]:
@ -425,5 +422,4 @@ class Arc(Shape, metaclass=AutoSlots):
def __repr__(self) -> str: def __repr__(self) -> str:
angles = f'{numpy.rad2deg(self.angles)}' angles = f'{numpy.rad2deg(self.angles)}'
rotation = f'{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else '' 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}>'
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}{dose}>'

View File

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

View File

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

View File

@ -151,7 +151,6 @@ class Path(Shape, metaclass=AutoSlots):
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
raw: bool = False, raw: bool = False,
@ -167,7 +166,6 @@ class Path(Shape, metaclass=AutoSlots):
self._repetition = repetition self._repetition = repetition
self._annotations = annotations if annotations is not None else {} self._annotations = annotations if annotations is not None else {}
self._layer = layer self._layer = layer
self._dose = dose
self._width = width self._width = width
self._cap = cap self._cap = cap
self._cap_extensions = cap_extensions self._cap_extensions = cap_extensions
@ -177,7 +175,6 @@ class Path(Shape, metaclass=AutoSlots):
self.repetition = repetition self.repetition = repetition
self.annotations = annotations if annotations is not None else {} self.annotations = annotations if annotations is not None else {}
self.layer = layer self.layer = layer
self.dose = dose
self.width = width self.width = width
self.cap = cap self.cap = cap
self.cap_extensions = cap_extensions self.cap_extensions = cap_extensions
@ -204,7 +201,6 @@ class Path(Shape, metaclass=AutoSlots):
rotation: float = 0, rotation: float = 0,
mirrored: Sequence[bool] = (False, False), mirrored: Sequence[bool] = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0,
) -> 'Path': ) -> 'Path':
""" """
Build a path by specifying the turn angles and travel distances 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, `mirrored=(True, False)` results in a reflection across the x-axis,
multiplying the path's y-coordinates by -1. Default `(False, False)` multiplying the path's y-coordinates by -1. Default `(False, False)`
layer: Layer, default `0` layer: Layer, default `0`
dose: Dose, default `1.0`
Returns: Returns:
The resulting Path object 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, 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)
def to_polygons( def to_polygons(
self, self,
@ -255,7 +250,7 @@ class Path(Shape, metaclass=AutoSlots):
if self.width == 0: if self.width == 0:
verts = numpy.vstack((v, v[::-1])) 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 perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
@ -306,12 +301,12 @@ class Path(Shape, metaclass=AutoSlots):
o1.append(v[-1] - perp[-1]) o1.append(v[-1] - perp[-1])
verts = numpy.vstack((o0, o1[::-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: if self.cap == PathCap.Circle:
#for vert in v: # not sure if every vertex, or just ends? #for vert in v: # not sure if every vertex, or just ends?
for vert in [v[0], v[-1]]: 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) polys += circ.to_polygons(poly_num_points=poly_num_points, poly_max_arclen=poly_max_arclen)
return polys return polys
@ -372,7 +367,7 @@ class Path(Shape, metaclass=AutoSlots):
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, False, self.dose), (offset, scale / norm_value, rotation, False),
lambda: Path(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))
@ -419,5 +414,4 @@ class Path(Shape, metaclass=AutoSlots):
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.offset + self.vertices.mean(axis=0) 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}>'
return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}{dose}>'

View File

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

View File

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

View File

@ -70,7 +70,6 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
rotation: float = 0.0, rotation: float = 0.0,
mirrored: ArrayLike = (False, False), mirrored: ArrayLike = (False, False),
layer: layer_t = 0, layer: layer_t = 0,
dose: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = None, annotations: Optional[annotations_t] = None,
raw: bool = False, raw: bool = False,
@ -80,7 +79,6 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
assert(isinstance(mirrored, numpy.ndarray)) assert(isinstance(mirrored, numpy.ndarray))
self._offset = offset self._offset = offset
self._layer = layer self._layer = layer
self._dose = dose
self._string = string self._string = string
self._height = height self._height = height
self._rotation = rotation self._rotation = rotation
@ -90,7 +88,6 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
else: else:
self.offset = offset self.offset = offset
self.layer = layer self.layer = layer
self.dose = dose
self.string = string self.string = string
self.height = height self.height = height
self.rotation = rotation self.rotation = rotation
@ -119,7 +116,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
# Move these polygons to the right of the previous letter # Move these polygons to the right of the previous letter
for xys in raw_polys: 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.mirror2d(self.mirrored)
poly.scale_by(self.height) poly.scale_by(self.height)
poly.offset = self.offset + [total_advance, 0] poly.offset = self.offset + [total_advance, 0]
@ -144,7 +141,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots):
rotation += self.rotation rotation += self.rotation
rotation %= 2 * pi rotation %= 2 * pi
return ((type(self), self.string, self.font_path, self.layer), 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, 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,
@ -254,6 +251,5 @@ def get_char_as_polygons(
def __repr__(self) -> str: def __repr__(self) -> str:
rotation = f'{self.rotation*180/pi:g}' if self.rotation != 0 else '' 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 '' 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 .utils import is_scalar, AutoSlots, annotations_t
from .repetition import Repetition from .repetition import Repetition
from .traits import ( from .traits import (
PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, PositionableImpl, RotatableImpl, ScalableImpl,
Mirrorable, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl, Mirrorable, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
) )
@ -27,7 +27,7 @@ if TYPE_CHECKING:
S = TypeVar('S', bound='SubPattern') S = TypeVar('S', bound='SubPattern')
class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable, class SubPattern(PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
metaclass=AutoSlots): metaclass=AutoSlots):
""" """
@ -49,7 +49,6 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
offset: ArrayLike = (0.0, 0.0), offset: ArrayLike = (0.0, 0.0),
rotation: float = 0.0, rotation: float = 0.0,
mirrored: Optional[Sequence[bool]] = None, mirrored: Optional[Sequence[bool]] = None,
dose: float = 1.0,
scale: float = 1.0, scale: float = 1.0,
repetition: Optional[Repetition] = None, repetition: Optional[Repetition] = None,
annotations: Optional[annotations_t] = 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. 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). 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. 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. scale: Scaling factor applied to the pattern's geometry.
repetition: `Repetition` object, default `None` repetition: `Repetition` object, default `None`
""" """
self.target = target self.target = target
self.offset = offset self.offset = offset
self.rotation = rotation self.rotation = rotation
self.dose = dose
self.scale = scale self.scale = scale
if mirrored is None: if mirrored is None:
mirrored = (False, False) mirrored = (False, False)
@ -80,7 +77,6 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi
target=self.target, target=self.target,
offset=self.offset.copy(), offset=self.offset.copy(),
rotation=self.rotation, rotation=self.rotation,
dose=self.dose,
scale=self.scale, scale=self.scale,
mirrored=self.mirrored.copy(), mirrored=self.mirrored.copy(),
repetition=copy.deepcopy(self.repetition), 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) pattern.rotate_around((0.0, 0.0), self.rotation)
if numpy.any(self.offset): if numpy.any(self.offset):
pattern.translate_elements(self.offset) pattern.translate_elements(self.offset)
if self.dose != 1:
pattern.scale_element_doses(self.dose)
if self.repetition is not None: if self.repetition is not None:
combined = type(pattern)() 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 '' rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
scale = f' d{self.scale:g}' if self.scale != 1 else '' scale = f' d{self.scale:g}' if self.scale != 1 else ''
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() 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}>'
return f'<SubPattern {name} at {self.offset}{rotation}{scale}{mirrored}{dose}>'

View File

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