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:
parent
f7a2edfe23
commit
c7f3e7ee52
@ -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)
|
||||
|
@ -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 = {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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'<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}>'
|
||||
|
@ -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}>'
|
||||
|
@ -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}>'
|
||||
|
@ -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}>'
|
||||
|
@ -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)}>'
|
||||
|
@ -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
|
||||
|
@ -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'<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}>'
|
||||
|
@ -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}>'
|
||||
|
@ -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
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user