Source code for pdkmaster.technology.technology_

# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-or-later OR CERN-OHL-S-2.0+ OR Apache-2.0
from math import floor, ceil
import abc
from typing import List, Dict, Optional, Union, Iterable, cast, overload

from . import (
    property_ as _prp, rule as _rle, mask as _msk, wafer_ as _wfr, geometry as _geo,
    primitive as _prm,
)


__all__ = ["Technology"]


[docs]class Technology(abc.ABC): """A `Technology` object is the representation of a semiconductor process. It's mainly a list of `_Primitive` objects with these primitives describing the capabilities of the technolgy. the `Technology` class is an abstract base class so subclasses need to implement some of the abstract methods defined in this base class. Subclasses need to overload the `__init__()` method and call the parent's `__init__()` with the list of the primitives for this technology. After this list has been passed to the super class's `__init__()` it is final and will be frozen. Next to the `__init__()` abstract method subclasses also need to define some abstract properties to define some properties of the technology. These are `name()`, `grid()`. """
[docs] class ConnectionError(Exception): pass
class _ComputedSpecs: """`Technology._ComputedSpecs` is a helper class that allow to compute some properties on a technology. This class should not be instantiated by user code by is access through the `computed` attribute of a `Technology` object. """ def __init__(self, tech: "Technology"): self.tech = tech @overload def min_space(self, primitive1: "_prm.MaskPrimitiveT", primitive2: None=None, *, max_enclosure: bool=False, ) -> float: ... # pragma: no cover @overload def min_space(self, primitive1: "_prm.PrimitiveT", primitive2: "_prm.PrimitiveT", *, max_enclosure: bool=False, ) -> float: ... # pragma: no cover def min_space(self, primitive1: "_prm.PrimitiveT", primitive2: Optional["_prm.PrimitiveT"]=None, *, max_enclosure: bool=False, ) -> float: """Compute the minimum space between one or two primitives. It will go over all the rules to determine the minimum space for the provided primitive. This function allows to compute the min_space on primitives that are derived from other primitives. It is also usable to derive more complex minimum spacing on WaferWire primitives. For exmaple, you can do `tech.computed.min_space(active.in_(nimplant), active.in_(pimplant))` the value is computed so that none of the implants drawn on the first waferwire is not overlapping with the one on the second. The enclosing layer is assumed to be drawn with minimal enclosure. Arguments: primitive1: the first primitive if primitive2 is not given, the minimum space between shapes on the primitive1 layer is returned. primitive2: the optional second primitive. if specified the minimum space between shape on layer primitive1 and shapes on layer primitive2 is returned. """ def check_prim(prim: _prm.Spacing) -> bool: if primitive2 is None: return ( (primitive1 in prim.primitives1) and (prim.primitives2 is None) ) elif prim.primitives2 is not None: return ( ( (primitive1 in prim.primitives1) and (primitive2 in prim.primitives2) ) or ( (primitive1 in prim.primitives2) and (primitive2 in prim.primitives1) ) ) else: return False spaces = list( p.min_space for p in filter( check_prim, self.tech.primitives.__iter_type__(_prm.Spacing), ) ) if (primitive2 is None) or (primitive1 == primitive2): try: space = cast(_prm.WidthSpacePrimitiveT, primitive1).min_space except AttributeError: # pragma: no cover pass else: spaces.append(space) if isinstance(primitive1, _prm.conductors._WaferWireIntersect): spaces.append(primitive1.waferwire.min_space) if ( isinstance(primitive1, _prm.conductors._WaferWireIntersect) and isinstance(primitive2, _prm.WaferWire) ): (primitive1, primitive2) = (primitive2, primitive1) def get_enc(*, ww: _prm.WaferWire, prim: _prm.MaskPrimitiveT): enc = None if prim in ww2.implant: idx = ww2.implant.index(prim) enc = ww2.min_implant_enclosure[idx] elif prim in ww2.well: idx = ww2.well.index(prim) enc = ww2.min_well_enclosure[idx] elif prim in ww2.oxide: idx = ww2.oxide.index(prim) enc = ww2.min_oxide_enclosure[idx] else: # pragma: no cover raise RuntimeError( "Internal error: unsupported enclosed layer" f" '{prim.name}' for '{ww2.name}'") return ( None if enc is None else enc.min() if not max_enclosure else enc.max() ) if isinstance(primitive1, _prm.WaferWire): if isinstance(primitive2, _prm.conductors._WaferWireIntersect): ww2 = primitive2.waferwire try: s = self.min_space(primitive1, ww2) except: # pragma: no cover pass else: spaces.append(s) if primitive1 == ww2: for prim in primitive2.prim: enc = get_enc(ww=ww2, prim=prim) if enc is not None: try: s = self.min_space(primitive1, prim) except: pass else: spaces.append(enc + s) elif ( isinstance(primitive1, _prm.conductors._WaferWireIntersect) and isinstance(primitive2, _prm.conductors._WaferWireIntersect) ): ww1 = primitive1.waferwire ww2 = primitive2.waferwire spaces.extend(( self.min_space(ww1, primitive2), self.min_space(ww2, primitive1), )) e1s: List[float] = [] for prim in primitive1.prim: enc = get_enc(ww=ww1, prim=prim) if enc is not None: e1s.append(enc) e2s: List[float] = [] for prim in primitive2.prim: enc = get_enc(ww=ww2, prim=prim) if enc is not None: e2s.append(enc) if (len(e1s) > 0) and (len(e2s) > 0): spaces.append(max(e1s) + max(e2s)) try: return max(spaces) except ValueError: raise AttributeError( f"min_space between {primitive1} and {primitive2} not found", ) def min_width(self, primitive: "_prm.WidthSpacePrimitiveT", *, up: bool=False, down: bool=False, min_enclosure: bool=False, ) -> float: """Compute the minimum width of a primitive. This method allows to take into account via enclosure rules to compute minimum width but still be contacted through a via. Arguments: primitive: the primitive up: wether it needs to take a connection from the top with a Via into account. down: wether it needs to take a connection from the bottom with a Via into account. min_enclosure: if True it will take the minimum value minimum value of the relevant via enclosure rule; otherwise the maximum value. """ assert primitive.min_width is not None, ( "primitive has to have the min_with attribute" ) def wupdown(via): if up and (primitive in via.bottom): idx = via.bottom.index(primitive) enc = via.min_bottom_enclosure[idx] w = via.width elif down and (primitive in via.top): idx = via.top.index(primitive) enc = via.min_top_enclosure[idx] w = via.width else: enc = _prp.Enclosure(0.0) w = 0.0 enc = enc.min() if min_enclosure else enc.max() return w + 2*enc return max(( primitive.min_width, *(wupdown(via) for via in self.tech.primitives.__iter_type__(_prm.Via)), )) def min_pitch(self, primitive: "_prm.WidthSpacePrimitiveT", *, up: bool=False, down: bool=False, min_enclosure: bool=False, ) -> float: """Compute the minimum pitch of a primitive. It's the minimum width plus the minimum space. Arguments: primitive: the primitive up: wether it needs to take a connection from the top with a Via into account. down: wether it needs to take a connection from the bottom with a Via into account. min_enclosure: if True it will take the minimum value minimum value of the relevant via enclosure rule; otherwise the maximum value. """ w = self.min_width(primitive, up=up, down=down, min_enclosure=min_enclosure) return w + primitive.min_space @property @abc.abstractmethod def name(self) -> str: """property with the name of the technology""" ... # pragma: no cover @property @abc.abstractmethod def grid(self) -> float: """property with the minimum grid of the technology Optionally primitives may define a bigger grid for their shapes. """ ... # pragma: no cover @abc.abstractmethod def __init__(self, *, primitives: "_prm.Primitives"): self._primitives = primitives # primitives needs to contain exectly one Base primitive which # is always called base try: base = primitives.base except: raise ValueError("A technology needs exactly one 'Base` primitive") else: if not isinstance(base, _prm.Base): raise ValueError("A technology needs exactly one 'Base` primitive") wells = tuple(self._primitives.__iter_type__(_prm.Well)) if not wells: self._substrate_prim = self.base else: self._substrate_prim = self.base.remove(wells).alias(f"substrate:{self.name}") self._build_interconnect() self._build_rules() primitives._freeze_() self.computed = self._ComputedSpecs(self)
[docs] def is_ongrid(self, v: float, mult: int=1) -> bool: """Returns wether a value is on grid or not. Arguments: w: value to check mult: value has to be on `mult*grid` """ g = mult*self.grid ng = round(v/g) return abs(v - ng*g) < _geo.epsilon
@overload def on_grid(self, dim: float, *, mult: int=1, rounding: str="nearest", ) -> float: ... # pragma: no cover @overload def on_grid(self, dim: _geo.Point, *, mult: int=1, rounding: str="nearest", ) -> _geo.Point: ... # pragma: no cover
[docs] def on_grid(self, dim: Union[float, _geo.Point], *, mult: int=1, rounding: str="nearest", ) -> Union[float, _geo.Point]: """Compute a value on grid from a given value. Arguments: dim: value to put on grid mult: value will be put on `mult*grid` rounding: how to round the value Has to be one of "floor", "nearest", "ceiling"; "nearest" is the default. """ if isinstance(dim, _geo.Point): return _geo.Point( x=self.on_grid(dim.x, mult=mult, rounding=rounding), y=self.on_grid(dim.y, mult=mult, rounding=rounding), ) if rounding == "floor": dim += _geo.epsilon elif rounding == "ceiling": dim -= _geo.epsilon else: if rounding != "nearest": raise ValueError( "rounding has to be one of ('floor', 'nearest', 'ceiling') " f"not '{rounding}'" ) flookup = {"nearest": round, "floor": floor, "ceiling": ceil} try: f = flookup[rounding] except KeyError: # pragma: no cover raise RuntimeError(f"Not implemeted: rounding '{rounding}'") return f(dim/(mult*self.grid))*mult*self.grid
@property def base(self) -> "_prm.Base": return cast(_prm.Base, self.primitives.base) @property def dbu(self) -> float: """Returns database unit compatible with technology grid. An exception is raised if the technology grid is not a multuple of 10pm. This method is specifically for use to export to format that use the dbu. """ igrid = int(round(1e6*self.grid)) assert (igrid%10) == 0 if (igrid%100) != 0: return 1e-5 elif (igrid%1000) != 0: return 1e-4 else: return 1e-3 def _build_interconnect(self) -> None: prims = self._primitives neworder = [] def add_prims(prims2): for prim in prims2: idx = prims.index(prim) if idx not in neworder: neworder.append(idx) def get_name(prim): return prim.name # base add_prims((prims.base,)) # set that are build up when going over the primitives # bottomwires: primitives that still need to be bottomconnected by a via bottomwires = set() # implants: used implant not added yet implants = set() # Implants to add markers = set() # Markers to add # the wells, fixed deepwells = set(prims.__iter_type__(_prm.DeepWell)) wells = set(prims.__iter_type__(_prm.Well)) # Wells are the first primitives in line add_prims(sorted(deepwells, key=get_name)) add_prims(sorted(wells, key=get_name)) # process waferwires waferwires = set(prims.__iter_type__(_prm.WaferWire)) bottomwires.update(waferwires) # They also need to be connected conn_wells = set() for wire in waferwires: implants.update((*wire.implant, *wire.well)) conn_wells.update(wire.well) if conn_wells != wells: raise _prm.UnconnectedPrimitiveError(primitive=(wells - conn_wells).pop()) # process gatewires bottomwires.update(prims.__iter_type__(_prm.GateWire)) # Already add implants that are used in the waferwires add_prims(sorted(implants, key=get_name)) implants = set() # Add the oxides for ww in waferwires: add_prims(sorted(ww.oxide, key=get_name)) # process vias vias = set(prims.__iter_type__(_prm.Via)) def allwires(wire): if isinstance(wire, _prm.Resistor): yield from allwires(wire.wire) yield from wire.indicator try: yield wire.pin # type: ignore except AttributeError: pass try: yield wire.blockage # type: ignore except AttributeError: pass yield wire connvias = set(filter(lambda via: any(w in via.bottom for w in bottomwires), vias)) if connvias: viatops = set() while connvias: viabottoms = set() viatops = set() for via in connvias: viabottoms.update(via.bottom) viatops.update(via.top) noconn = tuple(filter( # MIMTop does not need to be connected from bottom lambda l: not isinstance(l, _prm.MIMTop), bottomwires - viabottoms, )) if noconn: raise Technology.ConnectionError( f"wires ({', '.join(wire.name for wire in noconn)})" " not in bottom list of any via" ) for bottom in viabottoms: add_prims(allwires(bottom)) bottomwires -= viabottoms bottomwires.update(viatops) vias -= connvias connvias = set(filter(lambda via: any(w in via.bottom for w in bottomwires), vias)) # Add the top layers of last via to the prims for top in viatops: add_prims(allwires(top)) if vias: raise Technology.ConnectionError( f"vias ({', '.join(via.name for via in vias)}) have no connection to" " a technology bottom wire" ) # Add via and it's blockage layers vias = tuple(prims.__iter_type__(_prm.Via)) add_prims(prim.blockage for prim in filter( lambda v: hasattr(v, "blockage"), vias )) # Now add all vias add_prims(vias) # process mosfets mosfets = set(prims.__iter_type__(_prm.MOSFET)) gates = {mosfet.gate for mosfet in mosfets} allgates = set(prims.__iter_type__(_prm.MOSFETGate)) if gates != allgates: diff = allgates - gates if diff: raise _prm.UnusedPrimitiveError(primitive=diff.pop()) raise RuntimeError("Unhandled error condition") # pragma: no cover actives = {gate.active for gate in gates} polys = {gate.poly for gate in gates} for mosfet in mosfets: implants.update(mosfet.implant) if mosfet.well is not None: implants.add(mosfet.well) if mosfet.gate.inside is not None: markers.update(mosfet.gate.inside) # Each well and the substrate may either contain only transistors without # n/p type implants or all with n/p type implants # store first mosfet in a well in wellmosfet, check nect mosfets if they match wellmosfet: Dict[Optional[_prm.Well], _prm.MOSFET] = {} for mos in mosfets: well = mos.well if well not in wellmosfet: wellmosfet[well] = mos else: prevmos = wellmosfet[well] if prevmos.has_typeimplant != mos.has_typeimplant: name = f"same well '{well.name}'" if well is not None else "substrate" raise ValueError( f"MOSFETs '{prevmos.name}' and '{mos.name}' with and without type implant" f" in {name}" ) add_prims(( *sorted(implants, key=get_name), *sorted(actives, key=get_name), *sorted(polys, key=get_name), *sorted(markers, key=get_name), *sorted(gates, key=get_name), *sorted(mosfets, key=get_name), )) implants = set() markers = set() # proces pad openings padopenings = set(prims.__iter_type__(_prm.PadOpening)) viabottoms = set() for padopening in padopenings: add_prims(allwires(padopening.bottom)) add_prims(padopenings) # process top metal wires add_prims(prims.__iter_type__(_prm.TopMetalWire)) # process resistors resistors = set(prims.__iter_type__(_prm.Resistor)) for resistor in resistors: markers.update(resistor.indicator) implants.update(resistor.implant) # process capacitors mimtops = set(prims.__iter_type__(_prm.MIMTop)) mimcaps = tuple(prims.__iter_type__(_prm.MIMCapacitor)) usedtops = set(c.top for c in mimcaps) unusedtops = mimtops - usedtops if unusedtops: s_tops = ",".join(f"'{top}.name'" for top in unusedtops) raise _prm.UnusedPrimitiveError( primitive=unusedtops.pop(), msg=f"MIMTops {s_tops} not used in MIMCapacitor", ) # process diodes diodes = set(prims.__iter_type__(_prm.Diode)) for diode in diodes: markers.update(diode.indicator) # process bipolars bipolars = set(prims.__iter_type__(_prm.Bipolar)) for bipolar in bipolars: markers.update(bipolar.indicator) # extra rules rules = set(prims.__iter_type__(_prm.RulePrimitiveT)) add_prims((*implants, *markers, *resistors, *mimcaps, *diodes, *bipolars, *rules)) # process auxiliary def aux_key(aux: _prm.Auxiliary) -> str: return aux.name add_prims(sorted(prims.__iter_type__(_prm.Auxiliary), key=aux_key)) # reorder primitives unused = set(range(len(prims))) - set(neworder) if unused: raise _prm.UnusedPrimitiveError(primitive=prims[unused.pop()]) prims._reorder_(neworder=neworder) def _build_rules(self) -> None: prims = self._primitives self._rules = rules = _rle.Rules() # grid rules += _wfr.wafer.grid == self.grid # Generate the rule but don't add them yet. for prim in prims: prim._derive_rules(self) # First add substrate alias if needed. This will only be clear # after the rules have been generated. sub_mask = self.substrate_prim.mask if isinstance(sub_mask, _msk._MaskAlias): self._rules += sub_mask if sub_mask != _wfr.wafer: self._rules += _msk.Connect(sub_mask, _wfr.wafer) # Now we can add the rules for prim in prims: self._rules += prim.rules rules._freeze_() @property def substrate_prim(self) -> "_prm.MaskPrimitiveT": """Property representing the substrate of the technology; it's defined as the area that is outside any of the wells of the technology. As this value needs access to the list of wells it's only available afcer the technology has been initialized and is not available during run of the `_init()` method. """ if not hasattr(self, "_substrate_prim"): raise AttributeError("substrate may not be accessed during object initialization") return self._substrate_prim @property def rules(self) -> Iterable[_rle.RuleT]: """Return all the rules that are derived from the primitives of the technology.""" return self._rules @property def primitives(self) -> "_prm.Primitives": """Return the primitives of the technology.""" return self._primitives @property def designmasks(self) -> Iterable[_msk.DesignMask]: """Return all the `DesignMask` objects defined by the primitives of the technology. The property makes sure there are no duplicates in the returned iterable. """ masks = set() for prim in self._primitives: for mask in prim.designmasks: if mask not in masks: yield mask masks.add(mask)