/ pdkmaster / technology / mask.py
mask.py
  1  # SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-or-later OR CERN-OHL-S-2.0+ OR Apache-2.0
  2  import abc
  3  from typing import Tuple, Iterable, Optional, ClassVar, cast
  4  
  5  from ..typing import MultiT, cast_MultiT
  6  from . import rule as _rle, property_ as _prp
  7  
  8  
  9  __all__ = ["MaskT", "RuleMaskT", "DesignMask", "Join", "Intersect"]
 10  
 11  
 12  class _MaskProperty(_prp._Property):
 13      """`_MaskProperty` is a `Property` object on a single `MaskT` object.
 14      
 15      Typical examples are width, area, density etc.
 16      """
 17      def __init__(self, *, mask: "MaskT", name: str):
 18          super().__init__(name=(mask.name + "." + name))
 19          self.mask = mask
 20          self.prop_name = name
 21  
 22  
 23  class _DualMaskProperty(_prp._Property):
 24      """`_MaskProperty` if a `Property` object on two `MaskT` objects.
 25      
 26      Typical example is the spacing or overlap between two masks.
 27      """
 28      def __init__(self, *, mask1: "MaskT", mask2: "MaskT", name: str, commutative: bool):
 29          if commutative:
 30              supername = f"{name}({mask1.name},{mask2.name})"
 31          else:
 32              supername = f"{mask1.name}.{name}({mask2.name})"
 33          super().__init__(name=supername)
 34  
 35          self.mask1 = mask1
 36          self.mask2 = mask2
 37          self.prop_name = name
 38  
 39  
 40  class _DualMaskEnclosureProperty(_prp._EnclosureProperty):
 41      """`_DualMaskProperty` is a `Property` object with on two `_Mask` objects with
 42      an `Enclosure` object as value."""
 43      def __init__(self, *, mask1: "MaskT", mask2: "MaskT", name: str):
 44          super().__init__(name=f"{mask1.name}.{name}({mask2.name})")
 45  
 46          self.mask1 = mask1
 47          self.mask2 = mask2
 48          self.prop_name = name
 49  
 50  
 51  class _MultiMaskCondition(_rle._Condition):
 52      """_MultiMaskCondition is a `_Condition` object involving multiple masks.
 53  
 54      This class is a base class that needs to be subclassed with a `str` value given
 55      for the operation class variable. Implementation of the methods for this class
 56      are complete so defining a subclass that only sets this operation class variable
 57      should be enough.
 58      """
 59      operation: ClassVar[str]
 60  
 61      def __init__(self, *, mask: "MaskT", others: MultiT["MaskT"]):
 62          try:
 63              self.operation
 64          except AttributeError:
 65              raise AttributeError(
 66                  f"class '{self.__class__.__name__}' does not provide operation class variable"
 67              )
 68          others = cast_MultiT(others)
 69          super().__init__(elements=(mask, others))
 70          self._elements: Tuple["MaskT", Tuple["MaskT"]]
 71  
 72          self._hash: Optional[int] = None
 73  
 74      @property
 75      def mask(self) -> "MaskT":
 76          return self._elements[0]
 77      @property
 78      def others(self) -> Tuple["MaskT"]:
 79          return self._elements[1]
 80  
 81      def __eq__(self, other: object) -> bool:
 82          if self.__class__ is not other.__class__:
 83              return False
 84          else:
 85              other = cast(_MultiMaskCondition, other)
 86              return set((self.mask, *self.others)) == set((other.mask, *other.others))
 87  
 88      def __hash__(self) -> int:
 89          if self._hash is None:
 90              self._hash = hash(tuple(sorted(m.name for m in (self.mask, *self.others))))
 91          return self._hash
 92  
 93      def __repr__(self) -> str:
 94          return "{}.{}({})".format(
 95              repr(self.mask), self.operation,
 96              ",".join(repr(mask) for mask in self.others),
 97          )
 98  
 99  
100  class _InsideCondition(_MultiMaskCondition):
101      """`_MultiMaskCondition` true if the main `_Mask` is fully covered by
102      any of the other masks"""
103      operation = "is_inside"
104  class _OutsideCondition(_MultiMaskCondition):
105      """`_MultiMaskCondition` true if the main `_Mask` is outside
106      any of the other masks"""
107      operation = "is_outside"
108  
109  
110  class _Mask(abc.ABC):
111      """A `_Mask` object represents that can hold a collection of `_Shape` objects.
112      The mask can be with original `_Shape` objects as designed by the user or be
113      derived from the `_Shape` objects from other `_Mask` objects.
114  
115      Each `_Mask` object has default properties defined that can be accessed as
116      attributes of the `_Mask` object. The default properties are:
117      - `width`
118      - `length`
119      - `space`
120      - `area`
121      - `density`
122  
123      TODO: Define exact meaning of width/length
124      """
125      @abc.abstractmethod
126      def __init__(self, *, name: str):
127          self.name = name
128          self.width: _prp.PropertyT = _MaskProperty(mask=self, name="width")
129          self.length: _prp.PropertyT = _MaskProperty(mask=self, name="length")
130          self.space: _prp.PropertyT = _MaskProperty(mask=self, name="space")
131          self.area: _prp.PropertyT = _MaskProperty(mask=self, name="area")
132          self.density: _prp.PropertyT = _MaskProperty(mask=self, name="density")
133  
134      def __repr__(self) -> str:
135          return self.name
136  
137      def extend_over(self, other: "MaskT") -> _prp.PropertyT:
138          """Returns a `Property` object representing the extension of one
139          the shapes on one `MaskT` object over the shapes on another `MaskT`
140          object.
141          """
142          return _DualMaskProperty(
143              mask1=self, mask2=other, name="extend_over", commutative=False,
144          )
145  
146      def enclosed_by(self, other: "MaskT") -> _prp.EnclosurePropertyT:
147          """Returns a `EnclosureProperty` object representing the enclosure of one
148          the shapes on one `MaskT` object by the shapes on another `MaskT`
149          object.
150          """
151          return _DualMaskEnclosureProperty(mask1=self, mask2=other, name="enclosed_by")
152  
153      def is_inside(self, other: MultiT["MaskT"], *others: "MaskT") -> _rle.ConditionT:
154          """Returns a `_Condition` object representing wether all the shapes on a 'MaskT`
155          object are inside one of the other `MaskT` objects.
156          """
157          masks = (*cast_MultiT(other), *others)
158  
159          return _InsideCondition(mask=self, others=masks)
160  
161      def is_outside(self, other: MultiT["MaskT"], *others: "MaskT") -> _rle.ConditionT:
162          """Returns a `_Condition` object representing wether all the shapes on a 'MaskT`
163          object are outside any of the other `MaskT` objects.
164          """
165          masks = (*cast_MultiT(other), *others)
166  
167          return _OutsideCondition(mask=self, others=masks)
168  
169      def parts_with(self, condition: MultiT[_prp.ComparisonT]) -> "MaskT":
170          """Returns a derived `MaskT` representing the parts of the shapes on
171          the `MaskT` object that fulfill the given condition(s).  
172          The condition may only use properties from the same mask on which one
173          calls the `parts_with` method.
174  
175          Example: `small = mask.parts_with(mask.width <= 1.0)`
176          """
177          return _PartsWith(mask=self, condition=condition)
178  
179      def remove(self, what: MultiT["MaskT"], *args: "MaskT") -> "MaskT":
180          """Returns a derived `MaskT` representing the parts of the shapes on
181          the `MaskT` that don't overlap with shapes of the other `MaskT` object.
182          """
183          what = (*cast_MultiT(what), *args)
184          if len(what) == 0:
185              raise ValueError("No mask given")
186          elif len(what) == 1:
187              mask = what[0]
188          else:
189              mask = Join(what)
190          return _MaskRemove(from_=self, what=mask)
191  
192      def alias(self, name: str) -> "RuleMaskT":
193          """Returns a derived `MaskT` given an alias for another `MaskT` object.
194          The return object is also a `_Rule` object in order for scripts that
195          are generated from rules can define a variable representing the
196          `MaskT` that has been aliased. Typically the variable name will be
197          the alias name and further on in generated rules then this variable
198          will be used where this alias is used in other derived `MaskT` object
199          or properties.
200          """
201          return _MaskAlias(name=name, mask=self)
202  
203      @property
204      def same_net(self) -> "MaskT":
205          """Returns a derived `MaskT` representing the shapes on a `MaskT` that
206          are on the same net. It's thus connectivity related as defined by the
207          `Connect` object.  
208          The derived mask actually is a collection of separate masks for each
209          net that has shapes on this mask. Supporting this kind of mask in
210          generated rules may this be non-trivial.
211  
212          Typical use of this mask is to allow shape on the same net being put
213          closer together than shapes on a different net.
214          """
215          return _SameNet(mask=self)
216  
217      @abc.abstractproperty
218      def designmasks(self) -> Iterable["DesignMask"]: # pragma: no cover
219          """The designasks property of a `MaskT` object gives a list of
220          all designamsks used in a `MaskT`.
221  
222          API Notes:
223              * The returned Iterable may contain same `DesignMask` object multiple
224                times. User who need a unique set can use a `set` object for that.
225          """
226          ...
227  
228      @abc.abstractmethod
229      def __eq__(self, other: object) -> bool: # pragma: no cover
230          ...
231  
232      # When subclasses need to define __eq__ they also need to define
233      # __hash__(); otherwise the subclass is considered to not be hashable
234      @abc.abstractmethod
235      def __hash__(self) -> int:
236          # Assume mask names are different so will also give different hash
237          return hash(self.name)
238  MaskT = _Mask
239  
240  
241  class _RuleMask(_Mask, _rle._Rule):
242      "A `MaskT` object that is also a `RuleT` object"
243      pass
244  RuleMaskT = _RuleMask
245  
246  
247  class DesignMask(_RuleMask):
248      """A `DesignMask` object is a `_Mask` object with the shapes on the mask
249      provided by shapes by the user. It is not a derived mask.
250  
251      Arguments:
252          name: the name of the mask
253      """
254      def __init__(self, *, name: str):
255          super().__init__(name=name)
256  
257          self.grid: _prp.PropertyT = _MaskProperty(mask=self, name="grid")
258  
259      def __repr__(self) -> str:
260          return f"design({self.name})"
261  
262      @property
263      def designmasks(self) -> Iterable["DesignMask"]:
264          return (self,)
265  
266      def __eq__(self, other: object) -> bool:
267          return (
268              isinstance(other, DesignMask)
269              and (self.name == other.name)
270          )
271  
272      def __hash__(self) -> int:
273          return super().__hash__()
274  
275  
276  class _PartsWith(_Mask):
277      """`_Mask.parts_with()` support class"""
278      def __init__(self, *,
279          mask: MaskT, condition: MultiT[_prp.ComparisonT],
280      ):
281          self.mask = mask
282  
283          condition = cast_MultiT(condition)
284          if not all(
285              (
286                  isinstance(cond.left, _MaskProperty)
287                  and cond.left.mask == mask
288              ) for cond in condition
289          ):
290              raise TypeError(
291                  "condition has to a single or an iterable of condition on properties of mask '{}'".format(
292                      mask.name,
293                  ))
294          self.condition = condition
295  
296          super().__init__(name="{}.parts_with({})".format(
297              mask.name, ",".join(str(cond) for cond in condition),
298          ))
299  
300      @property
301      def designmasks(self) -> Iterable[DesignMask]:
302          return self.mask.designmasks
303  
304      def __eq__(self, other: object) -> bool:
305          if type(self) != type(other):
306              return False
307          else:
308              other = cast(_PartsWith, other)
309              return (
310                  (self.mask == other.mask)
311                  and (self.condition == other.condition)
312              )
313  
314      def __hash__(self) -> int:
315          return super().__hash__()
316  
317  
318  class Join(_Mask):
319      """A derived `_Mask` object that represenet the shapes resulting of joining
320      all the shapes of the provided masks.
321      """
322      def __init__(self, masks: MultiT[MaskT], *args: MaskT):
323          self.masks = masks = (*cast_MultiT(masks), *args)
324  
325          super().__init__(name="join({})".format(",".join(mask.name for mask in masks)))
326  
327          self._hash: Optional[int] = None
328  
329      @property
330      def designmasks(self) -> Iterable[DesignMask]:
331          for mask in self.masks:
332              yield from mask.designmasks
333  
334      def __eq__(self, other: object) -> bool:
335          if self.__class__ is not other.__class__:
336              return False
337          else:
338              other = cast("Join", other)
339              return set(self.masks) == set(other.masks)
340  
341      def __hash__(self) -> int:
342          if self._hash is None:
343              # Convert designmasks to a set to remove duplicates
344              # Then compute hash on sorted mask names
345              self._hash = hash(tuple(sorted(
346                  m.name for m in set(self.designmasks)
347              )))
348          return self._hash
349  
350  
351  class Intersect(_Mask):
352      """A derived `_Mask` object that represenet the shapes resulting of
353      the intersection of all the shapes of the provided masks.
354      """
355      def __init__(self, masks: MultiT[MaskT]):
356          self.masks = masks = cast_MultiT(masks)
357  
358          super().__init__(name="intersect({})".format(",".join(mask.name for mask in masks)))
359  
360          self._hash: Optional[int] = None
361  
362      @property
363      def designmasks(self) -> Iterable[DesignMask]:
364          for mask in self.masks:
365              yield from mask.designmasks
366  
367      def __eq__(self, other: object) -> bool:
368          if self.__class__ is not other.__class__:
369              return False
370          else:
371              other = cast("Intersect", other)
372              return set(self.masks) == set(other.masks)
373  
374      def __hash__(self) -> int:
375          if self._hash is None:
376              self._hash = hash(tuple(sorted(
377                  set(self.designmasks),
378                  key=(lambda m: m.name),
379              )))
380          return self._hash
381  
382  
383  class _MaskRemove(_Mask):
384      """`_Mask.remove()` support class"""
385      def __init__(self, *, from_: MaskT, what: MaskT):
386          super().__init__(name=f"{from_.name}.remove({what.name})")
387          self.from_ = from_
388          self.what = what
389  
390      @property
391      def designmasks(self) -> Iterable[DesignMask]:
392          for mask in (self.from_, self.what):
393              yield from mask.designmasks
394  
395      def __eq__(self, other: object) -> bool:
396          if self.__class__ is not other.__class__:
397              return False
398          else:
399              other = cast(_MaskRemove, other)
400              return (
401                  (self.from_ == other.from_)
402                  and (self.what == other.what)
403              )
404  
405      def __hash__(self) -> int:
406          return hash((self.from_, self.what))
407  
408  
409  class _MaskAlias(_RuleMask):
410      """`_Mask.alias()` support class"""
411      def __init__(self, *, name: str, mask: MaskT):
412          self.mask = mask
413  
414          super().__init__(name=name)
415  
416      def __repr__(self) -> str:
417          return f"{self.mask.name}.alias({self.name})"
418  
419      @property
420      def designmasks(self) -> Iterable[DesignMask]:
421          return self.mask.designmasks
422  
423      def __eq__(self, other: object) -> bool:
424          if self.__class__ is not other.__class__:
425              return False
426          else:
427              other = cast(_MaskAlias, other)
428              return (
429                  (self.name == other.name)
430                  and (self.mask == other.mask)
431              )
432  
433      def __hash__(self) -> int:
434          return super().__hash__()
435  
436  
437  class Spacing(_DualMaskProperty):
438      """A `Spacing` object is a `Property` that represents the spacing
439      between two shapes on two different masks.  
440      The masks may not be the same. For the space between shapes on the same
441      mask use the `MaskT.space` property.
442      """
443      def __init__(self, mask1: MaskT, mask2: MaskT):
444          if mask1 == mask2:
445              raise ValueError(
446                  f"mask1 and mask2 may not be the same for 'Spacing'\n"
447                  "use `MaskT.space` property for that"
448              )
449          super().__init__(mask1=mask1, mask2=mask2, name="space", commutative=True)
450  
451  
452  class Connect(_rle._Rule):
453      """A `Connect` object is a `_Rule` indicating that overlapping shapes on two
454      different layers are connecting with each other. This rule is base to determine
455      connectivity and which shapes are on the same net.
456  
457      The 'Connect` rules is not associative. For example a `Via` connects to the bottom
458      and top layer(s) but the bottom and top layer(s) typically don't connect to each
459      other directly.  
460      The `Connect` rule is commutative. Meaning that exchanging `mask1` and `mask2`
461      arguments results in the same `Connect` rule.
462  
463      A `Connect` object is created by specifying to mask arguments `mask1` and `mask2`.
464      Each of them can be one or more masks. The `Connect` rule then specifies that
465      each shape on one of the masks in `mask1` that overlaps with a shape on one of
466      the masks from `mask2` is connecting to it.
467      """
468      def __init__(self,
469          mask1: MultiT[MaskT], mask2: MultiT[MaskT],
470      ):
471          self.mask1 = mask1 = cast_MultiT(mask1)
472          self.mask2 = mask2 = cast_MultiT(mask2)
473  
474          self._hash: Optional[int] = None
475  
476      def __eq__(self, other: object) -> bool:
477          if self.__class__ is not other.__class__:
478              return False
479          else:
480              other = cast("Connect", other)
481  
482              masks1 = set(self.mask1)
483              masks2 = set(self.mask2)
484  
485              others1 = set(other.mask1)
486              others2 = set(other.mask2)
487  
488              return (
489                  ((masks1 == others1) and (masks2 == others2))
490                  or ((masks1 == others2) and (masks2 == others1))
491              )
492  
493      def __hash__(self) -> int:
494          if self._hash is None:
495              self._hash = hash(tuple(sorted(m.name for m in (*self.mask1, *self.mask2))))
496          return self._hash
497  
498      def __repr__(self) -> str:
499          s1 = self.mask1[0].name if len(self.mask1) == 1 else "({})".format(
500              ",".join(m.name for m in self.mask1)
501          )
502          s2 = self.mask2[0].name if len(self.mask2) == 1 else "({})".format(
503              ",".join(m.name for m in self.mask2)
504          )
505          return f"connect({s1},{s2})"
506  
507  
508  class _SameNet(_Mask):
509      """`_Mask.same_net()` support class"""
510      def __init__(self, mask: MaskT):
511          self.mask = mask
512  
513          super().__init__(name=f"same_net({mask.name})")
514  
515      @property
516      def designmasks(self) -> Iterable[DesignMask]:
517          return self.mask.designmasks
518  
519      def __eq__(self, other: object) -> bool:
520          if self.__class__ is not other.__class__:
521              return False
522          else:
523              other = cast(_SameNet, other)
524              return self.mask == other.mask
525  
526      def __hash__(self) -> int:
527          return super().__hash__()