Source code for pdkmaster.technology.mask

# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-or-later OR CERN-OHL-S-2.0+ OR Apache-2.0
import abc
from typing import Tuple, Iterable, Optional, ClassVar, cast

from ..typing import MultiT, cast_MultiT
from . import rule as _rle, property_ as _prp


__all__ = ["MaskT", "RuleMaskT", "DesignMask", "Join", "Intersect"]


class _MaskProperty(_prp._Property):
    """`_MaskProperty` is a `Property` object on a single `MaskT` object.
    
    Typical examples are width, area, density etc.
    """
    def __init__(self, *, mask: "MaskT", name: str):
        super().__init__(name=(mask.name + "." + name))
        self.mask = mask
        self.prop_name = name


class _DualMaskProperty(_prp._Property):
    """`_MaskProperty` if a `Property` object on two `MaskT` objects.
    
    Typical example is the spacing or overlap between two masks.
    """
    def __init__(self, *, mask1: "MaskT", mask2: "MaskT", name: str, commutative: bool):
        if commutative:
            supername = f"{name}({mask1.name},{mask2.name})"
        else:
            supername = f"{mask1.name}.{name}({mask2.name})"
        super().__init__(name=supername)

        self.mask1 = mask1
        self.mask2 = mask2
        self.prop_name = name


class _DualMaskEnclosureProperty(_prp._EnclosureProperty):
    """`_DualMaskProperty` is a `Property` object with on two `_Mask` objects with
    an `Enclosure` object as value."""
    def __init__(self, *, mask1: "MaskT", mask2: "MaskT", name: str):
        super().__init__(name=f"{mask1.name}.{name}({mask2.name})")

        self.mask1 = mask1
        self.mask2 = mask2
        self.prop_name = name


class _MultiMaskCondition(_rle._Condition):
    """_MultiMaskCondition is a `_Condition` object involving multiple masks.

    This class is a base class that needs to be subclassed with a `str` value given
    for the operation class variable. Implementation of the methods for this class
    are complete so defining a subclass that only sets this operation class variable
    should be enough.
    """
    operation: ClassVar[str]

    def __init__(self, *, mask: "MaskT", others: MultiT["MaskT"]):
        try:
            self.operation
        except AttributeError:
            raise AttributeError(
                f"class '{self.__class__.__name__}' does not provide operation class variable"
            )
        others = cast_MultiT(others)
        super().__init__(elements=(mask, others))
        self._elements: Tuple["MaskT", Tuple["MaskT"]]

        self._hash: Optional[int] = None

    @property
    def mask(self) -> "MaskT":
        return self._elements[0]
    @property
    def others(self) -> Tuple["MaskT"]:
        return self._elements[1]

    def __eq__(self, other: object) -> bool:
        if self.__class__ is not other.__class__:
            return False
        else:
            other = cast(_MultiMaskCondition, other)
            return set((self.mask, *self.others)) == set((other.mask, *other.others))

    def __hash__(self) -> int:
        if self._hash is None:
            self._hash = hash(tuple(sorted(m.name for m in (self.mask, *self.others))))
        return self._hash

    def __repr__(self) -> str:
        return "{}.{}({})".format(
            repr(self.mask), self.operation,
            ",".join(repr(mask) for mask in self.others),
        )


class _InsideCondition(_MultiMaskCondition):
    """`_MultiMaskCondition` true if the main `_Mask` is fully covered by
    any of the other masks"""
    operation = "is_inside"
class _OutsideCondition(_MultiMaskCondition):
    """`_MultiMaskCondition` true if the main `_Mask` is outside
    any of the other masks"""
    operation = "is_outside"


class _Mask(abc.ABC):
    """A `_Mask` object represents that can hold a collection of `_Shape` objects.
    The mask can be with original `_Shape` objects as designed by the user or be
    derived from the `_Shape` objects from other `_Mask` objects.

    Each `_Mask` object has default properties defined that can be accessed as
    attributes of the `_Mask` object. The default properties are:
    - `width`
    - `length`
    - `space`
    - `area`
    - `density`

    TODO: Define exact meaning of width/length
    """
    @abc.abstractmethod
    def __init__(self, *, name: str):
        self.name = name
        self.width: _prp.PropertyT = _MaskProperty(mask=self, name="width")
        self.length: _prp.PropertyT = _MaskProperty(mask=self, name="length")
        self.space: _prp.PropertyT = _MaskProperty(mask=self, name="space")
        self.area: _prp.PropertyT = _MaskProperty(mask=self, name="area")
        self.density: _prp.PropertyT = _MaskProperty(mask=self, name="density")

    def __repr__(self) -> str:
        return self.name

    def extend_over(self, other: "MaskT") -> _prp.PropertyT:
        """Returns a `Property` object representing the extension of one
        the shapes on one `MaskT` object over the shapes on another `MaskT`
        object.
        """
        return _DualMaskProperty(
            mask1=self, mask2=other, name="extend_over", commutative=False,
        )

    def enclosed_by(self, other: "MaskT") -> _prp.EnclosurePropertyT:
        """Returns a `EnclosureProperty` object representing the enclosure of one
        the shapes on one `MaskT` object by the shapes on another `MaskT`
        object.
        """
        return _DualMaskEnclosureProperty(mask1=self, mask2=other, name="enclosed_by")

    def is_inside(self, other: MultiT["MaskT"], *others: "MaskT") -> _rle.ConditionT:
        """Returns a `_Condition` object representing wether all the shapes on a 'MaskT`
        object are inside one of the other `MaskT` objects.
        """
        masks = (*cast_MultiT(other), *others)

        return _InsideCondition(mask=self, others=masks)

    def is_outside(self, other: MultiT["MaskT"], *others: "MaskT") -> _rle.ConditionT:
        """Returns a `_Condition` object representing wether all the shapes on a 'MaskT`
        object are outside any of the other `MaskT` objects.
        """
        masks = (*cast_MultiT(other), *others)

        return _OutsideCondition(mask=self, others=masks)

    def parts_with(self, condition: MultiT[_prp.ComparisonT]) -> "MaskT":
        """Returns a derived `MaskT` representing the parts of the shapes on
        the `MaskT` object that fulfill the given condition(s).  
        The condition may only use properties from the same mask on which one
        calls the `parts_with` method.

        Example: `small = mask.parts_with(mask.width <= 1.0)`
        """
        return _PartsWith(mask=self, condition=condition)

    def remove(self, what: "MaskT") -> "MaskT":
        """Returns a derived `MaskT` representing the parts of the shapes on
        the `MaskT` that don't overlap with shapes of the other `MaskT` object.
        """
        return _MaskRemove(from_=self, what=what)

    def alias(self, name: str) -> "RuleMaskT":
        """Returns a derived `MaskT` given an alias for another `MaskT` object.
        The return object is also a `_Rule` object in order for scripts that
        are generated from rules can define a variable representing the
        `MaskT` that has been aliased. Typically the variable name will be
        the alias name and further on in generated rules then this variable
        will be used where this alias is used in other derived `MaskT` object
        or properties.
        """
        return _MaskAlias(name=name, mask=self)

    @property
    def same_net(self) -> "MaskT":
        """Returns a derived `MaskT` representing the shapes on a `MaskT` that
        are on the same net. It's thus connectivity related as defined by the
        `Connect` object.  
        The derived mask actually is a collection of separate masks for each
        net that has shapes on this mask. Supporting this kind of mask in
        generated rules may this be non-trivial.

        Typical use of this mask is to allow shape on the same net being put
        closer together than shapes on a different net.
        """
        return _SameNet(mask=self)

    @abc.abstractproperty
    def designmasks(self) -> Iterable["DesignMask"]: # pragma: no cover
        """The designasks property of a `MaskT` object gives a list of
        all designamsks used in a `MaskT`.

        API Notes:
            * The returned Iterable may contain same `DesignMask` object multiple
              times. User who need a unique set can use a `set` object for that.
        """
        ...

    @abc.abstractmethod
    def __eq__(self, other: object) -> bool: # pragma: no cover
        ...

    # When subclasses need to define __eq__ they also need to define
    # __hash__(); otherwise the subclass is considered to not be hashable
    @abc.abstractmethod
    def __hash__(self) -> int:
        # Assume mask names are different so will also give different hash
        return hash(self.name)
MaskT = _Mask


class _RuleMask(_Mask, _rle._Rule):
    "A `MaskT` object that is also a `RuleT` object"
    pass
RuleMaskT = _RuleMask


[docs]class DesignMask(_RuleMask): """A `DesignMask` object is a `_Mask` object with the shapes on the mask provided by shapes by the user. It is not a derived mask. Arguments: name: the name of the mask """ def __init__(self, *, name: str): super().__init__(name=name) self.grid: _prp.PropertyT = _MaskProperty(mask=self, name="grid") def __repr__(self) -> str: return f"design({self.name})" @property def designmasks(self) -> Iterable["DesignMask"]: return (self,) def __eq__(self, other: object) -> bool: return ( isinstance(other, DesignMask) and (self.name == other.name) ) def __hash__(self) -> int: return super().__hash__()
class _PartsWith(_Mask): """`_Mask.parts_with()` support class""" def __init__(self, *, mask: MaskT, condition: MultiT[_prp.ComparisonT], ): self.mask = mask condition = cast_MultiT(condition) if not all( ( isinstance(cond.left, _MaskProperty) and cond.left.mask == mask ) for cond in condition ): raise TypeError( "condition has to a single or an iterable of condition on properties of mask '{}'".format( mask.name, )) self.condition = condition super().__init__(name="{}.parts_with({})".format( mask.name, ",".join(str(cond) for cond in condition), )) @property def designmasks(self) -> Iterable[DesignMask]: return self.mask.designmasks def __eq__(self, other: object) -> bool: if type(self) != type(other): return False else: other = cast(_PartsWith, other) return ( (self.mask == other.mask) and (self.condition == other.condition) ) def __hash__(self) -> int: return super().__hash__()
[docs]class Join(_Mask): """A derived `_Mask` object that represenet the shapes resulting of joining all the shapes of the provided masks. """ def __init__(self, masks: MultiT[MaskT]): self.masks = masks = cast_MultiT(masks) super().__init__(name="join({})".format(",".join(mask.name for mask in masks))) self._hash: Optional[int] = None @property def designmasks(self) -> Iterable[DesignMask]: for mask in self.masks: yield from mask.designmasks def __eq__(self, other: object) -> bool: if self.__class__ is not other.__class__: return False else: other = cast("Join", other) return set(self.masks) == set(other.masks) def __hash__(self) -> int: if self._hash is None: # Convert designmasks to a set to remove duplicates # Then compute hash on sorted mask names self._hash = hash(tuple(sorted( m.name for m in set(self.designmasks) ))) return self._hash
[docs]class Intersect(_Mask): """A derived `_Mask` object that represenet the shapes resulting of the intersection of all the shapes of the provided masks. """ def __init__(self, masks: MultiT[MaskT]): self.masks = masks = cast_MultiT(masks) super().__init__(name="intersect({})".format(",".join(mask.name for mask in masks))) self._hash: Optional[int] = None @property def designmasks(self) -> Iterable[DesignMask]: for mask in self.masks: yield from mask.designmasks def __eq__(self, other: object) -> bool: if self.__class__ is not other.__class__: return False else: other = cast("Intersect", other) return set(self.masks) == set(other.masks) def __hash__(self) -> int: if self._hash is None: self._hash = hash(tuple(sorted( set(self.designmasks), key=(lambda m: m.name), ))) return self._hash
class _MaskRemove(_Mask): """`_Mask.remove()` support class""" def __init__(self, *, from_: MaskT, what: MaskT): super().__init__(name=f"{from_.name}.remove({what.name})") self.from_ = from_ self.what = what @property def designmasks(self) -> Iterable[DesignMask]: for mask in (self.from_, self.what): yield from mask.designmasks def __eq__(self, other: object) -> bool: if self.__class__ is not other.__class__: return False else: other = cast(_MaskRemove, other) return ( (self.from_ == other.from_) and (self.what == other.what) ) def __hash__(self) -> int: return hash((self.from_, self.what)) class _MaskAlias(_RuleMask): """`_Mask.alias()` support class""" def __init__(self, *, name: str, mask: MaskT): self.mask = mask super().__init__(name=name) def __repr__(self) -> str: return f"{self.mask.name}.alias({self.name})" @property def designmasks(self) -> Iterable[DesignMask]: return self.mask.designmasks def __eq__(self, other: object) -> bool: if self.__class__ is not other.__class__: return False else: other = cast(_MaskAlias, other) return ( (self.name == other.name) and (self.mask == other.mask) ) def __hash__(self) -> int: return super().__hash__() class Spacing(_DualMaskProperty): """A `Spacing` object is a `Property` that represents the spacing between two shapes on two different masks. The masks may not be the same. For the space between shapes on the same mask use the `MaskT.space` property. """ def __init__(self, mask1: MaskT, mask2: MaskT): if mask1 == mask2: raise ValueError( f"mask1 and mask2 may not be the same for 'Spacing'\n" "use `MaskT.space` property for that" ) super().__init__(mask1=mask1, mask2=mask2, name="space", commutative=True) class Connect(_rle._Rule): """A `Connect` object is a `_Rule` indicating that overlapping shapes on two different layers are connecting with each other. This rule is base to determine connectivity and which shapes are on the same net. The 'Connect` rules is not associative. For example a `Via` connects to the bottom and top layer(s) but the bottom and top layer(s) typically don't connect to each other directly. The `Connect` rule is commutative. Meaning that exchanging `mask1` and `mask2` arguments results in the same `Connect` rule. A `Connect` object is created by specifying to mask arguments `mask1` and `mask2`. Each of them can be one or more masks. The `Connect` rule then specifies that each shape on one of the masks in `mask1` that overlaps with a shape on one of the masks from `mask2` is connecting to it. """ def __init__(self, mask1: MultiT[MaskT], mask2: MultiT[MaskT], ): self.mask1 = mask1 = cast_MultiT(mask1) self.mask2 = mask2 = cast_MultiT(mask2) self._hash: Optional[int] = None def __eq__(self, other: object) -> bool: if self.__class__ is not other.__class__: return False else: other = cast("Connect", other) masks1 = set(self.mask1) masks2 = set(self.mask2) others1 = set(other.mask1) others2 = set(other.mask2) return ( ((masks1 == others1) and (masks2 == others2)) or ((masks1 == others2) and (masks2 == others1)) ) def __hash__(self) -> int: if self._hash is None: self._hash = hash(tuple(sorted(m.name for m in (*self.mask1, *self.mask2)))) return self._hash def __repr__(self) -> str: s1 = self.mask1[0].name if len(self.mask1) == 1 else "({})".format( ",".join(m.name for m in self.mask1) ) s2 = self.mask2[0].name if len(self.mask2) == 1 else "({})".format( ",".join(m.name for m in self.mask2) ) return f"connect({s1},{s2})" class _SameNet(_Mask): """`_Mask.same_net()` support class""" def __init__(self, mask: MaskT): self.mask = mask super().__init__(name=f"same_net({mask.name})") @property def designmasks(self) -> Iterable[DesignMask]: return self.mask.designmasks def __eq__(self, other: object) -> bool: if self.__class__ is not other.__class__: return False else: other = cast(_SameNet, other) return self.mask == other.mask def __hash__(self) -> int: return super().__hash__()