Source code for pdkmaster.technology.geometry

# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-or-later OR CERN-OHL-S-2.0+ OR Apache-2.0
"""The pdkmaster.design.geometry module provides classes to represent shapes drawn in
a DesignMask of a technology.

Attributes:
    epsilon: value under which two coordinate values are considered equal.
        Default is 1e-6; as coordinates are assumed to be in µm this
        corresponds with 1 fm.
    origin: (0.0, 0.0)
"""
import abc, enum
from itertools import product
from math import floor
from typing import (
    Any, Dict, Iterable, Iterator, Collection, Tuple, List,
    Optional, Union, TypeVar, cast, overload
)

from .. import _util
from ..typing import MultiT, cast_MultiT, OptMultiT, cast_OptMultiT
from . import property_ as _prp, mask as _msk


__all__ = [
    "epsilon",
    "Rotation", "FloatPoint",
    "RotationContext", "MoveContext",
    "ShapeT", "RectangularT", "PointsShapeT",
    "Point", "origin", "Line", "Polygon", "Rect", "MultiPath", "Ring", "RectRing",
    "MultiPartShape", "MultiShape",
    "RepeatedShape", "ArrayShape",
    "MaskShape", "MaskShapes",
    "Start", "SetWidth", "GoLeft", "GoDown", "GoRight", "GoUp", "Knot", "NoStart",
]


epsilon: float = 1e-6
def _eq(v1: float, v2: float):
    """Compare if two floats have a difference smaller than epsilon

    API Notes:
        This function may only be used inside this module
    """
    return (abs(v1 - v2) < epsilon)


_shape_childclass = TypeVar("_shape_childclass", bound="_Shape")


[docs]class Rotation(enum.Enum): """Enum type to represent supported `_Shape` rotations """ No = "no" R0 = "no" # alias R90 = "90" R180 = "180" R270 = "270" MX = "mirrorx" MX90 = "mirrorx&90" MY = "mirrory" MY90 = "mirrory&90"
[docs] @staticmethod def from_name(rot: str) -> "Rotation": """Helper function to convert a rotation string representation to a `Rotation` value. Arguments: rot: string r of the rotation; supported values: ("no", "90", "180", "270", "mirrorx", "mirrorx&90", "mirrory", "mirrory&90") Returns: Corresponding `Rotation` value """ lookup = { "no": Rotation.No, "90": Rotation.R90, "180": Rotation.R180, "270": Rotation.R270, "mirrorx": Rotation.MX, "mirrorx&90": Rotation.MX90, "mirrory": Rotation.MY, "mirrory&90": Rotation.MY90, } assert rot in lookup return lookup[rot]
@overload def __mul__(self, shape: "Rotation") -> "Rotation": ... # pragma: no cover @overload def __mul__(self, shape: _shape_childclass) -> _shape_childclass: ... # pragma: no cover @overload def __mul__(self, shape: "MaskShape") -> "MaskShape": ... # pragma: no cover @overload def __mul__(self, shape: "MaskShapes") -> "MaskShapes": ... # pragma: no cover def __mul__(self, shape) -> Union["Rotation", "ShapeT", "MaskShape", "MaskShapes"]: if isinstance(shape, Rotation): lookup: Dict["Rotation", Dict["Rotation", "Rotation"]] = { Rotation.R0: { Rotation.R0: Rotation.R0, Rotation.R90: Rotation.R90, Rotation.R180: Rotation.R180, Rotation.R270: Rotation.R270, Rotation.MX: Rotation.MX, Rotation.MX90: Rotation.MX90, Rotation.MY: Rotation.MY, Rotation.MY90: Rotation.MY90, }, Rotation.R90: { Rotation.R0: Rotation.R90, Rotation.R90: Rotation.R180, Rotation.R180: Rotation.R270, Rotation.R270: Rotation.R0, Rotation.MX: Rotation.MY90, Rotation.MX90: Rotation.R270, Rotation.MY: Rotation.MX90, Rotation.MY90: Rotation.MX, }, Rotation.R180: { Rotation.R0: Rotation.R180, Rotation.R90: Rotation.R270, Rotation.R180: Rotation.R0, Rotation.R270: Rotation.R90, Rotation.MX: Rotation.MY, Rotation.MX90: Rotation.MY90, Rotation.MY: Rotation.MX, Rotation.MY90: Rotation.MX90, }, Rotation.R270: { Rotation.R0: Rotation.R270, Rotation.R90: Rotation.R0, Rotation.R180: Rotation.R90, Rotation.R270: Rotation.R180, Rotation.MX: Rotation.MY90, Rotation.MX90: Rotation.MX, Rotation.MY: Rotation.MX90, Rotation.MY90: Rotation.MY, }, Rotation.MX: { Rotation.R0: Rotation.MX, Rotation.R90: Rotation.MX90, Rotation.R180: Rotation.MY, Rotation.R270: Rotation.MY90, Rotation.MX: Rotation.R0, Rotation.MX90: Rotation.R90, Rotation.MY: Rotation.R180, Rotation.MY90: Rotation.R270, }, Rotation.MX90: { Rotation.R0: Rotation.MX90, Rotation.R90: Rotation.MY, Rotation.R180: Rotation.MY90, Rotation.R270: Rotation.MX, Rotation.MX: Rotation.R270, Rotation.MX90: Rotation.R0, Rotation.MY: Rotation.R90, Rotation.MY90: Rotation.R180, }, Rotation.MY: { Rotation.R0: Rotation.MY, Rotation.R90: Rotation.MY90, Rotation.R180: Rotation.MX, Rotation.R270: Rotation.MX90, Rotation.MX: Rotation.R180, Rotation.MX90: Rotation.R270, Rotation.MY: Rotation.R0, Rotation.MY90: Rotation.R90, }, Rotation.MY90: { Rotation.R0: Rotation.MY90, Rotation.R90: Rotation.MX, Rotation.R180: Rotation.MX90, Rotation.R270: Rotation.MY, Rotation.MX: Rotation.R90, Rotation.MX90: Rotation.R180, Rotation.MY: Rotation.R270, Rotation.MY90: Rotation.R0, }, } return lookup[self][shape] elif isinstance(shape, (_Shape, MaskShape, MaskShapes)): if self == Rotation.R0: return shape else: return shape.rotated(rotation=self) else: raise TypeError( "unsupported operand type(s) for *: " f"'{self.__class__.__name__}' and '{shape.__class__.__name__}'" ) __rmul__ = __mul__
[docs]class RotationContext: """Context for rotate operations that are considered to belong together. Currently it will cache rotated MultiPartShape and link part to the rotated parts. API Notes: * API of `RotationContext` is not fixed yet. No backwards compatible guarantees are given. User code using the class may need to be adapted in the future. see [#76](https://gitlab.com/Chips4Makers/PDKMaster/-/issues/76) """ def __init__(self): self._rotation: Optional[Rotation] = None self._mps_cache: Dict["MultiPartShape", "MultiPartShape"] = {} def _rotate_part(self, *, part: "MultiPartShape._Part", rotation: Rotation, ) -> "MultiPartShape._Part": if self._rotation is None: self._rotation = rotation else: assert self._rotation == rotation mps = part.multipartshape idx = mps.parts.index(part) if mps in self._mps_cache: mps2 = self._mps_cache[mps] else: mps2 = mps.rotated(rotation=rotation) self._mps_cache[mps] = mps2 return mps2.parts[idx]
[docs]class MoveContext: """Context for move operations that are considered to be part of one move. Currently it will cache moved MultiPartShape and link part to the moved parts. API Notes: * API of `MoveContext` is not fixed yet. No backwards compatible guarantees are given. User code using the class may need to be adapted in the future. see [#76](https://gitlab.com/Chips4Makers/PDKMaster/-/issues/76) """ def __init__(self): self._dxy: Optional["Point"] = None self._mps_cache: Dict["MultiPartShape", "MultiPartShape"] = {} def _move_part(self, *, part: "MultiPartShape._Part", dxy: "Point") -> "MultiPartShape._Part": if self._dxy is None: self._dxy = dxy else: assert self._dxy == dxy mps = part.multipartshape idx = mps.parts.index(part) try: mps2 = self._mps_cache[mps] except KeyError: mps2 = mps.moved(dxy=dxy) self._mps_cache[mps] = mps2 return mps2.parts[idx]
class _Shape(abc.ABC): """The base class for representing shapes API Notes: * _Shape objects need to be immutable objects. They need to implement __hash__() and __eq__() """ @abc.abstractmethod def __init__(self): pass @property @abc.abstractmethod def pointsshapes(self) -> Iterable["PointsShapeT"]: raise NotImplementedError @property @abc.abstractmethod def bounds(self) -> "RectangularT": raise NotImplementedError @abc.abstractmethod def moved( self: "_shape_childclass", *, dxy: "Point", context: Optional[MoveContext]=None ) -> "_shape_childclass": """Move a _Shape object by a given vector This method is called moved() to represent the fact the _Shape objects are immutable and a new object is created by the moved() method. """ raise NotImplementedError def repeat(self, *, offset0: "Point", n: int, n_dxy: "Point", m: int=1, m_dxy: Optional["Point"]=None, ) -> "RepeatedShape": return RepeatedShape( shape=self, offset0=offset0, n=n, n_dxy=n_dxy, m=m, m_dxy=m_dxy, ) @abc.abstractmethod def rotated( self: "_shape_childclass", *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "_shape_childclass": """Rotate a _Shape object by a given vector This method is called rotated() to represent the fact the _Shape objects are immutable and a new object is created by the rotated() method. """ raise NotImplementedError @property @abc.abstractmethod def area(self) -> float: raise NotImplementedError @abc.abstractmethod def __eq__(self, o: object) -> bool: raise NotImplementedError @abc.abstractmethod def __hash__(self) -> int: raise NotImplementedError ShapeT = _Shape class _Rectangular(_Shape): """Mixin base class rectangular shapes API Notes: * This is private class for this module and is not exported by default. It should only be used as mixing inside this module. """ @property @abc.abstractmethod def left(self) -> float: raise NotImplementedError @property @abc.abstractmethod def bottom(self) -> float: raise NotImplementedError @property @abc.abstractmethod def right(self) -> float: raise NotImplementedError @property @abc.abstractmethod def top(self) -> float: raise NotImplementedError # Computed properties @property def width(self) -> float: return self.right - self.left @property def height(self) -> float: return self.top - self.bottom @property def center(self) -> "Point": return Point( x=0.5*(self.left + self.right), y=0.5*(self.bottom + self.top), ) RectangularT = _Rectangular class _PointsShape(_Shape): """base class for single shape that can be described as a list of points API Notes: * This is private class for this module and is not exported by default. It should only be used as mixing inside this module. """ @property @abc.abstractmethod def points(self) -> Iterable["Point"]: raise NotImplementedError def __eq__(self, o: object) -> bool: if not isinstance(o, _PointsShape): return False p_it1 = iter(self.points) p_it2 = iter(o.points) while True: try: p1 = next(p_it1) except StopIteration: try: p2 = next(p_it2) except StopIteration: # All points the same return True else: return False else: try: p2 = next(p_it2) except StopIteration: # Different number of points return False if p1 != p2: # Non-equal point return False def __hash__(self) -> int: return hash(tuple(self.points)) PointsShapeT = _PointsShape FloatPoint = Union[Tuple[float, float], List[float]]
[docs]class Point(_PointsShape, _Rectangular): """A point object Arguments: x: X-coordinate y: Y-coordinate API Notes: * Point objects are immutable, x and y coordinates may not be changed after object creation. * Point is a final class, no backwards compatibility is guaranteed for subclassing this class. """ def __init__(self, *, x: float, y: float): self._x = x self._y = y
[docs] @staticmethod def from_float(*, point: FloatPoint) -> "Point": assert len(point) == 2 return Point(x=point[0], y=point[1])
[docs] @staticmethod def from_point( *, point: "Point", x: Optional[float]=None, y: Optional[float]=None, ) -> "Point": if x is None: x = point.x if y is None: y = point.y return Point(x=x, y=y)
@property def x(self) -> float: """X-coordinate""" return self._x @property def y(self) -> float: """Y-coordinate""" return self._y # _Shape base class abstract methods @property def pointsshapes(self) -> Iterable[PointsShapeT]: return (self,) @property def bounds(self) -> RectangularT: return self
[docs] def moved(self, *, dxy: "Point", context: Optional[MoveContext]=None) -> "Point": x = self.x + dxy.x y = self.y + dxy.y return Point(x=x, y=y)
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None) -> "Point": x = self.x y = self.y tx, ty = { Rotation.No: (x, y), Rotation.R90: (-y, x), Rotation.R180: (-x, -y), Rotation.R270: (y, -x), Rotation.MX: (x, -y), Rotation.MX90: (y, x), Rotation.MY: (-x, y), Rotation.MY90: (-y, -x), }[rotation] return Point(x=tx, y=ty)
# _PointsShape base class abstract methods @property def points(self) -> Iterable["Point"]: return (self,) # _Rectangular mixin abstract methods @property def left(self) -> float: return self._x @property def bottom(self) -> float: return self._y @property def right(self) -> float: return self._x @property def top(self) -> float: return self._y def __neg__(self) -> "Point": return Point(x=-self.x, y=-self.y) @property def area(self): return 0.0 def __eq__(self, o: object) -> bool: if not isinstance(o, Point): return False else: return _eq(self.x, o.x) and _eq(self.y, o.y) def __hash__(self) -> int: return hash((self.x, self.y)) @overload def __add__(self, shape: _shape_childclass) -> _shape_childclass: ... # pragma: no cover @overload def __add__(self, shape: "MaskShape") -> "MaskShape": ... # pragma: no cover @overload def __add__(self, shape: "MaskShapes") -> "MaskShapes": ... # pragma: no cover def __add__(self, shape) -> Union[_Shape, "MaskShape", "MaskShapes"]: """The + operation with a Point. The + operation on a (mask)shape will move that shape with the given point as vector. Returns Shape shifted by the point as vector """ if isinstance(shape, (_Shape, MaskShape, MaskShapes)): return shape.moved(dxy=self) else: raise TypeError( "unsupported operand type(s) for +: " f"'{self.__class__.__name__}' and '{shape.__class__.__name__}'" ) __radd__ = __add__ @overload def __rsub__(self, shape: _shape_childclass) -> _shape_childclass: ... # pragma: no cover @overload def __rsub__(self, shape: "MaskShape") -> "MaskShape": ... # pragma: no cover @overload def __rsub__(self, shape: "MaskShapes") -> "MaskShapes": ... # pragma: no cover def __rsub__(self, shape) -> Union[_Shape, "MaskShape", "MaskShapes"]: """Operation shape - `Point` Returns Shape shifted by the negative of the point as vector """ if isinstance(shape, (_Shape, MaskShape, MaskShapes)): return shape.moved(dxy=-self) else: raise TypeError( "unsupported operand type(s) for -: " f"'{shape.__class__.__name__}' and '{self.__class__.__name__}'" ) # Point - Point is not handled by __rsub__ def __sub__(self, point: "Point") -> "Point": if isinstance(point, Point): return self.moved(dxy=-point) else: raise TypeError( "unsupported operand type(s) for -: " f"'{self.__class__.__name__}' and '{point.__class__.__name__}'" ) def __mul__(self, m: Union[float, Rotation]) -> "Point": if isinstance(m, (int, float)): return Point(x=m*self.x, y=m*self.y) elif isinstance(m, Rotation): return self.rotated(rotation=m) else: raise TypeError( f"unsupported operand type(s) for *: " f"'{self.__class__.__name__}' and '{m.__class__.__name__}'" ) __rmul__ = __mul__ def __str__(self) -> str: return f"({self.x:.6},{self.y:.6})" def __repr__(self) -> str: return f"Point(x={self.x:.6},y={self.y:.6})"
origin: Point = Point(x=0.0, y=0.0)
[docs]class Line(_PointsShape, _Rectangular): """A line shape A line consist of a start point and an end point. It is considered to be directional so two lines with start en and point exchanged are not considered equal. """ def __init__(self, *, point1: Point, point2: Point): self._point1 = point1 self._point2 = point2 @property def point1(self) -> Point: return self._point1 @property def point2(self) -> Point: return self._point2 # _Shape base class abstraxt methods @property def pointsshapes(self) -> Iterable[PointsShapeT]: return (self,) @property def bounds(self) -> RectangularT: return self
[docs] def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "Line": return Line( point1=self._point1.moved(dxy=dxy, context=context), point2=self._point2.moved(dxy=dxy, context=context), )
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None) -> "Line": return Line( point1=self.point1.rotated(rotation=rotation, context=context), point2=self.point2.rotated(rotation=rotation, context=context), )
# _PointsShape mixin abstract methods @property def points(self) -> Iterable[Point]: return (self._point1, self._point2) # _Rectangular mixin abstract methods @property def left(self) -> float: return min(self._point1.left, self._point2.left) @property def bottom(self) -> float: return min(self._point1.bottom, self._point2.bottom) @property def right(self) -> float: return max(self._point1.right, self._point2.right) @property def top(self) -> float: return max(self._point1.top, self._point2.top) @property def area(self): return 0.0 def __str__(self) -> str: return f"{self.point1}-{self.point2}" def __repr__(self) -> str: return f"Line(point1={self.point1!r},point2={self.point2!r})"
[docs]class Polygon(_PointsShape): def __init__(self, *, points: Iterable["Point"]): self._points = points = tuple(points) if points[0] != points[-1]: raise ValueError("Last point has to be the same as the first point") left = min(point.x for point in points) bottom = min(point.y for point in points) right = max(point.x for point in points) top = max(point.y for point in points) if _eq(left, right) or _eq(bottom, top): raise ValueError("Polygon with only colinear points not allowed") self._bounds: Rect = Rect(left=left, bottom=bottom, right=right, top=top)
[docs] @classmethod def from_floats( cls, *, points: Iterable[FloatPoint], ) -> "Polygon": """ API Notes: * This method is only meant to be called as Outline.from_floats not as obj.__class__.from_floats(). This means that subclasses may overload this method with incompatible call signature. """ return cls(points=(Point(x=x, y=y) for x, y in points))
# _Shape base class abstraxt methods @property def pointsshapes(self) -> Iterable[PointsShapeT]: yield self @property def bounds(self) -> RectangularT: return self._bounds
[docs] def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "Polygon": return Polygon(points=(point + dxy for point in self.points))
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "Polygon": return Polygon(points=( point.rotated(rotation=rotation, context=context) for point in self.points ))
# _PointsShape mixin abstract methods @property def points(self) -> Iterable[Point]: return self._points @property def area(self) -> float: raise NotImplementedError def __str__(self) -> str: s = "=".join(f"{str(p)}" for p in self.points) return f"{{{s}}}" def __repr__(self) -> str: s = ",".join(f"{repr(p)}" for p in self.points) return f"Polygon(points=({s}))"
[docs]class Rect(Polygon, _Rectangular): """A rectangular shape object Arguments: left, bottom, right, top: Edge coordinates of the rectangle; left, bottom have to be smaller than resp. right, top. API Notes: * Rect objects are immutable, dimensions may not be changed after creation. * This class is final. No backwards guarantess given for subclasses in user code """ def __init__(self, *, left: float, bottom: float, right: float, top: float): assert (left < right) and (bottom < top) self._left = left self._bottom = bottom self._right = right self._top = top
[docs] @staticmethod # type: ignore[override] def from_floats(*, values: Tuple[float, float, float, float]) -> "Rect": left, bottom, right, top = values return Rect(left=left, bottom=bottom, right=right, top=top)
[docs] @staticmethod def from_rect( *, rect: "_Rectangular", left: Optional[float]=None, bottom: Optional[float]=None, right: Optional[float]=None, top: Optional[float]=None, bias: Union[float, _prp.Enclosure]=0.0, ) -> "Rect": if not isinstance(bias, _prp.Enclosure): bias = _prp.Enclosure(bias) hbias = bias.first vbias = bias.second if left is None: left = rect.left left -= hbias if bottom is None: bottom = rect.bottom bottom -= vbias if right is None: right = rect.right right += hbias if top is None: top = rect.top top += vbias return Rect(left=left, bottom=bottom, right=right, top=top)
[docs] @staticmethod def from_corners(*, corner1: Point, corner2: Point) -> "Rect": left = min(corner1.x, corner2.x) bottom = min(corner1.y, corner2.y) right = max(corner1.x, corner2.x) top = max(corner1.y, corner2.y) return Rect(left=left, bottom=bottom, right=right, top=top)
[docs] @staticmethod def from_float_corners(*, corners: Tuple[FloatPoint, FloatPoint]) -> "Rect": return Rect.from_corners( corner1=Point.from_float(point=corners[0]), corner2=Point.from_float(point=corners[1]), )
[docs] @staticmethod def from_size( *, center: Point=Point(x=0, y=0), width: float, height: float, ) -> "Rect": assert (width > 0) and (height > 0) x = center.x y = center.y left = x - 0.5*width bottom = y - 0.5*height right = x + 0.5*width top = y + 0.5*height return Rect(left=left, bottom=bottom, right=right, top=top)
@property def left(self) -> float: return self._left @property def bottom(self) -> float: return self._bottom @property def right(self) -> float: return self._right @property def top(self) -> float: return self._top @property def bounds(self) -> RectangularT: return self # overloaded _Shape base class abstract methods
[docs] def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "Rect": left = self.left + dxy.x bottom = self.bottom + dxy.y right = self.right + dxy.x top = self.top + dxy.y return Rect(left=left, bottom=bottom, right=right, top=top)
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "Rect": if rotation in (Rotation.No, Rotation.R180, Rotation.MX, Rotation.MY): width = self.width height = self.height elif rotation in (Rotation.R90, Rotation.R270, Rotation.MX90, Rotation.MY90): width = self.height height = self.width else: raise RuntimeError( f"Internal error: unsupported rotation '{rotation}'" ) return Rect.from_size( center=self.center.rotated(rotation=rotation, context=context), width=width, height=height, )
# overloaded _PointsShape mixin abstract methods @property def points(self) -> Iterable[Point]: return ( Point(x=self.left, y=self.bottom), Point(x=self.left, y=self.top), Point(x=self.right, y=self.top), Point(x=self.right, y=self.bottom), Point(x=self.left, y=self.bottom), ) def __str__(self) -> str: p1 = Point(x=self.left, y=self.bottom) p2 = Point(x=self.right, y=self.top) return f"[{str(p1)}-{str(p2)}]" def __repr__(self) -> str: return ( f"{self.__class__.__name__}(" f"left={self.left:.6},bottom={self.bottom:.6}," f"right={self.right:.6},top={self.top:.6})" ) @property def area(self) -> float: return self.width*self.height def __eq__(self, o: object) -> bool: if not isinstance(o, Rect): return False return ( _eq(self.left, o.left) and _eq(self.bottom, o.bottom) and _eq(self.right, o.right) and _eq(self.top, o.top) ) def __hash__(self) -> int: return hash((self.left, self.bottom, self.right, self.top))
[docs]class MultiPath(Polygon): """A shape consisting of one or more paths. A single path consist of manhattan connections of varying width between points. A ``MultiPath`` object is created specifying a list of instructions that build the ``MultiPath``. The first instruction has to be the Start instructions and then follow by a list of other instructions. If a ``Knot`` instruction is included it has to be only once in the list as the last instruction. """ class _Instruction: pass
[docs] class Start(_Instruction): """Indicates the start of a MultiPath""" def __init__(self, *, point: Point, width: float): if width < -epsilon: raise ValueError( f"width has to be a positive value not '{width}'" ) self._point = point self._width = width def __eq__(self, obj: object) -> bool: return ( False if not isinstance(obj, Start) else ( (self._point == obj._point) and (abs(self._width - obj._width) < epsilon) ) )
[docs] class SetWidth(_Instruction): """Set the width for the next segment(s)""" def __init__(self, width: float): if width < -epsilon: raise ValueError( f"width has to be a positive value not '{width}'" ) self._width = width def __eq__(self, obj: object) -> bool: return ( False if not isinstance(obj, SetWidth) else abs(self._width - obj._width) < epsilon )
class _Go(_Instruction): """Base class for drawing a segment with a certain distance""" def __init__(self, dist: float): if dist < -epsilon: raise ValueError( f"dist has to be a positive value not '{dist}'" ) self._dist = dist def __eq__(self, obj: object) -> bool: if self.__class__ != obj.__class__: # _Go objects are final and have to be the same class, not just subclasses return False else: assert isinstance(obj, MultiPath._Go) return abs(self._dist - obj._dist) < epsilon
[docs] class GoLeft(_Go): """Go left from the current location API Notes: This class is final, subclassing may cause backwards compatibility problems. """ pass
[docs] class GoDown(_Go): """Go down from the current location API Notes: This class is final, subclassing may cause backwards compatibility problems. """ pass
[docs] class GoRight(_Go): """Go right from the current location API Notes: This class is final, subclassing may cause backwards compatibility problems. """ pass
[docs] class GoUp(_Go): """Go up from the current location API Notes: This class is final, subclassing may cause backwards compatibility problems. """ pass
[docs] class Knot(_Instruction): """A node is a point where different subpaths start from. Arguments: left, down, right, up: instructions for each subpath starting from the current location. At least two directions need to be specified. The first instruction in a direction that is not ``SetWidth`` may not be another ``Knot`` instruction. The direction in conflict with last ``_Go`` instruction may not be specified; e.g. if last instruction was ``GoUp``, down may not be specified. API Notes: This class is final, subclassing may cause backwards compatibility problems. """ def __init__(self, *, left: OptMultiT["MultiPath.NoKnot"]=None, down: OptMultiT["MultiPath.NoKnot"]=None, right: OptMultiT["MultiPath.NoKnot"]=None, up: OptMultiT["MultiPath.NoKnot"]=None, ): n_dirs = sum(ins is not None for ins in (left, down, right, up)) if n_dirs < 2: raise TypeError("At least two directions need instuctions for 'Knot'") self.left = cast_OptMultiT(left) self.down = cast_OptMultiT(down) self.right = cast_OptMultiT(right) self.up = cast_OptMultiT(up)
# All instructions except Start; for typing only NoStart = Union[SetWidth, _Go, Knot] # All instructions except Start & Knot; for typing only NoKnot = Union[SetWidth, _Go] class _PointsBuilder: def __init__(self, *, first: "MultiPath.Start"): self.location = first._point self.width = first._width self.prevwidth = first._width self.previnstr: MultiPath._Instruction = first self.prevdirtype: type = type(first) self.clkwcoords: List[Point] = [] self.cclkwcoords: List[Point] = [] def do_go(self, instr: "MultiPath._Go"): clkwcoords = self.clkwcoords cclkwcoords = self.cclkwcoords width = self.width prevwidth = self.prevwidth location = self.location prevdirtype = self.prevdirtype instrtype = type(instr) if prevdirtype == MultiPath.Start: if instrtype == MultiPath.GoLeft: dxy = Point(x=0.0, y=0.5*width) clkwcoords.append(location - dxy) cclkwcoords.append(location - dxy) cclkwcoords.append(location + dxy) elif instrtype == MultiPath.GoDown: dxy = Point(x=0.5*width, y=0.0) clkwcoords.append(location + dxy) cclkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif instrtype == MultiPath.GoRight: dxy = Point(x=0.0, y=0.5*width) clkwcoords.append(location + dxy) cclkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif instrtype == MultiPath.GoUp: dxy = Point(x=0.5*width, y=0.0) clkwcoords.append(location - dxy) cclkwcoords.append(location - dxy) cclkwcoords.append(location + dxy) else: # pragma: no cover raise RuntimeError( f"Internal error: unknown instruction type '{instrtype}'" ) elif prevdirtype == MultiPath.GoLeft: if instrtype == MultiPath.GoLeft: dxy1 = Point(x=0.0, y=0.5*self.prevwidth) dxy2 = Point(x=0.0, y=0.5*width) clkwcoords.extend((location - dxy1, location - dxy2)) cclkwcoords.extend((location + dxy1, location + dxy2)) elif instrtype == MultiPath.GoDown: dxy = Point(x=0.5*width, y=-0.5*prevwidth) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif instrtype == MultiPath.GoRight: raise ValueError( "GoRight instruction after GoLeft not allowed" ) elif instrtype == MultiPath.GoUp: dxy = Point(x=-0.5*width, y=-0.5*prevwidth) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) else: # pragma: no cover raise RuntimeError( f"Internal error: unknown instruction type '{instrtype}'" ) elif prevdirtype == MultiPath.GoDown: if instrtype == MultiPath.GoLeft: dxy = Point(x=0.5*prevwidth, y=-0.5*width) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif instrtype == MultiPath.GoDown: dxy1 = Point(x=0.5*prevwidth, y=0.0) dxy2 = Point(x=0.5*width, y=0.0) clkwcoords.extend((location + dxy1, location + dxy2)) cclkwcoords.extend((location - dxy1, location - dxy2)) elif instrtype == MultiPath.GoRight: dxy = Point(x=0.5*prevwidth, y=0.5*width) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif instrtype == MultiPath.GoUp: raise ValueError( "GoUp instruction after GoDown not allowed" ) else: # pragma: no cover raise RuntimeError( f"Internal error: unknown instruction type '{instrtype}'" ) elif prevdirtype == MultiPath.GoRight: if instrtype == MultiPath.GoLeft: raise ValueError( "GoLeft instruction after GoRight not allowed" ) elif instrtype == MultiPath.GoDown: dxy = Point(x=0.5*width, y=0.5*prevwidth) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif instrtype == MultiPath.GoRight: dxy1 = Point(x=0.0, y=0.5*prevwidth) dxy2 = Point(x=0.0, y=0.5*width) clkwcoords.extend((location + dxy1, location + dxy2)) cclkwcoords.extend((location - dxy1, location - dxy2)) elif instrtype == MultiPath.GoUp: dxy = Point(x=-0.5*width, y=0.5*prevwidth) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) else: # pragma: no cover raise RuntimeError( f"Internal error: unknown instruction type '{instrtype}'" ) elif prevdirtype == MultiPath.GoUp: if instrtype == MultiPath.GoLeft: dxy = Point(x=-0.5*prevwidth, y=-0.5*width) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif instrtype == MultiPath.GoDown: raise ValueError( "GoDown instruction after GoUp not allowed" ) elif instrtype == MultiPath.GoRight: dxy = Point(x=-0.5*prevwidth, y=0.5*width) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif instrtype == MultiPath.GoUp: dxy1 = Point(x=0.5*prevwidth, y=0.0) dxy2 = Point(x=0.5*width, y=0.0) clkwcoords.extend((location - dxy1, location - dxy2)) cclkwcoords.extend((location + dxy1, location + dxy2)) else: # pragma: no cover raise RuntimeError( f"Internal error: unknown instruction type '{instrtype}'" ) else: # pragma: no cover raise RuntimeError( f"Internal error: unknown instruction type '{instrtype}'" ) # Update location if instrtype == MultiPath.GoLeft: self.location += Point(x=-cast(MultiPath.GoLeft, instr)._dist, y=0.0) elif instrtype == MultiPath.GoDown: self.location += Point(x=0.0, y=-cast(MultiPath.GoDown, instr)._dist) elif instrtype == MultiPath.GoRight: self.location += Point(x=cast(MultiPath.GoRight, instr)._dist, y=0.0) elif instrtype == MultiPath.GoUp: self.location += Point(x=0.0, y=cast(MultiPath.GoDown, instr)._dist) else: # pragma: no cover raise RuntimeError( f"Internal error: unknown `_Go` instruction type '{instrtype}'" ) def _knot_builder(self, *, instrs: Optional[Tuple["MultiPath.NoStart", ...]] ) -> Optional["MultiPath._PointsBuilder"]: if instrs is None: return None else: first = instrs[0] if isinstance(first, MultiPath.SetWidth): start = MultiPath.Start(point=self.location, width=first._width) instrs = instrs[1:] else: start = MultiPath.Start(point=self.location, width=self.width) builder = MultiPath._PointsBuilder(first=start) for instr2 in instrs: builder.do_instr(instr2) builder.finalize() return builder def do_knot(self, instr: "MultiPath.Knot"): prevdirtype = self.prevdirtype left_builder = self._knot_builder(instrs=instr.left) up_builder = self._knot_builder(instrs=instr.up) right_builder = self._knot_builder(instrs=instr.right) down_builder = self._knot_builder(instrs=instr.down) def conn_ends(*, end: Point, start: Point): end_sw = (end.x < self.location.x) and (end.y < self.location.y) end_se = (end.x > self.location.x) and (end.y < self.location.y) end_nw = (end.x < self.location.x) and (end.y > self.location.y) end_ne = (end.x > self.location.x) and (end.y > self.location.y) start_sw = (start.x < self.location.x) and (start.y < self.location.y) start_se = (start.x > self.location.x) and (start.y < self.location.y) start_nw = (start.x < self.location.x) and (start.y > self.location.y) start_ne = (start.x > self.location.x) and (start.y > self.location.y) if (end_sw or end_ne) and (start_sw or start_ne): self.clkwcoords.append(Point(x=end.x, y=start.y)) elif (end_se or end_nw) and (start_se or start_nw): self.clkwcoords.append(Point(x=start.x, y=end.y)) elif (end_sw and start_nw) or (end_ne and start_se): if abs(end.x - start.x) > epsilon: self.clkwcoords.extend(( Point(x=end.x, y=self.location.y), Point(x=start.x, y=self.location.y) )) elif (end_nw and start_ne) or (end_se and start_sw): if abs(end.y - start.y) > epsilon: self.clkwcoords.extend(( Point(x=self.location.x, y=end.y), Point(x=self.location.x, y=start.y), )) else: # pragma: no cover raise RuntimeError("Internal error") if prevdirtype == MultiPath.GoUp: assert down_builder is None builders = (left_builder, up_builder, right_builder) elif prevdirtype == MultiPath.GoLeft: assert right_builder is None builders = (down_builder, left_builder, up_builder) elif prevdirtype == MultiPath.GoDown: assert up_builder is None builders = (right_builder, down_builder, left_builder) elif prevdirtype == MultiPath.GoRight: assert left_builder is None builders = (up_builder, right_builder, down_builder) else: # pragma: no cover raise RuntimeError("Internal error") for builder in builders: if builder is not None: conn_ends(end=self.clkwcoords[-1], start=builder.clkwcoords[1]) self.clkwcoords.extend(( *builder.clkwcoords[1:], *reversed(builder.cclkwcoords[2:]), )) conn_ends(end=self.clkwcoords[-1], start=self.cclkwcoords[-1]) def do_instr(self, instr: "MultiPath.NoStart"): prevtype: type = type(self.previnstr) if issubclass(prevtype, (MultiPath._Go, MultiPath.Knot)): self.prevdirtype = prevtype instrtype: type = type(instr) prevdirtype = self.prevdirtype if instrtype == prevtype: raise ValueError( "Two instructions of same type after each other is not allowed " ) elif instrtype == MultiPath.Start: raise ValueError("No 'Start' instruction allowed after the first one") if ( (instrtype == MultiPath.SetWidth) and (self.prevdirtype == MultiPath.Start) ): raise ValueError( "First instruction after 'Start' may not be 'SetWidth'", ) if prevdirtype == MultiPath.Knot: raise ValueError( "No instuction allowed after 'Knot' instruction", ) # First instruction after Start needs to be handled differently if isinstance(instr, MultiPath._Go): self.do_go(instr) elif isinstance(instr, MultiPath.Knot): self.do_knot(instr) elif not isinstance(instr, MultiPath.SetWidth): # pragma: no cover raise NotImplementedError(f"instuction type '{instrtype}'") # Update width newwidth = instr._width if isinstance(instr, MultiPath.SetWidth) else self.width self.prevwidth = self.width self.width = newwidth self.previnstr = instr def finalize(self): location = self.location width = self.width clkwcoords = self.clkwcoords cclkwcoords = self.cclkwcoords # Complete the last instruction prevtype = type(self.previnstr) if prevtype == MultiPath.SetWidth: raise ValueError( f"SetWidth may not be the last instruction" ) elif prevtype == MultiPath.GoLeft: dxy = Point(x=0.0, y=0.5*width) clkwcoords.append(location - dxy) cclkwcoords.append(location + dxy) elif prevtype == MultiPath.GoDown: dxy = Point(x=0.5*width, y=0.0) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif prevtype == MultiPath.GoRight: dxy = Point(x=0.0, y=0.5*width) clkwcoords.append(location + dxy) cclkwcoords.append(location - dxy) elif prevtype == MultiPath.GoUp: dxy = Point(x=0.5*width, y=0.0) clkwcoords.append(location - dxy) cclkwcoords.append(location + dxy) elif prevtype == MultiPath.Knot: pass else: # pragma: no cover raise RuntimeError( f"Internal error: unknown instruction type '{prevtype}'", ) assert len(clkwcoords) > 0 assert len(cclkwcoords) > 0 def __init__(self, first: Start, *instrs: "MultiPath.NoStart"): if len(instrs) == 0: raise ValueError("At least one instruction needed after 'Start'") self._first: MultiPath.Start = first self._instrs = instrs # Build the coordinates builder = MultiPath._PointsBuilder(first=first) for instr in instrs: builder.do_instr(instr) builder.finalize() super().__init__(points=( *builder.clkwcoords, *reversed(builder.cclkwcoords), )) @property def first(self) -> Start: return self._first @property def instrs(self) -> Tuple[_Instruction]: return self._instrs
# Instruction aliases Start = MultiPath.Start SetWidth = MultiPath.SetWidth GoLeft = MultiPath.GoLeft GoDown = MultiPath.GoDown GoRight = MultiPath.GoRight GoUp = MultiPath.GoUp Knot = MultiPath.Knot NoStart = MultiPath.NoStart # For typing only
[docs]class Ring(MultiPath): """A shape representating a ring shape polygon Arguments: outer_bound: the outer edge of the shape ring_width: the width of the ring, it has to be smaller than half the width or height of the outer edge. """ def __init__(self, *, outer_bound: Rect, ring_width: float): if (ring_width + epsilon) > outer_bound.width/2.0: raise ValueError( f"ring_width '{ring_width}' is bigger than half outer bound width" f" '{outer_bound.width}'", ) if (ring_width + epsilon) > outer_bound.height/2.0: raise ValueError( f"ring_width '{ring_width}' is bigger than half outer bound height" f" '{outer_bound.height}'", ) oleft = outer_bound.left obottom = outer_bound.bottom oright = outer_bound.right otop = outer_bound.top oheight = otop - obottom owidth = oright - oleft mleft = oleft + 0.5*ring_width instrs = ( Start(point=Point(x=mleft, y=obottom), width=ring_width), GoUp(oheight - 0.5*ring_width), GoRight(owidth - ring_width), GoDown(oheight - ring_width), GoLeft(owidth - 1.5*ring_width), ) super().__init__(*instrs) self.outer_bound = outer_bound self.ring_width = ring_width
[docs]class RectRing(_Shape): """A `RectRing` object is a shape that consists of a ring of `Rect` objects. An exception will be raised when there is not enough room to put the four corner rects. If the 'Rect' objects needs to be on a grid all dimensions specified for this object - including outer bound placement, width & height - have to be double that grid number. Arguments: outer_bound: the outer bound of the ring; e.g. the generated rect shapes will be inside and touching the bound. rect_width: the width of the generated rect objects. rect_height: the height of the generated rect objects; by default it will be the same as rect_width. min_rect_space: the minimum space between two rect structures. """ # TODO: Describe rules to get shapes on grid def __init__(self, *, outer_bound: Rect, rect_width: float, rect_height: Optional[float]=None, min_rect_space: float, ): if rect_height is None: rect_height = rect_width if (outer_bound.width + epsilon) < (2*rect_width + min_rect_space): raise ValueError( "outer_bound width not big enough to fit two rects in" ) if (outer_bound.height + epsilon) < (2*rect_height + min_rect_space): raise ValueError( "outer_bound height not big enough to fit two rects in" ) self._outer_bound = outer_bound self._rect_width = float(rect_width) self._rect_height = float(rect_height) self._min_rect_space = float(min_rect_space) pitch_x = rect_width + min_rect_space # Rects in horizontal direction besides corners self._n_x = floor( ( self.outer_bound.width - (2*self.rect_width + self._min_rect_space) + epsilon )/pitch_x ) assert self._n_x >= 0, "Internal error" pitch_y = rect_height + min_rect_space # Rects in vertical direction besides corners self._n_y = floor( ( self.outer_bound.height - (2*self.rect_height + self._min_rect_space) + epsilon )/pitch_y ) assert self._n_y >= 0, "Internal error" @property def outer_bound(self) -> Rect: return self._outer_bound @property def rect_width(self) -> float: return self._rect_width @property def rect_height(self) -> float: return self._rect_height @property def min_rect_space(self) -> float: return self._min_rect_space
[docs] def moved(self, *, dxy: "Point", context: Optional[MoveContext]=None, ) -> "RectRing": return RectRing( outer_bound=self.outer_bound.moved(dxy=dxy, context=context), rect_width=self.rect_width, rect_height=self.rect_height, min_rect_space=self.min_rect_space, )
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "RectRing": return RectRing( outer_bound=self.outer_bound.rotated(rotation=rotation, context=context), rect_width=self.rect_width, rect_height=self.rect_height, min_rect_space=self.min_rect_space, )
@property def pointsshapes(self) -> Iterable["PointsShapeT"]: rect = Rect.from_size(width=self.rect_width, height=self.rect_height) left = self.outer_bound.left bottom = self.outer_bound.bottom right = self.outer_bound.right top = self.outer_bound.top left_x = left + 0.5*self.rect_width right_x = right - 0.5*self.rect_width mid_x = self.outer_bound.center.x bottom_y = bottom + 0.5*self.rect_height top_y = top - 0.5*self.rect_height mid_y = self.outer_bound.center.y pitch_x = self.rect_width + self.min_rect_space pitch_y = self.rect_height + self.min_rect_space # corners yield rect + Point(x=left_x, y=bottom_y) yield rect + Point(x=left_x, y=top_y) yield rect + Point(x=right_x, y=bottom_y) yield rect + Point(x=right_x, y=top_y) # bottom and top left_x2 = mid_x - 0.5*(self._n_x - 1)*pitch_x for n in range(self._n_x): x = left_x2 + n*pitch_x yield rect + Point(x=x, y=bottom_y) yield rect + Point(x=x, y=top_y) # left and right bottom_y2 = mid_y - 0.5*(self._n_y - 1)*pitch_y for n in range(self._n_y): y = bottom_y2 + n*pitch_y yield rect + Point(x=left_x, y=y) yield rect + Point(x=right_x, y=y) @property def bounds(self) -> RectangularT: return self.outer_bound @property def area(self) -> float: return (4 + 2*self._n_x + 2*self._n_y)*self.rect_width*self.rect_height def __eq__(self, o: object) -> bool: if not isinstance(o, RectRing): return False else: return all(( self.outer_bound == o.outer_bound, abs(self.rect_width - o.rect_width) <= epsilon, abs(self.rect_height - o.rect_height) <= epsilon, abs(self.min_rect_space - o.min_rect_space) <= epsilon, )) def __hash__(self) -> int: return hash( (self.outer_bound, self.rect_width, self.rect_height, self.min_rect_space), ) def __repr__(self) -> str: s_args = ",".join(( f"outer_bound={self.outer_bound!r}", f"rect_width={self.rect_width!r}", f"rect_height={self.rect_height!r}", f"min_rect_space={self.min_rect_space!r}", )) return f"RingRect({s_args})"
[docs]class MultiPartShape(Polygon): """This shape represents a single polygon shape that consist of a build up of touching parts. Main use case is to represent a shape where parts are on a different net as is typically the case for a WaferWire. Arguments: fullshape: The full shape parts: The subshapes The subshapes should be touching shapes and joined should form the fullshape shape. Currently it is only checked if the areas match, in better checking may be implemented. The subshapes will be converted to MultiPartShape._Part objects before becoming member of the parts property """ # TODO: merging of shapes is not complete. standard cell library still seems to # generate geometries that are no properly merged. class _Part(Polygon): """A shape representing one part of a MultiPartShape This object keeps reference to the MultiPartShape so the parts can be added to nets in layout and the shapes still being able to know to which MultiPartShape object they belong. """ def __init__(self, *, partshape: Polygon, multipartshape: "MultiPartShape"): self._partshape = partshape self._multipartshape = multipartshape @property def partshape(self) -> Polygon: return self._partshape @property def multipartshape(self) -> "MultiPartShape": return self._multipartshape @property def pointsshapes(self) -> Iterable[PointsShapeT]: yield self @property def bounds(self) -> RectangularT: return self.partshape.bounds def moved(self, *, dxy: Point, context: Optional[MoveContext]=None, ) -> "MultiPartShape._Part": if context is None: idx = self.multipartshape.parts.index(self) return self.multipartshape.moved(dxy=dxy).parts[idx] else: return context._move_part(part=self, dxy=dxy) def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "MultiPartShape._Part": if context is None: idx = self.multipartshape.parts.index(self) return self.multipartshape.rotated(rotation=rotation).parts[idx] else: return context._rotate_part(part=self, rotation=rotation) # _PointsShape mixin abstract methods @property def points(self) -> Iterable[Point]: return self.partshape.points @property def area(self) -> float: return self.partshape.area def __str__(self) -> str: return f"<<{str(self.partshape)}>>" def __repr__(self) -> str: ps = self.partshape return f"MultiPartShape._Part(partshape={ps!r})" def __hash__(self) -> int: return hash((self.partshape, self.multipartshape)) def __eq__(self, other: Any) -> bool: if not isinstance(other, MultiPartShape._Part): return False else: return ( (self.partshape == other.partshape) and (self.multipartshape == other.multipartshape) ) def __init__(self, fullshape: Polygon, parts: Iterable[Polygon]): # TODO: check if shape is actually build up of the parts self._fullshape = fullshape self._parts = tuple( MultiPartShape._Part(partshape=part, multipartshape=self) for part in parts ) @property def fullshape(self) -> Polygon: return self._fullshape @property def parts(self) -> Tuple["MultiPartShape._Part", ...]: return self._parts @property def pointsshapes(self) -> Iterable[PointsShapeT]: return self.fullshape.pointsshapes @property def bounds(self) -> RectangularT: return self.fullshape.bounds
[docs] def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "MultiPartShape": return MultiPartShape( fullshape=self.fullshape.moved(dxy=dxy), parts=(part.partshape.moved(dxy=dxy) for part in self.parts) )
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "MultiPartShape": return MultiPartShape( fullshape=self.fullshape.rotated(rotation=rotation, context=context), parts=( part.partshape.rotated(rotation=rotation, context=context) for part in self.parts ) )
# _PointsShape mixin abstract methods @property def points(self) -> Iterable[Point]: return self.fullshape.points @property def area(self) -> float: return self.fullshape.area def __hash__(self) -> int: return hash(self.fullshape) def __eq__(self, other: Any) -> bool: if not isinstance(other, MultiPartShape): return False else: return ( {part.partshape for part in self.parts} == {part.partshape for part in other.parts} ) def __str__(self) -> str: s = "|".join(str(p.partshape) for p in self._parts) return f"({s})" def __repr__(self) -> str: s1 = repr(self.fullshape) s2 = ",".join(repr(p.partshape) for p in self._parts) return f"MultiPartShape(fullshape={s1},parts=({s2}))"
[docs]class MultiShape(_Shape, Collection[_Shape]): """A shape representing a group of shapes Arguments: shapes: the sub shapes. Subshapes may or may not overlap. The object will fail to create if only one unique shape is provided including if the same shape is provided multiple times without another shape. MultiShape objects part of the provided shapes will be flattened and it's children will be joined with the other shapes. """ def __init__(self, *, shapes: Iterable[_Shape]): def iterate_shapes(ss: Iterable[_Shape]) -> Iterable[_Shape]: for shape in ss: if isinstance(shape, MultiShape): yield from iterate_shapes(shape.shapes) else: yield shape self._shapes = shapes = frozenset(iterate_shapes(shapes)) if len(shapes) < 2: raise ValueError("MultiShape has to consist of more than one shape") @property def shapes(self) -> Iterable[ShapeT]: return self._shapes # _Shape base class abstract methods @property def pointsshapes(self) -> Iterable[PointsShapeT]: for shape in self._shapes: yield from shape.pointsshapes @property def bounds(self) -> RectangularT: boundss = tuple(shape.bounds for shape in self.shapes) left = min(bounds.left for bounds in boundss) bottom = min(bounds.bottom for bounds in boundss) right = max(bounds.right for bounds in boundss) top = max(bounds.top for bounds in boundss) # It should be impossible to create a MultiShape where bounds # corresponds with a point. assert (left != right) or (bottom != top), "Internal error" if (left == right) or (bottom == top): return Line( point1=Point(x=left, y=bottom), point2=Point(x=right, y=top), ) else: return Rect(left=left, bottom=bottom, right=right, top=top)
[docs] def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "MultiShape": # Avoid generating different MultiPartShape for parts from the same MultiPartShape if context is None: context=MoveContext() return MultiShape( shapes=( polygon.moved(dxy=dxy, context=context) for polygon in self.pointsshapes ), )
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "MultiShape": return MultiShape( shapes=( polygon.rotated(rotation=rotation, context=context) for polygon in self.pointsshapes ) )
# Collection mixin abstract methods def __iter__(self) -> Iterator[_Shape]: return iter(self.shapes) def __len__(self) -> int: return len(self._shapes) def __contains__(self, shape: object) -> bool: return shape in self._shapes @property def area(self) -> float: # TODO: guarantee non overlapping shapes return sum(shape.area for shape in self._shapes) def __eq__(self, o: object) -> bool: if not isinstance(o, MultiShape): return False else: return self._shapes == o._shapes def __hash__(self) -> int: return hash(self.shapes) def __str__(self) -> str: # substrings are sorted to get reproducable order independent str representation return "(" + ",".join(sorted(str(shape) for shape in self.shapes)) + ")" def __repr__(self) -> str: # substrings are sorted to get reproducable order independent str representation return ( "MultiShape(shapes=(" + ",".join(sorted(repr(shape) for shape in self.shapes)) + "))" )
[docs]class RepeatedShape(_Shape): """A repetition of a shape allowing easy generation of array of objects. Implementation is generic so that one can represent any repetition with one or two vector that don't need to be manhattan. API Notes: * The current implementation assumes repeated shapes don't overlap. If they do area property will give wrong value. """ # TODO: decide if repeated shapes may overlap, if not can we check it ? def __init__(self, *, shape: ShapeT, offset0: Point, n: int, n_dxy: Point, m: int=1, m_dxy: Optional[Point]=None, ): if n < 2: raise ValueError(f"n has to be equal to or higher than 2, not '{n}'") if m < 1: raise ValueError(f"m has to be equal to or higher than 1, not '{m}'") if (m > 1) and (m_dxy is None): raise ValueError("m_dxy may not be None if m > 1") self._shape = shape self._offset0 = offset0 self._n = n self._n_dxy = n_dxy self._m = m self._m_dxy = m_dxy self._hash = None @property def shape(self) -> ShapeT: return self._shape @property def offset0(self) -> Point: return self._offset0 @property def n(self) -> int: return self._n @property def n_dxy(self) -> Point: return self._n_dxy @property def m(self) -> int: return self._m @property def m_dxy(self) -> Optional[Point]: return self._m_dxy
[docs] def moved( self: "RepeatedShape", *, dxy: "Point", context: Optional[MoveContext]=None, ) -> "RepeatedShape": return RepeatedShape( shape=self.shape, offset0=(self.offset0 + dxy), n=self.n, n_dxy=self.n_dxy, m=self.m, m_dxy=self.m_dxy, )
@property def pointsshapes(self) -> Iterable[PointsShapeT]: if self.m <= 1: for i_n in range(self.n): dxy = self.offset0 + i_n*self.n_dxy yield from (polygon + dxy for polygon in self.shape.pointsshapes) else: assert self.m_dxy is not None for i_n, i_m in product(range(self.n), range(self.m)): dxy = self.offset0 + i_n*self.n_dxy + i_m*self.m_dxy yield from (polygon + dxy for polygon in self.shape.pointsshapes) @property def bounds(self) -> RectangularT: b0 = self.shape.bounds b1 = b0 + self.offset0 if self.m <= 1: b2 = b0 + (self.offset0 + (self.n - 1)*self.n_dxy) else: assert self.m_dxy is not None b2 = b0 + ( self.offset0 + (self.n - 1)*self.n_dxy + (self.m - 1)*self.m_dxy ) return Rect( left=min(b1.left, b2.left), right=max(b1.right, b2.right), bottom=min(b1.bottom, b2.bottom), top=max(b1.top, b2.top), )
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "RepeatedShape": return RepeatedShape( shape=self.shape.rotated(rotation=rotation, context=context), offset0=self.offset0.rotated(rotation=rotation, context=context), n=self.n, n_dxy=self.n_dxy.rotated(rotation=rotation, context=context), m=self.m, m_dxy=( None if self.m_dxy is None else self.m_dxy.rotated(rotation=rotation, context=context) ) )
@property def area(self) -> float: # TODO: Support case with overlapping shapes ? return self.n*self.m*self.shape.area def __eq__(self, o: object) -> bool: if not isinstance(o, RepeatedShape): return False elif (self.shape != o.shape) or (self.offset0 != o.offset0): return False elif self.m == 1: return ( (self.n == o.n) and (self.n_dxy == o.n_dxy) and (o.m == 1) ) elif self.n == self.m: return ( (self.n == o.n == o.m) # dxy value may be exchanged => compare sets and ({self.n_dxy, self.m_dxy} == {o.n_dxy, o.m_dxy}) ) else: # (self.n != self.m) and (self.m > 1) return ( ( (self.n == o.n) and (self.n_dxy == o.n_dxy) and (self.m == o.m) and (self.m_dxy == o.m_dxy) ) or ( (self.n == o.m) and (self.n_dxy == o.m_dxy) and (self.m == o.n) and (self.m_dxy == o.n_dxy) ) ) def __hash__(self) -> int: if self._hash is None: if self.m == 1: self._hash = hash(frozenset(( self.shape, self.offset0, self.n, self.n_dxy, ))) else: self._hash = hash(frozenset(( self.shape, self.offset0, self.n, self.n_dxy, self.m, self.m_dxy, ))) return self._hash def __repr__(self) -> str: s_args = ",".join(( f"shape={self.shape!r}", f"offset0={self.offset0!r}", f"n={self.n}", f"n_dxy={self.n_dxy!r}", f"m={self.m}", f"m_dxy={self.m_dxy!r}", )) return f"RepeatedShape({s_args})"
[docs]class ArrayShape(RepeatedShape): """Object representing a manhattan repeared shape. This is a RepeatedShape subclass with repeat vectors either a horizontal and/or a vertical one. Arguments: shape: The object to repeat offset0: The placement of the first shape rows, columns: The number of rows and columns Both have to be equal or higher than 1 and either rows or columns has to be higher than 1. pitch_y, pitch_x: The displacement for resp. the rows and the columns. """ def __init__(self, *, shape: _Shape, offset0: Point, rows: int, columns: int, pitch_y: Optional[float]=None, pitch_x: Optional[float]=None, ): if (rows <= 0) or (columns <= 0): raise ValueError( f"rows ({rows}) and columns ({columns}) need to be integers greater than zero" ) if (rows == 1) and (columns == 1): raise ValueError( "either rows or columns or both have to be greater than 1" ) if (rows > 1) and (pitch_y is None): raise ValueError( "pitch_y not given for rows > 1" ) if (columns > 1) and (pitch_x is None): raise ValueError( "pitch_x not given for columns > 1" ) self._rows = rows self._columns = columns self._pitch_x = pitch_x self._pitch_y = pitch_y if rows == 1: n = columns n_dxy = Point(x=cast(float, pitch_x), y=0.0) m = 1 m_dxy = None else: n = rows n_dxy = Point(x=0.0, y=cast(float, pitch_y)) m = columns m_dxy = None if pitch_x is None else Point(x=pitch_x, y=0.0) super().__init__( shape=shape, offset0=offset0, n=n, n_dxy=n_dxy, m=m, m_dxy=m_dxy, ) @property def rows(self) -> int: return self._rows @property def columns(self) -> int: return self._columns @property def pitch_x(self) -> Optional[float]: return self._pitch_x @property def pitch_y(self) -> Optional[float]: return self._pitch_y
[docs]class MaskShape: def __init__(self, *, mask: _msk.DesignMask, shape: ShapeT): self._mask = mask self._shape = shape # TODO: Check grid @property def mask(self) -> _msk.DesignMask: return self._mask @property def shape(self) -> ShapeT: return self._shape
[docs] def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "MaskShape": return MaskShape(mask=self.mask, shape=self.shape.moved(dxy=dxy, context=context))
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "MaskShape": return MaskShape( mask=self.mask, shape=self.shape.rotated(rotation=rotation, context=context), )
@property def area(self) -> float: return self.shape.area def __eq__(self, o: object) -> bool: if not isinstance(o, MaskShape): return False return (self.mask == o.mask) and (self.shape == o.shape) def __hash__(self) -> int: return hash((self.mask, self.shape)) def __repr__(self) -> str: return f"MaskShape=(mask={self.mask!r},shape={self.shape!r})" @property def bounds(self) -> RectangularT: return self.shape.bounds
[docs]class MaskShapes(_util.ExtendedListMapping[MaskShape, _msk.DesignMask]): """A TypedListMapping of MaskShape objects. API Notes: Contrary to other classes a MaskShapes object is mutable if not frozen. """ @property def _index_attribute_(self): return "mask" def __init__(self, iterable: MultiT[MaskShape]): shapes = cast_MultiT(iterable) def join_shapes() -> Iterable[MaskShape]: masks = [] for shape in shapes: mask = shape.mask if mask not in masks: shapes2 = tuple(filter(lambda ms: ms.mask == mask, shapes)) if len(shapes2) == 1: yield shapes2[0] else: yield MaskShape( mask=mask, shape=MultiShape(shapes=(ms.shape for ms in shapes2)) ) masks.append(mask) super().__init__(join_shapes()) def __iadd__(self, shape: MultiT[MaskShape]) -> "MaskShapes": for s in cast_MultiT(shape): mask = s.mask try: ms = self[mask] except KeyError: super().__iadd__(s) except: # pragma: no cover raise else: if ms.shape != s.shape: ms2 = MaskShape( mask=mask, shape=MultiShape(shapes=(ms.shape, s.shape)), ) self[mask] = ms2 return self
[docs] def move(self, *, dxy: Point, context: Optional[MoveContext]=None) -> None: if context is None: context = MoveContext() if self._frozen_: raise TypeError(f"moving frozen '{self.__class__.__name__}' object not allowed") for i in range(len(self)): self[i] = self[i].moved(dxy=dxy, context=context)
[docs] def moved(self, *, dxy: Point, context: Optional[MoveContext]=None) -> "MaskShapes": """Moved MaskShapes object will not be frozen""" if context is None: context = MoveContext() return MaskShapes(ms.moved(dxy=dxy, context=context) for ms in self)
[docs] def rotate(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> None: if self._frozen_: raise TypeError(f"rotating frozen '{self.__class__.__name__}' object not allowed") if context is None: context = RotationContext() for i in range(len(self)): self[i] = self[i].rotated(rotation=rotation, context=context)
[docs] def rotated(self, *, rotation: Rotation, context: Optional[RotationContext]=None, ) -> "MaskShapes": """Rotated MaskShapes object will not be frozen""" if context is None: context = RotationContext() return MaskShapes( ms.rotated(rotation=rotation, context=context) for ms in self )