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__()