"""The native technology primitives"""
# SPDX-License-Identifier: GPL-2.0-or-later OR AGPL-3.0-or-later OR CERN-OHL-S-2.0+
from itertools import product, combinations, chain
import abc
from typing import (
Any, Generator, Iterable, Optional, List, Set, Dict, Tuple, Union, cast,
)
from ..typing import OptMultiT, cast_OptMultiT, SingleOrMulti, OptSingleOrMulti, IntFloat
from .. import _util
from . import (
rule as rle, property_ as prp, net as net_, mask as msk, wafer_ as wfr,
edge as edg, technology_ as tch,
)
__all__ = ["Marker", "Auxiliary", "ExtraProcess",
"Implant", "Well",
"Insulator", "WaferWire", "GateWire", "MetalWire", "TopMetalWire",
"Via", "PadOpening",
"Resistor", "Diode",
"MOSFETGate", "MOSFET", "Bipolar",
"Spacing",
"UnusedPrimitiveError", "UnconnectedPrimitiveError"]
class _Primitive(abc.ABC):
@abc.abstractmethod
def __init__(self, *, name: str):
self.name = name
self.ports = _PrimitivePorts()
self.params = _Params()
self._rules: Optional[Tuple[rle._Rule, ...]] = None
def __repr__(self):
cname = self.__class__.__name__.split(".")[-1]
return f"{cname}(name={self.name})"
def __eq__(self, other: object) -> bool:
"""Two primitives are the same if their name is the same"""
return (isinstance(other, _Primitive)) and (self.name == other.name)
def __hash__(self):
return hash(self.name)
@property
def rules(self) -> Tuple[rle._Rule, ...]:
if self._rules is None:
raise AttributeError(
"Internal error: accessing rules before they are generated",
)
return self._rules
@abc.abstractmethod
def _generate_rules(self, *,
tech: tch.Technology,
) -> Iterable[rle._Rule]:
return tuple()
def _derive_rules(self, tech: tch.Technology) -> None:
if self._rules is not None:
raise ValueError(
"Internal error: rules can only be generated once",
)
self._rules = tuple(self._generate_rules(tech=tech))
@abc.abstractproperty
def designmasks(self) -> Iterable[msk.DesignMask]:
return tuple()
def cast_params(self, params):
casted = {}
for param in self.params:
try:
default = param.default
except AttributeError:
try:
v = params.pop(param.name)
except KeyError:
if param.allow_none:
v = None
else:
raise ValueError(
f"Missing required parameter '{param.name}' for"
f" primitive '{self.name}'"
)
else:
v = params.pop(param.name, default)
casted[param.name] = param.cast(v)
if len(self.ports) > 0:
try:
portnets = params.pop("portnets")
except KeyError:
# Specifying nets is optional
pass
else:
# If nets are specified all nets need to be specified
portnames = {p.name for p in self.ports}
portnetnames = set(portnets.keys())
if (
(portnames != portnetnames)
or (len(self.ports) != len(portnets)) # Detect removal of doubles in set
):
raise ValueError(
f"Nets for ports {portnetnames} specified but prim '{self.name}'"
f" has ports {portnames}"
)
casted["portnets"] = portnets
if len(params) > 0:
raise TypeError(
f"primitive '{self.name}' got unexpected parameter(s) "
f"{tuple(params.keys())}"
)
return casted
class _Param(prp.Property):
def __init__(self, *, primitive: _Primitive, name: str, allow_none=False, default=None):
super().__init__(name, allow_none=allow_none)
self._primitive = primitive
if default is not None:
try:
default = self.cast(default)
except TypeError:
raise TypeError(
f"default can't be converted to type '{self.value_type_str}'"
)
self.default = default
@property
def primitive(self) -> _Primitive:
return self._primitive
def cast(self, value):
if (value is None) and hasattr(self, "default"):
return self.default
else:
return super().cast(value)
def __eq__(self, other):
equal = super().__eq__(other)
if isinstance(equal, rle._Rule):
return equal
else:
return (
equal
and isinstance(other, _Param)
and (self.primitive == other.primitive)
)
class _IntParam(_Param):
value_conv = None
value_type = int
value_type_str = "int"
class _BoolParam(_Param):
value_conv = None
value_type = bool
value_type_str = "bool"
class _PrimitiveParam(_Param):
value_conv = None
value_type = _Primitive
value_type_str = "'_Primitive'"
def __init__(self, *,
primitive: _Primitive, name: str, allow_none=False, default=None, choices=None,
):
if choices is not None:
if not _util.is_iterable(choices):
raise TypeError(
"choices has to be iterable of '_Primitive' objects"
)
choices = tuple(choices)
if not all(isinstance(prim, _Primitive) for prim in choices):
raise TypeError(
"choices has to be iterable of '_Primitive' objects"
)
self.choices = choices
super().__init__(
primitive=primitive, name=name, allow_none=allow_none, default=default,
)
def cast(self, value):
value = super().cast(value)
if hasattr(self, "choices"):
if not ((value is None) or (value in self.choices)):
raise ValueError(
f"Param '{self.name}' is not one of the allowed values:\n"
f" {self.choices}"
)
return value
class _EnclosureParam(_Param):
value_type_str = "'Enclosure'"
def cast(self, value):
if value is None:
if hasattr(self, "default"):
value = self.default
elif not self.allow_none:
raise TypeError(
f"'None' value not allowed for parameter '{self.name}'"
)
elif not (
isinstance(value, prp.Enclosure)
or (value in ("wide", "tall"))
):
try:
value = prp.Enclosure(value)
except:
raise TypeError(
f"value {repr(value)} can't be converted to an Enclosure object"
)
return value
class _EnclosuresParam(_Param):
value_type_str = "iterable of 'Enclosure'"
def __init__(self, *,
primitive: _Primitive, name: str, allow_none=False, default=None, n: int,
):
self.n = n
super().__init__(
primitive=primitive, name=name, allow_none=allow_none, default=default,
)
def cast(self, value):
if value is None:
if hasattr(self, "default"):
value = self.default
elif not self.allow_none:
raise TypeError(
f"'None' value not allowed for parameter '{self.name}'"
)
elif not _util.is_iterable(value):
try:
value = self.n*(prp.Enclosure(value),)
except:
raise TypeError(
f"param '{self.name}' has to be an enclosure value or an iterable \n"
f"of type 'Enclosure' with length {self.n}"
)
else:
try:
value = tuple(
(None if elem is None
else elem if isinstance(elem, prp.Enclosure)
else prp.Enclosure(elem)
) for elem in value
)
except:
raise TypeError(
f"param '{self.name}' has to be an enclosure value or an iterable \n"
f"of type 'Enclosure' with length {self.n}"
)
return value
class _Params(_util.TypedListStrMapping[_Param]):
@property
def _elem_type_(self):
return _Param
class _PrimitiveNet(net_.Net):
def __init__(self, *, prim: _Primitive, name: str):
super().__init__(name)
self.prim = prim
def __hash__(self):
return hash((self.name, self.prim))
def __eq__(self, other: object) -> bool:
if type(other) is not _PrimitiveNet:
return False
else:
return (self.name == other.name) and (self.prim == other.prim)
def __repr__(self):
return f"{self.prim.name}.{self.name}"
class _PrimitivePorts(
_util.IterableOverride[Union[_PrimitiveNet, wfr.SubstrateNet]],
net_.Nets,
):
@property
def _elem_type_(self):
return (_PrimitiveNet, wfr.SubstrateNet)
class _MaskPrimitive(_Primitive):
@abc.abstractmethod
def __init__(self, *,
name: Optional[str]=None, mask: msk._Mask, grid: Optional[IntFloat]=None,
**primitive_args,
):
if name is None:
name = mask.name
super().__init__(name=name, **primitive_args)
self.mask = mask
if grid is not None:
grid = _util.i2f(grid)
self.grid = grid
@abc.abstractmethod
def _generate_rules(self, *,
tech: tch.Technology, gen_mask: bool=True,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
if gen_mask and isinstance(self.mask, rle._Rule):
yield cast(rle._Rule, self.mask)
if self.grid is not None:
yield cast(msk.DesignMask, self.mask).grid == self.grid
@property
def designmasks(self):
return self.mask.designmasks
class _DerivedPrimitive(_MaskPrimitive):
"""A primitive that is derived from other primitives and not a
Primitive that can be part of the primitive list of a technology.
"""
def _generate_rules(self, *, tech: tch.Technology) -> Tuple[rle._Rule, ...]:
"""As _DerivedPrimitive will not be added to the list of primitives
of a technology node, it does not need to generate rules.
"""
raise RuntimeError("Internal error") # pragma: no cover
class _DesignMaskPrimitive(_MaskPrimitive):
@property
@abc.abstractmethod
def fill_space(self) -> str: # pragma: no cover
raise RuntimeError("Unimplemented abstract property")
@abc.abstractmethod
def __init__(self, *, name: str, **super_args):
if "mask" in super_args:
raise TypeError(
f"{self.__class__.__name__} got unexpected keyword argument 'mask'",
)
mask = msk.DesignMask(
name=name, fill_space=self.fill_space,
)
super().__init__(name=name, mask=mask, **super_args)
self.mask: msk.DesignMask
class _BlockageAttribute(_Primitive):
"""Mixin class for primitives with a blockage attribute"""
def __init__(self, blockage: Optional["Marker"]=None, **super_args):
self.blockage = blockage
super().__init__(**super_args)
class _PinAttribute(_Primitive):
"""Mixin class for primitives with a pin attribute"""
def __init__(self,
pin: OptSingleOrMulti["Marker"].T=None,
**super_args,
):
if pin is not None:
pin = _util.v2t(pin)
self.pin = pin
super().__init__(**super_args)
if pin is not None:
self.params += _PrimitiveParam(
primitive=self, name="pin", allow_none=True, choices=self.pin,
)
class _Intersect(_MaskPrimitive):
"""A primitive representing the overlap of a list of primitives"""
def __init__(self, *, prims: Iterable[_MaskPrimitive]):
prims2: Tuple[_MaskPrimitive, ...] = _util.v2t(prims)
if len(prims2) < 2:
raise ValueError(f"At least two prims needed for '{self.__class__.__name__}'")
self.prims = prims2
mask = msk.Intersect(p.mask for p in prims2)
_MaskPrimitive.__init__(self, mask=mask)
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
# This seems that it should not be called.
# see also: https://gitlab.com/Chips4Makers/PDKMaster/-/issues/27
return super()._generate_rules(tech=tech, gen_mask=False) # pragma: no cover
[docs]class Marker(_DesignMaskPrimitive):
"""The Marker primitive represents a layer used by other primitives for definition
of these primitives; typically a recognition.
It does not represent a processing layer and thus no physical mask is corresponding
with this primitive.
"""
@property
def fill_space(self):
return "yes"
def __init__(self, **super_args):
super().__init__(**super_args)
self.params += (
_Param(primitive=self, name="width", allow_none=True),
_Param(primitive=self, name="height", allow_none=True),
)
def _generate_rules(self, *,
tech: tch.Technology,
) -> Iterable[rle._Rule]:
return super()._generate_rules(tech=tech)
[docs]class Auxiliary(_DesignMaskPrimitive):
"""The Auxiliary primitive represents a layer that is defined by a foundry's
technology but not used in other PDKMaster primitives.
"""
@property
def fill_space(self):
return "no"
def __init__(self, **super_args):
super().__init__(**super_args)
def _generate_rules(self, *,
tech: tch.Technology,
) -> Iterable[rle._Rule]:
return super()._generate_rules(tech=tech)
SpaceTableRow = Tuple[
Union[float, Tuple[float, float]],
float,
]
class _WidthSpacePrimitive(_MaskPrimitive):
"""Common abstract base class for Primitives that have width and space property.
Subclasses of this class will need to provide certain properties as parameters
to the object `__init__()`
Arguments:
min_width: min width of drawn feature
min_space: min space between drawn features
space_table: optional width dependent spacing rules.
it is an iterable of rows with each row of the form
`width, space` or `(width, height), space`
min_density, max_density: optional minimum or maximum denity specification
"""
@abc.abstractmethod
def __init__(self, *,
min_width: IntFloat, min_space: IntFloat,
space_table: Optional[Iterable[Iterable[float]]]=None,
min_area: Optional[IntFloat]=None,
min_density: Optional[float]=None, max_density: Optional[float]=None,
**maskprimitive_args
):
self.min_width = min_width = _util.i2f(min_width)
self.min_space = min_space = _util.i2f(min_space)
self.min_area = min_area = _util.i2f(min_area)
self.min_density = min_density
if (
(min_density is not None)
and ((min_density < 0.0) or (min_density > 1.0))
):
raise ValueError("min_density has be between 0.0 and 1.0")
self.max_density = max_density
if (
(max_density is not None)
and ((max_density < 0.0) or (max_density > 1.0))
):
raise ValueError("max_density has be between 0.0 and 1.0")
if space_table is not None:
table: List[SpaceTableRow] = []
for row in space_table:
values = _util.i2f_recursive(row)
width, space = values
if not (
isinstance(width, float)
or (
isinstance(width, tuple) and (len(width) == 2)
and all(isinstance(w, float) for w in width)
)
):
raise TypeError(
"first element in a space_table row has to be a float "
"or an iterable of two floats"
)
table.append((
cast(Union[float, Tuple[float, float]], width),
space,
))
self.space_table = tuple(table)
else:
self.space_table = None
super().__init__(**maskprimitive_args)
self.params += (
_Param(primitive=self, name="width", default=self.min_width),
_Param(primitive=self, name="height", default=self.min_width),
)
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
yield from (
self.mask.width >= self.min_width,
self.mask.space >= self.min_space,
)
if self.min_area is not None:
yield self.mask.area >= self.min_area
if self.min_density is not None:
yield self.mask.density >= self.min_density
if self.max_density is not None:
yield self.mask.density <= self.max_density
if self.space_table is not None:
for row in self.space_table:
w = row[0]
if isinstance(w, float):
submask = self.mask.parts_with(
condition=self.mask.width >= w,
)
else:
submask = self.mask.parts_with(condition=(
self.mask.width >= w[0],
self.mask.length >= w[1],
))
yield msk.Spacing(submask, self.mask) >= row[1]
if isinstance(self, _PinAttribute) and self.pin is not None:
yield from (
msk.Connect(self.mask, pin.mask) for pin in self.pin
)
class _WidthSpaceDesignMaskPrimitive(_DesignMaskPrimitive, _WidthSpacePrimitive):
"""_WidthSpacePrimitive that is also a _DesignMaskPrimitive
"""
pass
[docs]class Implant(_WidthSpaceDesignMaskPrimitive):
"""Implant is a layer that represent an implantation step in the
semiconductor processing.
Arguments:
type_: type of the implant; has to be "n", "p" or "adjust"
an "adjust" layer is extra implant that can be used on
both n-type and p-type regions.
super_args: `_WidthSpacePrimitive` and `_DesignMaskPrimitive`
arguments
"""
@property
def fill_space(self):
return "yes"
# Implants are supposed to be disjoint unless they are used as combined implant
# MOSFET and other primitives
def __init__(self, *, type_: str, **super_args):
if type_ not in ("n", "p", "adjust"):
raise ValueError("type_ has to be 'n', 'p' or adjust")
self.type_ = type_
super().__init__(**super_args)
[docs]class Insulator(_WidthSpaceDesignMaskPrimitive):
"""Insulator is a layer representing an insulator layer.
Typical use is for thick oxide layer for higher voltage transistors.
Arguments:
fill_space: wether to allow fillinig regions that are violating
minimum spacing; it can only be "yes" or "no"; "same_net"
does not make sense for an insulator.
"""
def __init__(self, *, fill_space: str, **super_args):
if not fill_space in ("no", "yes"):
raise ValueError("fill_space has to be either 'yes' or 'no'")
self._fill_space = fill_space
super().__init__(**super_args)
@property
def fill_space(self) -> str:
return self._fill_space
class _Conductor(_BlockageAttribute, _PinAttribute, _DesignMaskPrimitive):
"""Primitive that acts as a conductor.
This primitive is assumed to use a DesignMask as it's mask. And will
allow a blockage and a pin layer.
"""
@abc.abstractmethod
def __init__(self, **super_args):
super().__init__(**super_args)
self.ports += _PrimitiveNet(prim=self, name="conn")
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
# Generate a mask for connection, thus without resistor parts
# or ActiveWire without gate etc.
indicators = chain(*tuple(r.indicator for r in filter(
lambda p: p.wire == self,
tech.primitives.__iter_type__(Resistor),
)))
polys = tuple(g.poly for g in filter(
lambda p: p.active == self,
tech.primitives.__iter_type__(MOSFETGate)
))
removes = {p.mask for p in chain(indicators, polys)}
if removes:
if len(removes) == 1:
remmask = removes.pop()
else:
remmask = msk.Join(removes)
self.conn_mask = self.mask.remove(remmask).alias(self.mask.name + "__conn")
yield self.conn_mask
else:
self.conn_mask = self.mask
class _DesignMaskConductor(_Conductor, _DesignMaskPrimitive):
"""_DesignMaskPrimitive that is also a _Conductor"""
pass
class _WidthSpaceConductor(_Conductor, _WidthSpacePrimitive):
"""_WidthSpacePrimitive that is also a _Conductor"""
pass
class _WidthSpaceDesignMaskConductor(_WidthSpaceConductor, _DesignMaskPrimitive):
"""Combination of _DesignMaskConductor and _WidthSpaceConductor"""
@property
def fill_space(self):
return "same_net"
[docs]class Well(_WidthSpaceDesignMaskConductor, Implant):
"""Well is an Implant layer that has deeper implant so it forms a
well of a certain type.
Typical application is for both NMOS and PMOS transistors on a wafer
without shorting the source/drain regions to the bulk.
Arguments:
min_space_samenet: the smaller spacing between two wells on
the same net.
"""
# Wells are non-overlapping by design
def __init__(self, *,
min_space_samenet: Optional[IntFloat]=None, **super_args,
):
super().__init__(**super_args)
if min_space_samenet is not None:
min_space_samenet = _util.i2f(min_space_samenet)
if min_space_samenet >= self.min_space:
raise ValueError("min_space_samenet has to be smaller than min_space")
self.min_space_samenet = min_space_samenet
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
if self.min_space_samenet is not None:
yield self.mask.same_net.space >= self.min_space_samenet
class DeepWell(_WidthSpaceDesignMaskConductor, Implant):
"""The DeepWell primitive defines a well deeper into the substrate and normally
used to connect a normal Well and in that way isolate some part of the wafer
substrate. Most commonly this is the combination of the N-Well together with a
depp N-Well to isolate the holes in the N-Well layer.
Currently only low-level _Layout.add_shape() is supported for DeepWell, no
support is present for combined Well + DeepWell layout generation.
"""
def __init__(self, *,
well: Well, type_: Optional[str]=None,
min_well_overlap: float, min_well_enclosure: float,
**super_args,
):
if type_ is None:
type_ = well.type_
elif type_ != well.type_:
raise ValueError(
f"DeepWell type '{type_}' is different from type {well.type_} of Well"
f" '{well.name}'"
)
super().__init__(type_=type_, **super_args)
self.well = well
self.min_well_overlap = min_well_overlap
self.min_well_enclosure = min_well_enclosure
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
yield (
msk.Intersect((self.mask, self.well.mask)).width >= self.min_well_overlap
)
yield (
self.well.mask.remove(self.mask).width >= self.min_well_enclosure
)
[docs]class WaferWire(_WidthSpaceDesignMaskConductor):
"""WaferWire is representing the wire made from wafer material and normally
isolated by LOCOS for old technlogies and STI (shallow-trench-isolation)
or FINFET for newer/recent ones.
The doping type is supposed the be determing by implant layers.
Arguments:
implant: the valid `Implant` layers for this primitive
min_implant_enclosure: the minimum required enclosure by the implant
over the waferwire. If a single enclosure is specified it is
the spec for all the implants.
implant_abut: wether to allow the abutment of two waferwire shapes
of opposite type.
allow_contactless_implant: wether to allow waferwire shapes without
a contact. If True it is assumed that abutted shapes are on the same
net.
allow_in_substrate: wether to allow a waferwire shape that is not in a
well. Some processes use wafers of a certain doping type; others
need a well for all active devices.
well: the well primitives valid for this WaferWire,
It is assumed that WaferWire with implant type the same as the well
connect to that well.
min_well_enclosure: the minimum required enclosure of the WaferWire by
the well. If only one value is given it is valid for all the wells.
min_well_enclosure_same_type: allow to specify other enclosure for
WaferWires with the same type as the well.
min_substrate_enclosure: the minimum required enclosure of the WaferWire by
the substrate with the substrate defined as any wafer region that is
not covered by a well. If not specified the same value as
min_well_enclosure is used.
min_substrate_enclosure_same_type: allow to specify other enclosure for
WaferWires with the same type as the well. If not specified the same value
as min_well_enclosure_same_type is used.
allow_well_crossing: wether it is allow for a WaferWire to go over a well
boundary
oxide: the list of valid oxide layers for this WaferWire. This can be empty.
min_oxide_enclosure: the minimum required enclosure of the WaferWire by
the oxide layer. If only one value is given it is valid for all the oxide
layers.
super_args: the argument for `_WidthSpacePrimitive` and `_DesignMaskPrimitive`
"""
def __init__(self, *,
implant: SingleOrMulti[Implant].T,
min_implant_enclosure: SingleOrMulti[prp.Enclosure].T,
implant_abut: Union[str, SingleOrMulti[Implant].T],
allow_contactless_implant: bool,
allow_in_substrate: bool,
well: SingleOrMulti[Well].T,
min_well_enclosure: SingleOrMulti[prp.Enclosure].T,
min_well_enclosure_same_type: OptSingleOrMulti[Optional[prp.Enclosure]].T=None,
min_substrate_enclosure: Optional[prp.Enclosure]=None,
min_substrate_enclosure_same_type: Optional[prp.Enclosure]=None,
allow_well_crossing: bool,
oxide: OptSingleOrMulti[Insulator].T=None,
min_oxide_enclosure: SingleOrMulti[Optional[prp.Enclosure]].T=None,
**super_args
):
self.allow_in_substrate = allow_in_substrate
self.implant = implant = _util.v2t(implant)
for impl in implant:
if isinstance(impl, Well):
raise TypeError(f"well '{impl.name}' may not be part of implant")
self.min_implant_enclosure = min_implant_enclosure = _util.v2t(
min_implant_enclosure, n=len(implant),
)
if isinstance(implant_abut, str):
_conv: Dict[str, Tuple[Implant, ...]] = {
"all": implant, "none": tuple()
}
if implant_abut not in _conv:
raise ValueError(
"only 'all' or 'none' allowed for a string implant_abut"
)
implant_abut = _conv[implant_abut]
else:
implant_abut = _util.v2t(implant_abut)
for impl in implant_abut:
if impl not in implant:
raise ValueError(
f"implant_abut member '{impl.name}' not in implant list"
)
self.implant_abut = implant_abut
self.allow_contactless_implant = allow_contactless_implant
self.well = well = _util.v2t(well)
for w in well:
if not any(impl.type_ == w.type_ for impl in implant):
raise UnconnectedPrimitiveError(well[0])
self.min_well_enclosure = min_well_enclosure = _util.v2t(
min_well_enclosure, n=len(well),
)
if min_well_enclosure_same_type is None:
self.min_well_enclosure_same_type = None
else:
self.min_well_enclosure_same_type = cast(
Tuple[Optional[prp.Enclosure], ...],
_util.v2t(min_well_enclosure_same_type, n=len(well)),
)
if allow_in_substrate:
if min_substrate_enclosure is None:
if len(min_well_enclosure) == 1:
min_substrate_enclosure = min_well_enclosure[0]
if min_substrate_enclosure_same_type is not None:
raise TypeError(
"min_substrate_enclosure_same_type has to be 'None' "
"if min_substrate_enclosure is 'None'"
)
if self.min_well_enclosure_same_type is not None:
min_substrate_enclosure_same_type = \
self.min_well_enclosure_same_type[0]
else:
raise TypeError(
"min_substrate_enclosure has be provided when providing "
"multiple wells"
)
elif min_substrate_enclosure is not None:
raise TypeError(
"min_substrate_enclosure has to be 'None' if allow_in_substrate "
"is 'False'"
)
self.allow_well_crossing = allow_well_crossing
self.min_substrate_enclosure = min_substrate_enclosure
self.min_substrate_enclosure_same_type = min_substrate_enclosure_same_type
if oxide is not None:
oxide = _util.v2t(oxide)
min_oxide_enclosure = _util.v2t(min_oxide_enclosure, n=len(oxide))
elif min_oxide_enclosure is not None:
raise ValueError("min_oxide_enclosure provided with no oxide given")
self.oxide = oxide
self.min_oxide_enclosure = min_oxide_enclosure
super().__init__(**super_args)
if len(implant) > 1:
self.params += (
_PrimitiveParam(primitive=self, name="implant", choices=self.implant),
_EnclosureParam(primitive=self, name="implant_enclosure", allow_none=True),
)
else:
self.params += (
_EnclosureParam(
primitive=self, name="implant_enclosure",
default=min_implant_enclosure[0],
),
)
if (len(well) > 1) or allow_in_substrate:
self.params += (
_PrimitiveParam(
primitive=self, name="well", allow_none=allow_in_substrate,
choices=self.well
),
_EnclosureParam(primitive=self, name="well_enclosure", allow_none=True),
)
else:
self.params += (
_EnclosureParam(
primitive=self, name="well_enclosure", default=min_well_enclosure[0],
),
)
if self.oxide is not None:
self.params += (
_PrimitiveParam(primitive=self, name="oxide", choices=self.oxide, allow_none=True),
_EnclosureParam(primitive=self, name="oxide_enclosure", allow_none=True),
)
[docs] def cast_params(self, params):
well_net = params.pop("well_net", None)
params = super().cast_params(params)
if "implant" in params:
implant = params["implant"]
else:
params["implant"] = implant = self.implant[0]
if params["implant_enclosure"] is None:
idx = self.implant.index(implant)
params["implant_enclosure"] = self.min_implant_enclosure[idx]
if "well" in params:
well = params["well"]
if (well is not None) and (params["well_enclosure"] is None):
idx = self.well.index(well)
params["well_enclosure"] = self.min_well_enclosure[idx]
else:
# well parameter will always be there except when next condition
# is met.
assert (
(len(self.well) == 1) and (not self.allow_in_substrate)
), "Internal error"
params["well"] = well = self.well[0]
if well is not None:
if well_net is None:
raise TypeError(
f"No well net specified for primitive '{self.name}' in a well"
)
params["well_net"] = well_net
elif well_net is not None:
raise TypeError(
f"Well net specified for primitive '{self.name}' not in a well"
)
if ("oxide" in params):
oxide = params["oxide"]
if oxide is not None:
assert self.oxide is not None
assert self.min_oxide_enclosure is not None
oxide_enclosure = params["oxide_enclosure"]
if oxide_enclosure is None:
idx = self.oxide.index(oxide)
params["oxide_enclosure"] = self.min_oxide_enclosure[idx]
return params
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
for i, impl in enumerate(self.implant):
sd_mask_impl = msk.Intersect((self.conn_mask, impl.mask)).alias(
f"{self.conn_mask.name}:{impl.name}",
)
yield from (sd_mask_impl, msk.Connect(self.conn_mask, sd_mask_impl))
if self.allow_in_substrate and (impl.type_ == tech.substrate_type):
yield msk.Connect(sd_mask_impl, tech.substrate)
if impl not in self.implant_abut:
yield edg.MaskEdge(impl.mask).interact_with(self.mask).length == 0
enc = self.min_implant_enclosure[i]
yield self.mask.enclosed_by(impl.mask) >= enc
for w in self.well:
if impl.type_ == w.type_:
yield msk.Connect(sd_mask_impl, w.mask)
for implduo in combinations((impl.mask for impl in self.implant_abut), 2):
yield msk.Intersect(implduo).area == 0
# TODO: allow_contactless_implant
for i, w in enumerate(self.well):
enc = self.min_well_enclosure[i]
if enc.is_assymetric: # pragma: no cover
raise NotImplementedError(
f"Asymmetric enclosure of WaferWire '{self.name}' "
f"by well '{w.name}'",
)
if self.min_well_enclosure_same_type is None:
yield self.mask.enclosed_by(w.mask) >= enc
else:
enc2 = self.min_well_enclosure_same_type[i]
if enc2 is None:
yield self.mask.enclosed_by(w.mask) >= enc
else:
if enc2.is_assymetric: # pragma: no cover
raise NotImplementedError(
f"Asymmetric same type enclosure of WaferWire '{self.name}"
f"by well '{w.name}",
)
for ww in (
cast("_WaferWireIntersect", self.in_(impl))
for impl in filter(
# other type
lambda impl2: w.type_ != impl2.type_, self.implant,
)
):
yield ww.mask.enclosed_by(w.mask) >= enc
for ww in (
cast("_WaferWireIntersect", self.in_(impl))
for impl in filter(
# same type
lambda impl2: w.type_ == impl2.type_, self.implant,
)
):
yield ww.mask.enclosed_by(w.mask) >= enc2
if self.min_substrate_enclosure is not None:
if self.min_substrate_enclosure_same_type is None:
yield (
self.mask.enclosed_by(tech.substrate)
>= self.min_substrate_enclosure
)
else:
for ww in (
cast("_WaferWireIntersect", self.in_(impl)) for impl in filter(
# other type
lambda impl2: tech.substrate_type != impl2.type_, self.implant,
)):
yield (
ww.mask.enclosed_by(tech.substrate)
>= self.min_substrate_enclosure
)
for ww in (
cast("_WaferWireIntersect", self.in_(impl)) for impl in filter(
# same type
lambda impl2: tech.substrate_type == impl2.type_, self.implant,
)):
yield (
ww.mask.enclosed_by(tech.substrate)
>= self.min_substrate_enclosure_same_type
)
if self.oxide is not None:
assert self.min_oxide_enclosure is not None
for i, ox in enumerate(self.oxide):
enc = self.min_oxide_enclosure[i]
if enc is not None:
yield self.mask.enclosed_by(ox.mask) >= enc
if not self.allow_well_crossing:
mask_edge = edg.MaskEdge(self.mask)
yield from (
mask_edge.interact_with(edg.MaskEdge(w.mask)).length == 0
for w in self.well
)
[docs] def in_(self, prim: SingleOrMulti[_MaskPrimitive].T) -> "_DerivedPrimitive":
return _WaferWireIntersect(waferwire=self, prim=prim)
class _WaferWireIntersect(_DerivedPrimitive, _Intersect):
"""Intersect of WaferWire with one or more of it's implants, wells and
oxides"""
def __init__(self, *,
waferwire: WaferWire, prim: SingleOrMulti[_MaskPrimitive].T,
):
ww_prims: Set[_MaskPrimitive] = set(waferwire.implant)
if waferwire.well is not None:
ww_prims.update(waferwire.well)
if waferwire.oxide is not None:
ww_prims.update(waferwire.oxide)
prim = _util.v2t(prim)
for p in prim:
if p not in ww_prims:
raise ValueError(
f"prim '{p.name}' not an implant, well or oxide layer for"
f" WaferWire '{waferwire.name}'"
)
self.waferwire = waferwire
self.prim = prim
super().__init__(prims=(waferwire, *prim))
[docs]class GateWire(_WidthSpaceDesignMaskConductor):
"""GateWire is a _WidthSpaceDesignMaskConductor that can act as the
gate of a MOSFET.
No extra arguments next to the `_WidthSpacePrimitive` and `_DesignMaskPrimitive`
ones.
"""
def __init__(self, **super_args):
super().__init__(**super_args)
class MIMTop(MetalWire):
"""MIMTop is a primitive to be used as the top of a MIM Capacitor
"""
pass
ViaBottom = Union[WaferWire, GateWire, MetalWire, "Resistor"]
ViaTop = Union[MetalWire, "Resistor"]
[docs]class Via(_DesignMaskConductor):
"""A Via layer is a layer that connect two conductor layer vertically.
Arguments:
width: the fixed width; only squares with this width are allowed
on the Via layer.
bottom: list of valid bottom primitives for this Via layer.
These have to be `WaferWire`, `GateWire`, `MetalWire` or `Resistor`
objects
min_bottom_enclosure: the minimum required enclosure of the Via by
the bottom layer. If only one value is given it is valid for all the
bottom layers.
top: list of valid bottom primitives for this Via layer.
These have to be `WaferWire`, `GateWire`, `MetalWire` or `Resistor`
objects
min_bottom_enclosure: the minimum required enclosure of the Via by
the top layer. If only one value is given it is valid for all the
top layers.
super_args: parameters for `_DesignMaskPrimitive`
"""
@property
def fill_space(self):
return "no"
# When drawing via and bottom or top is not specified by default the first layer
# will be used if it is a MetalWire, otherwise it needs to be specified.
def __init__(self, *,
width: IntFloat, min_space: IntFloat,
bottom: SingleOrMulti[ViaBottom].T, top: SingleOrMulti[ViaTop].T,
min_bottom_enclosure: SingleOrMulti[prp.Enclosure].T,
min_top_enclosure: SingleOrMulti[prp.Enclosure].T,
**super_args,
):
super().__init__(**super_args)
self.bottom = bottom = _util.v2t(bottom)
self.min_bottom_enclosure = min_bottom_enclosure = _util.v2t(min_bottom_enclosure, n=len(bottom))
for b in bottom:
if isinstance(b, TopMetalWire):
raise TypeError(
f"TopMetalWire '{b.name} not allowed as bottom of Via '{self.name}'",
)
self.top = top = _util.v2t(top)
self.min_top_enclosure = min_top_enclosure = _util.v2t(
min_top_enclosure, n=len(top),
)
self.width = width = _util.i2f(width)
self.min_space = min_space = _util.i2f(min_space)
self.params += (
_Param(primitive=self, name="space", default=min_space),
_IntParam(primitive=self, name="rows", allow_none=True),
_IntParam(primitive=self, name="columns", allow_none=True),
_EnclosureParam(primitive=self, name="bottom_enclosure", allow_none=True),
_Param(primitive=self, name="bottom_width", allow_none=True),
_Param(primitive=self, name="bottom_height", allow_none=True),
_EnclosureParam(primitive=self, name="top_enclosure", allow_none=True),
_Param(primitive=self, name="top_width", allow_none=True),
_Param(primitive=self, name="top_height", allow_none=True),
)
if len(bottom) > 1:
default = bottom[0]
if not isinstance(default, MetalWire) or isinstance(default, MIMTop):
default = None
self.params += _PrimitiveParam(
primitive=self, name="bottom", default=default, choices=bottom,
)
choices = sum(
(cast(WaferWire, wire).implant for wire in filter(
lambda w: isinstance(w, WaferWire),
bottom,
)),
tuple(),
)
if choices:
self.params += (
_PrimitiveParam(
primitive=self, name="bottom_implant",
allow_none=True, choices=choices,
),
_EnclosureParam(
primitive=self, name="bottom_implant_enclosure", allow_none=True,
),
_PrimitiveParam(primitive=self, name="bottom_well", allow_none=True),
_EnclosureParam(
primitive=self, name="bottom_well_enclosure", allow_none=True,
),
)
choices = sum(
(cast(Tuple[Insulator, ...], cast(WaferWire, wire).oxide)
for wire in filter(
lambda w: isinstance(w, WaferWire) and (w.oxide is not None),
bottom,
)),
tuple(),
)
if choices:
self.params += (
_PrimitiveParam(
primitive=self, name="bottom_oxide", allow_none=True, choices=choices,
),
_EnclosureParam(
primitive=self, name="bottom_oxide_enclosure", allow_none=True,
),
)
if len(top) > 1:
default = top[0]
assert isinstance(default, MetalWire), "Not implemented"
self.params += _PrimitiveParam(
primitive=self, name="top", default=default, choices=top,
)
[docs] def cast_params(self, params):
well_net = params.pop("well_net", None)
params = super().cast_params(params)
def _check_param(name):
return (name in params) and (params[name] is not None)
has_bottom = _check_param("bottom")
# has_bottom_enclosure = _check_param("bottom_enclosure")
has_bottom_implant = _check_param("bottom_implant")
has_bottom_implant_enclosure = _check_param("bottom_implant_enclosure")
has_bottom_well = _check_param("bottom_well")
has_bottom_well_enclosure = _check_param("bottom_well_enclosure")
has_bottom_width = _check_param("bottom_width")
has_bottom_height = _check_param("bottom_height")
has_top = _check_param("top")
has_rows = _check_param("rows")
has_columns = _check_param("columns")
has_top_width = _check_param("top_width")
has_top_height = _check_param("top_height")
if has_bottom:
bottom = params["bottom"]
else:
bottom = params["bottom"] = self.bottom[0]
if isinstance(bottom, WaferWire):
impl = params["bottom_implant"]
if impl is None:
raise ValueError(
"bottom_implant parameter not provided for use of\n"
f"bottom '{bottom.name}' for via '{self.name}'"
)
if not has_bottom_implant_enclosure:
idx = bottom.implant.index(impl)
params["bottom_implant_enclosure"] = bottom.min_implant_enclosure[idx]
if has_bottom_well:
bottom_well = params["bottom_well"]
if bottom_well not in bottom.well:
raise ValueError(
f"bottom_well '{bottom_well.name}' not a valid well for "
f"bottom wire '{bottom.name}'"
)
if not has_bottom_well_enclosure:
idx = bottom.well.index(bottom_well)
params["bottom_well_enclosure"] = (
bottom.min_well_enclosure[idx]
)
params["well_net"] = well_net
elif not bottom.allow_in_substrate:
raise ValueError(
f"bottom wire '{bottom.name}' needs a well"
)
elif has_bottom_implant:
bottom_implant = params["bottom_implant"]
raise TypeError(
f"bottom_implant '{bottom_implant.name}' not a valid implant for "
f"bottom wire '{bottom.name}'"
)
elif has_bottom_implant_enclosure:
raise TypeError(
"bottom_implant_enclosure wrongly provided for bottom wire "
f"'{bottom.name}'"
)
elif has_bottom_well:
bottom_well = params["bottom_well"]
raise TypeError(
f"bottom_well '{bottom_well.name}' not a valid well for "
f"bottom wire '{bottom.name}'"
)
elif has_bottom_well_enclosure:
raise TypeError(
"bottom_well_enclosure wrongly provided for bottom wire "
f"'{bottom.name}'"
)
if has_top:
top = params["top"]
else:
top = params["top"] = self.top[0]
if not any((has_rows, has_bottom_height, has_top_height)):
params["rows"] = 1
if not any((has_columns, has_bottom_width, has_top_width)):
params["columns"] = 1
return params
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
yield from (
self.mask.width == self.width,
self.mask.space >= self.min_space,
msk.Connect((b.conn_mask for b in self.bottom), self.mask),
msk.Connect(self.mask, (b.conn_mask for b in self.top)),
)
for i in range(len(self.bottom)):
bot_mask = self.bottom[i].mask
enc = self.min_bottom_enclosure[i]
yield self.mask.enclosed_by(bot_mask) >= enc
for i in range(len(self.top)):
top_mask = self.top[i].mask
enc = self.min_top_enclosure[i]
yield self.mask.enclosed_by(top_mask) >= enc
@property
def designmasks(self):
yield from super().designmasks
for conn in self.bottom + self.top:
yield from conn.designmasks
[docs] def in_(self, prim: SingleOrMulti[_MaskPrimitive].T) -> _DerivedPrimitive:
return _ViaIntersect(via=self, prim=prim)
class _ViaIntersect(_DerivedPrimitive, _Intersect):
"""Intersect of WaferWire with one or more of it's implants, wells and
oxides"""
def __init__(self, *,
via: Via, prim: SingleOrMulti[_MaskPrimitive].T,
):
via_prims: Set[_MaskPrimitive] = {*via.bottom, *via.top}
prim = _util.v2t(prim)
for p in prim:
if isinstance(p, _WaferWireIntersect):
p = p.waferwire
if p not in via_prims:
raise ValueError(
f"prim '{p.name}' not a bottom or top layer for Via '{via.name}'"
)
self.via = via
self.prin = prim
super().__init__(prims=(via, *prim))
[docs]class PadOpening(_WidthSpaceDesignMaskConductor):
"""PadOpening is a layer representing an opening in the top layer in the
processing of a semiconductor wafer.
Typical application is for wirebonding, bumping for flip-chip or an RDL
(redistribution) layer.
Arguments:
bottom: the MetalWire layer for which on top of which an opening in
the isolation is made.
min_bottom_enclsoure: the minimum enclosure of the `PadOpening` layer
by the bottom layer.
super_args: arguments for `WidthSpacePrimitive` and `_DesignMaskPrimitive`
"""
@property
def fill_space(self):
return "no"
def __init__(self, *,
bottom: MetalWire, min_bottom_enclosure: prp.Enclosure, **super_args,
):
super().__init__(**super_args)
if isinstance(bottom, TopMetalWire):
raise TypeError(
f"TopMetalWire '{bottom.name}' not allowed for PadOpening '{self.name}'",
)
self.bottom = bottom
self.min_bottom_enclosure = min_bottom_enclosure
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
yield (
self.mask.enclosed_by(self.bottom.mask)
>= self.min_bottom_enclosure
)
@property
def designmasks(self):
yield from super().designmasks
yield from self.bottom.designmasks
ResistorWire = Union[WaferWire, GateWire, MetalWire]
ResistorIndicator = Union[Marker, ExtraProcess]
[docs]class Resistor(_WidthSpacePrimitive):
"""Resistor is a primitive that represent a resistor device.
The resistor itself is not drawn by defined by the overlap
of other drawn layers.
Attributes:
mask: the resulting mask with the shape of the mask. It is
the overlapping part of the base wire with all the
indicators
Arguments:
wire: the base wire used to make a resistor device.
It has to be a `WaferWire`, a `Gatewire` or a `MetalWire`
contact: optional Via layer used to connect the base wire.
min_contact_space: the minimum of the contact to the overlap
of the base wire and all the indicator layers.
indicator: list of indicator layers for the Resistor. Only the
overlapping area of all the indicator layers will be seen as
the Resistor device.
For am indicator of type ExtraProcess it is assumed that this
extra process does influence the resulting resistance as with
a Marker type it does not and is thus just a recognition layer.
min_indicator_extension: minimum required extension of the indicator
shapes over the base wire. If only one value is given it will
be used for all the indicators.
implant: optional implant layer for the resistor. This allows to
have different sheet resistance and models for wire with
different implants. If wire is a WaferWire the implant
layer has to be a valid implant for that waferwire.
model: spice model name of the Resistor; the width and height of
the overlapping area are used as parameters.
subckt_params: Conversion of width and height parameter to the name
used by the model. It is thus a dict with keys "width" and "height"
and the value for each the actual spice model parameter name.
By default "width" is translated to "w" and "height to "l".
sheetres: the optional sheet resistance value.
If both sheetres and model are given, sheetres will be used for LVS
and model for generating extracted netlist meant for simulation.
"""
def __init__(self, *, name: str,
wire: ResistorWire,
contact: Optional[Via], min_contact_space: Optional[IntFloat]=None,
indicator: SingleOrMulti[ResistorIndicator].T,
min_indicator_extension: SingleOrMulti[IntFloat].T,
implant: Optional[Implant]=None,
min_implant_enclosure: Optional[prp.Enclosure]=None,
model: Optional[str]=None, subckt_params: Optional[Dict[str, str]]=None,
sheetres: Optional[IntFloat]=None, **widthspace_args,
):
# If both model and sheetres are specified, sheetres will be used for
# LVS circuit generation in pyspice export.
self.wire = wire
if "grid" in widthspace_args:
raise TypeError("Resistor got an unexpected keyword argument 'grid'")
if "min_width" in widthspace_args:
if widthspace_args["min_width"] < wire.min_width:
raise ValueError("min_width may not be smaller than base wire min_width")
else:
widthspace_args["min_width"] = wire.min_width
if "min_space" in widthspace_args:
if widthspace_args["min_space"] < wire.min_space:
raise ValueError("min_space may not be smaller than base wire min_space")
else:
widthspace_args["min_space"] = wire.min_space
self.indicator = indicator = _util.v2t(indicator)
self.min_indicator_extension = min_indicator_extension = _util.v2t(
_util.i2f_recursive(min_indicator_extension), n=len(indicator),
)
if implant is not None:
if isinstance(implant, Well):
raise TypeError(
f"Resistor implant may not be Well '{implant.name}'",
)
if isinstance(wire, WaferWire):
if implant not in wire.implant:
raise ValueError(
f"implant '{implant.name}' is not valid for waferwire '{wire.name}'"
)
elif not isinstance(wire, GateWire):
raise ValueError(
f"Resistor {name}: "
"implant may only be provided for a wire of type "
"'WaferWire' or 'GateWire'"
)
elif min_implant_enclosure is not None:
raise TypeError(
"min_implant_enclosure has to be 'None' if no implant is given"
)
self.implant = implant
self.min_implant_enclosure = min_implant_enclosure
prims = (wire, *indicator)
if implant:
prims += (implant,)
mask = msk.Intersect(prim.mask for prim in prims).alias(f"resistor:{name}")
super().__init__(name=name, mask=mask, **widthspace_args)
self.ports += (
_PrimitiveNet(prim=self, name=name)
for name in ("port1", "port2")
)
if contact is not None:
if wire not in (contact.bottom + contact.top):
raise ValueError(
f"wire {wire.name} does not connect to via {contact.name}"
)
if min_contact_space is None:
raise TypeError(
"min_contact_space not given when contact is given"
)
elif min_contact_space is not None:
raise TypeError(
"min_contact_space has to be 'None' if no contact layer is given"
)
self.contact = contact
self.min_contact_space = min_contact_space
if (model is None) and (sheetres is None):
raise TypeError(
"Either model or sheetres have to be specified"
)
if model is not None:
if subckt_params is not None:
if set(subckt_params.keys()) != {"width", "height"}:
raise ValueError(
"subckt_params has to be None or a dict with keys ('width', 'height')"
)
elif subckt_params is not None:
raise TypeError("subckt_params provided without a model")
self.model = model
self.subckt_params = subckt_params
sheetres = _util.i2f(sheetres)
self.sheetres = sheetres
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
# Do not generate the default width/space rules.
yield from _Primitive._generate_rules(self, tech=tech)
# TODO: Can we provide proper type for self.mask ?
yield cast(msk.DesignMask, self.mask)
self.conn_mask = msk.Intersect((self.mask, *(p.mask for p in self.indicator)))
if self.min_width > self.wire.min_width:
yield self.mask.width >= self.min_width
if self.min_space > self.wire.min_space:
yield self.mask.space >= self.min_space
if self.min_area is not None:
if (self.wire.min_area is None) or (self.min_area > self.wire.min_area):
yield self.mask.area >= self.min_area
for i, ind in enumerate(self.indicator):
ext = self.min_indicator_extension[i]
mask = self.wire.mask.remove(ind.mask)
yield mask.width >= ext
class Capacitor(_WidthSpacePrimitive):
"""This is a abstract base class for all capacitor types.
It needs to be subclassed.
"""
pass
class MIMCapacitor(Capacitor):
"""`MIMCapactor` represents the so-called Metal-Insulator-Metal
type of capacitor.
Currently it specifically tackles the MIM capacitor made with
a dieletric on top of a MetalWire with on top of that an
intermediate metal layer.
Arguments:
name: the name of the MIMCapacitor
bottom: the bottom layer of the MIM capacitor
top: the top layer of the MIM capacitor
via: the Via layer contacting both the bottom and top layer
of the MIM capacitor
min_bottom_top_enclosure: min required enclosure of the top
layer by the bottom layer
min_bottomvia_top_space: min space from a via contacting the bottom
layer to the top layers
min_top_via_enclosure: min enclosure of a via contacting the top
layer by the top layer.
min_bottom_space: minimum space from a shape that is used as the
bottom of a MIM capacitor to any other shape on the bottom
MetalWire layer.
min_top2bottom_space: minimum space from the top layer to any shape
on the bottom MetalWire that is not the bottom plate of the
same capacitor.
model: the spice model name for the capacitor
subckt_params: Conversion of width and height parameter to the name
used by the model. It is thus a dict with keys "width" and "height"
and the value for each the actual spice model parameter name.
By default "width" is translated to "w" and "height to "l".
"""
def __init__(self, *, name: str,
bottom: MetalWire, top: MIMTop, via: Via,
min_bottom_top_enclosure: prp.Enclosure, min_bottomvia_top_space: IntFloat,
min_top_via_enclosure: prp.Enclosure,
min_bottom_space: Optional[IntFloat], min_top2bottom_space: Optional[IntFloat],
model: str, subckt_params: Optional[Dict[str, str]]=None,
**widthspace_args,
):
if not bottom in via.bottom:
raise ValueError(
f"MIMCapacitor '{name}':"
f" bottom '{bottom.name}' is not a bottom layer for via '{via.name}'"
)
if not top in via.bottom:
raise ValueError(
f"MIMCapacitor '{name}':"
f" top '{top.name}' is not a bottom layer for via '{via.name}'"
)
if "min_width" in widthspace_args:
if widthspace_args["min_width"] < top.min_width:
raise ValueError("min_width may not be smaller than top wire min_width")
else:
widthspace_args["min_width"] = top.min_width
if "min_space" in widthspace_args:
if widthspace_args["min_space"] < top.min_space:
raise ValueError("min_space may not be smaller than top wire min_space")
else:
widthspace_args["min_space"] = top.min_space
self.bottom = bottom
self.top = top
self.via = via
self.min_bottom_top_enclosure = min_bottom_top_enclosure
self.min_bottomvia_top_space = min_bottomvia_top_space
self.min_top_via_enclosure = min_top_via_enclosure
self.min_bottom_space = min_bottom_space
self.min_top2bottom_space = min_top2bottom_space
mask = top.mask.alias(f"mimcap:{name}")
super().__init__(name=name, mask=mask, **widthspace_args)
self.params += (
_BoolParam(primitive=self, name="bottom_connect_up", default=True),
)
self.ports += (
_PrimitiveNet(prim=self, name=name)
for name in ("bottom", "top")
)
if subckt_params is not None:
if set(subckt_params.keys()) != {"width", "height"}:
raise ValueError(
"subckt_params has to be None or a dict with keys ('width', 'height')"
)
self.model = model
self.subckt_params = subckt_params
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
# TODO: MIMCapacitor rules
DiodeIndicator = Union[Marker, ExtraProcess]
[docs]class Diode(_WidthSpacePrimitive):
"""`Diode` represent a diode device made up of a WaferWire object.
A diode device needs a pn-junction which does need to be achieved by
implants.
Arguments:
name: optional name for the diode; otherwise a unique name starting
with "diode:" will be generated.
wire: the base layer for the diode
indicator: list of indicator layers for the Diode. Only the
overlapping area of all the indicator layers will be seen as
the Resistor device.
Both ExtraProcess and Marker are valid indicator layers.
min_indicator_extension: minimum required enclosure of the base wire
by the indicator. If only one value is given it will
be used for all the indicators.
implant: the implant layer of the WaferWire forming the diode.
The implant layer has to be a valid implant layer for the
base WaferWire primitive.
min_implant_enclosure: the optional minimum required enclosure
of the base wire by the implant. If not provided the one specified
for the base wire will be used as default.
well: optional well to place the diode in. This well has to be of
opposite type of the implant layer. If no well is specified
the diode is in the substrate. So then the base wire must be
allowed to be placed in the substrate and the technology
substrate type has to be the opposite of the implant of
the diode.
min_well_enclosure: optional minimum required enclosure of the
base wire by the well. If no well is specified no
min_well_enclosure may be specified either. If a well is
specified but no min_well_enclosure the minimum well enclosure
from the base WaferWire will be used.
model: the spice model name
widthspace_args: the arguments for `_WidthSpacePrimitive`
"""
def __init__(self, *, name: Optional[str]=None,
wire: WaferWire, indicator: SingleOrMulti[DiodeIndicator].T,
min_indicator_enclosure: SingleOrMulti[prp.Enclosure].T,
implant: Implant, min_implant_enclosure: Optional[prp.Enclosure]=None,
well: Optional[Well]=None, min_well_enclosure: Optional[prp.Enclosure]=None,
model: Optional[str]=None, **widthspace_args,
):
self.wire = wire
self.indicator = indicator = _util.v2t(indicator)
if "grid" in widthspace_args:
raise TypeError("Resistor got an unexpected keyword argument 'grid'")
if "min_width" in widthspace_args:
if widthspace_args["min_width"] < wire.min_width:
raise ValueError("min_width may not be smaller than base wire min_width")
else:
widthspace_args["min_width"] = wire.min_width
if "min_space" in widthspace_args:
if widthspace_args["min_space"] < wire.min_space:
raise ValueError("min_space may not be smaller than base wire min_space")
else:
widthspace_args["min_space"] = wire.min_space
self.min_indicator_enclosure = min_indicator_enclosure = _util.v2t(
min_indicator_enclosure, n=len(indicator),
)
if isinstance(implant, Well):
raise TypeError(f"implant '{implant.name}' is a well")
if not implant in wire.implant:
raise ValueError(
f"implant '{implant.name}' is not valid for waferwire '{wire.name}'"
)
self.implant = implant
self.min_implant_enclosure = min_implant_enclosure
if "mask" in widthspace_args:
raise TypeError("Diode got an unexpected keyword argument 'mask'")
else:
widthspace_args["mask"] = msk.Intersect(
prim.mask for prim in (wire, *indicator, implant)
).alias(f"diode:{name}")
super().__init__(name=name, **widthspace_args)
self.ports += (
_PrimitiveNet(prim=self, name=name)
for name in ("anode", "cathode")
)
if well is None:
if not wire.allow_in_substrate:
raise TypeError(f"wire '{wire.name}' has to be in a well")
# TODO: check types of substrate and implant
if min_well_enclosure is not None:
raise TypeError("min_well_enclosure given without a well")
else:
if well not in wire.well:
raise ValueError(
f"well '{well.name}' is not a valid well for wire '{wire.name}'"
)
if well.type_ == implant.type_:
raise ValueError(
f"type of implant '{implant.name}' may not be the same as"
" type of well '{well.name}' for a diode"
)
self.well = well
self.min_well_enclosure = min_well_enclosure
self.model = model
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
# Do not generate the default width/space rules.
yield from _Primitive._generate_rules(self, tech=tech)
# TODO: Can we provide proper type for self.mask ?
yield cast(msk.DesignMask, self.mask)
if self.min_width > self.wire.min_width:
yield self.mask.width >= self.min_width
if self.min_space > self.wire.min_space:
yield self.mask.space >= self.min_space
for i, ind in enumerate(self.indicator):
enc = self.min_indicator_enclosure[i]
yield self.wire.mask.enclosed_by(ind.mask) >= enc
if self.min_implant_enclosure is not None:
enc = self.min_implant_enclosure
yield self.mask.enclosed_by(self.implant.mask) >= enc
[docs]class MOSFETGate(_WidthSpacePrimitive):
"""MOSFETGate is a primitive representing the gate of a MOSFET transistor.
A self-aligned process is assumed for the MOSFET so the gate is basically
the area where a gate layer crosses the active layer. A dielectric layer
in between the two layers is forming the gate capacitor the is part of the
basic principles of how a MOSFET device funtions.
The gate has a seaparte primitive as it often is common between different
MOSFET devices (e.g. nmos and pmos or multi-Vt devices) with common rules.
Arguments:
name: optional name for the gate.
If not specified a unique name based on the layers is given
active: the bottom layer for the gate.
The part of the active layer under the gate layer is acting as the
channel of the MOSFET.
poly: the top layer of gate.
oxide: optionally an oxide layer can be given to have gate for different
types of devices.
If not specified it means to default oxide layer of the process is
present to form the
min_gateoxide_enclosure: optional minimum required enclosure of the gate
by the oxide layer
inside: optional marker layers for the gate.
This allows to specify alternative rules for a device that is
physically processed the same as another device.
Example use is the marking of ESD devices with own rules but
being physically the same device as the other higher voltage
devices.
min_gateinside_enclosure: optional minimum required enclosure of the gate
by the inside layer. If 1 value is specified it is used for all the
inside layers.
min_l: optional minimum l specification valid for all MOSFET devices
using this gate.
If not specified the minimum poly layer width will be used as
the minimum l.
min_w: optional minimum w specification valid for all MOSFET devices
using this gate.
If not specified the minimum active layer width will be used as
the minimum w.
min_sd_width: optional minimum extension of the active layer over
the gate.
min_polyactive_extension: optional minimum extension of the poly layer
over the gate.
min_gate_space: optional minimum spacing between two gates sharing
the same active wire
contact: optional contact layer for this device; this is needed to
allow to specify the minimum contact to gate spacing.
min_contactgate_space: optional common specification of minimum contact
to gate spacing
"""
class _ComputedProps:
def __init__(self, gate: "MOSFETGate"):
self.gate = gate
@property
def min_l(self) -> float:
min_l = self.gate.min_l
if min_l is None:
min_l = self.gate.poly.min_width
return min_l
@property
def min_w(self) -> float:
min_w = self.gate.min_w
if min_w is None:
min_w = self.gate.active.min_width
return min_w
@property
def min_gate_space(self) -> float:
s = self.gate.min_gate_space
if s is None:
s = self.gate.poly.min_space
return s
@property
def min_sd_width(self) -> Optional[float]:
return self.gate.min_sd_width
@property
def min_polyactive_extension(self) -> Optional[float]:
return self.gate.min_polyactive_extension
@property
def computed(self):
"""the computed property allows to get values for parameters that
were not specified during object init.
For example assume that `gate` is MOSFETGate object that did not
specify `min_l`. Then `gate.min_l` is `None` and `gate.computed.min_l`
is equal to `gate.poly.min_width`.
This separation is done to server different use cases. When looking
at DRC rules `gate.min_l` being `None` indicated no extra rule
needs to be generated for this gate. For layout it is easier to use
`gate.computed.min_l` to derive the dimension of the device to be
drawn.
"""
return MOSFETGate._ComputedProps(self)
def __init__(self, *, name: Optional[str]=None,
active: WaferWire, poly: GateWire,
oxide: Optional[Insulator]=None,
min_gateoxide_enclosure: Optional[prp.Enclosure]=None,
inside: OptSingleOrMulti[Marker].T=None,
min_gateinside_enclosure: OptSingleOrMulti[prp.Enclosure].T=None,
min_l: Optional[IntFloat]=None, min_w: Optional[IntFloat]=None,
min_sd_width: Optional[IntFloat]=None,
min_polyactive_extension: Optional[IntFloat]=None,
min_gate_space: Optional[IntFloat]=None,
contact: Optional[Via]=None,
min_contactgate_space: Optional[IntFloat]=None,
):
self.active = active
self.poly = poly
prims = (poly, active)
if oxide is not None:
if (active.oxide is None) or (oxide not in active.oxide):
raise ValueError(
f"oxide '{oxide.name}' is not valid for active '{active.name}'"
)
prims += (oxide,)
elif min_gateoxide_enclosure is not None:
raise TypeError("min_gateoxide_enclosure provided without an oxide")
self.oxide = oxide
self.min_gateoxide_enclosure = min_gateoxide_enclosure
if inside is not None:
inside = _util.v2t(inside)
prims += inside
if min_gateinside_enclosure is not None:
min_gateinside_enclosure = _util.v2t(min_gateinside_enclosure, n=len(inside))
elif min_gateinside_enclosure is not None:
raise TypeError("min_gateinside_enclosure provided without inside provided")
self.inside = inside
self.min_gateinside_enclosure = min_gateinside_enclosure
if name is None:
name = "gate({})".format(",".join(prim.name for prim in prims))
gatename = "gate:" + "+".join(prim.name for prim in prims)
else:
gatename = f"gate:{name}"
if min_l is not None:
min_l = _util.i2f(min_l)
self.min_l = min_l
else:
# local use only
min_l = poly.min_width
self.min_l = None
if min_w is not None:
min_w = _util.i2f(min_w)
self.min_w = min_w
else:
# local use only
min_w = active.min_width
self.min_w = None
if min_sd_width is not None:
min_sd_width = _util.i2f(min_sd_width)
self.min_sd_width = min_sd_width
if min_polyactive_extension is not None:
min_polyactive_extension = _util.i2f(min_polyactive_extension)
self.min_polyactive_extension = min_polyactive_extension
if min_gate_space is not None:
min_gate_space = _util.i2f(min_gate_space)
self.min_gate_space = min_gate_space
else:
# Local use only
min_gate_space = poly.min_space
self.min_gate_space = None
if min_contactgate_space is not None:
if contact is None:
raise TypeError(
"min_contactgate_space given without contact layer"
)
min_contactgate_space = _util.i2f(min_contactgate_space)
elif contact is not None:
raise TypeError(
"contact layer provided without min_contactgate_space specification"
)
self.contact = contact
self.min_contactgate_space = min_contactgate_space
mask = msk.Intersect(prim.mask for prim in prims).alias(gatename)
super().__init__(
name=name, mask=mask,
min_width=min(min_l, min_w), min_space=min_gate_space,
)
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
active_mask = self.active.mask
poly_mask = self.poly.conn_mask
# Update mask if it has no oxide
extra_masks = tuple()
if self.oxide is None:
extra_masks += tuple(
cast(Any, gate).oxide.mask for gate in filter(
lambda prim: (
isinstance(prim, MOSFETGate)
and prim.active == self.active
and prim.poly == self.poly
and (prim.oxide is not None)
), tech.primitives,
)
)
if self.inside is None:
def get_key(gate: "MOSFETGate"):
if gate.oxide is not None:
return frozenset((gate.active, gate.poly, gate.oxide))
else:
return frozenset((gate.active, gate.poly))
for gate in filter(
lambda prim: (
isinstance(prim, MOSFETGate)
and (get_key(prim) == get_key(self))
and prim.inside is not None
), tech.primitives,
):
extra_masks += tuple(inside.mask for inside in cast(Any, gate).inside)
masks = (active_mask, poly_mask)
if self.oxide is not None:
masks += (self.oxide.mask,)
if self.inside is not None:
masks += tuple(inside.mask for inside in self.inside)
if extra_masks:
masks += (wfr.outside(extra_masks),)
# Keep the alias but change the mask of the alias
cast(msk._MaskAlias, self.mask).mask = msk.Intersect(masks)
mask = self.mask
mask_used = False
rules: List[rle._Rule] = []
if self.min_l is not None:
rules.append(
edg.Intersect(
(edg.MaskEdge(active_mask), edg.MaskEdge(self.mask))
).length >= self.min_l,
)
if self.min_w is not None:
rules.append(
edg.Intersect(
(edg.MaskEdge(poly_mask), edg.MaskEdge(self.mask))
).length >= self.min_w,
)
if self.min_sd_width is not None:
rules.append(active_mask.extend_over(mask) >= self.min_sd_width)
mask_used = True
if self.min_polyactive_extension is not None:
rules.append(
poly_mask.extend_over(mask) >= self.min_polyactive_extension,
)
mask_used = True
if self.min_gate_space is not None:
rules.append(mask.space >= self.min_gate_space)
mask_used = True
if self.min_contactgate_space is not None:
assert self.contact is not None
rules.append(
msk.Spacing(mask, self.contact.mask) >= self.min_contactgate_space,
)
mask_used = True
if mask_used:
# This rule has to be put before the other rules that use the alias
yield cast(rle._Rule, mask)
yield from _MaskPrimitive._generate_rules(self, tech=tech, gen_mask=False)
yield from rules
[docs]class MOSFET(_Primitive):
"""MOSFET is a primitive representing a MOSFET transistor.
MOS stands for metal-oxide-semiconductor; see
https://en.wikipedia.org/wiki/MOSFET for explanation of a MOSFET device.
Arguments:
name: name for the gate.
gate: the `MOSFETGate` object for this device
implant: implant layers for this device
well: optional well in which this MOSFET needs to be located.
If gate.active can't be put in substrate well has to be
specified. If specified the well has to be valid for
gate.active and the implant type has to be opposite to the
implant types.
min_l: optional minimum l specification for the MOSFET device
If not specified the min_l of the gate will be used, which
in turn could be the gate poly layer minimum width.
min_w: optional minimum w specification valid the MOSFET device
If not specified the min_w of the gate will be used, which
in turn could be the gate active layer minimum width.
min_sd_width: optional minimum extension of the active layer over
the gate.
If not specified the value from the gate will be used.
This value has to be specified either here or for the gate.
min_polyactive_extension: optional minimum extension of the poly layer
over the gate.
If not specified the value from the gate will be used.
This value has to be specified either here or for the gate.
min_gateimplant_enclosure: SingleOrMulti[prp.Enclosure].T,
min_gate_space: optional minimum spacing between two gates sharing
the same active wire
If not specified the value from the gate will be used.
This value has to be specified either here or for the gate.
contact: optional contact layer for this device; this is needed to
allow to specify the minimum contact to gate spacing.
If not specified the value from the gate will be used.
min_contactgate_space: optional common specification of minimum contact
to gate spacing
If not specified the value from the gate will be used.
If neither the gate nor here contact is specified this parameter may
not be specified either. Otherwise here and/or the gate have to
speicify a value.
model: optional spice model name for the device. Needed when one wants
to do spice simulations or LVS.
"""
class _ComputedProps:
def __init__(self, mosfet: "MOSFET"):
self.mosfet = mosfet
def _lookup(self, name: str, allow_none: bool):
mosfet = self.mosfet
v = getattr(mosfet, name)
if v is None:
v = getattr(mosfet.gate.computed, name, None)
if v is None:
v = getattr(mosfet.gate, name, None)
if not allow_none:
assert v is not None, "needed attribute"
return v
@property
def min_l(self) -> float:
return cast(float, self._lookup("min_l", False))
@property
def min_w(self) -> float:
return cast(float, self._lookup("min_w", False))
@property
def min_sd_width(self) -> float:
return cast(float, self._lookup("min_sd_width", False))
@property
def min_polyactive_extension(self) -> float:
return cast(float, self._lookup("min_polyactive_extension", False))
@property
def min_gate_space(self) -> float:
return cast(float, self._lookup("min_gate_space", False))
@property
def contact(self) -> Optional[Via]:
return cast(Optional[Via], self._lookup("contact", True))
@property
def min_contactgate_space(self) -> float:
return cast(float, self._lookup("min_contactgate_space", False))
@property
def computed(self):
"""the computed property allows to get values for parameters that
were not specified during object init.
For example assume that `nmos` is MOSFET object that did not
specify `min_l`. Then `nmos.min_l` is `None` and `nmos.computed.min_l`
is equal to `nmos.gate.computed.min_l`, which can then
refer further to `nmos.gate.poly.min_width`.
This separation is done to server different use cases. When looking
at DRC rules `gate.min_l` being `None` indicated no extra rule
needs to be generated for this gate. For layout it is easier to use
`gate.computed.min_l` to derive the dimension of the device to be
drawn.
"""
return MOSFET._ComputedProps(self)
def __init__(
self, *, name: str,
gate: MOSFETGate, implant: SingleOrMulti[Implant].T,
well: Optional[Well]=None,
min_l: Optional[IntFloat]=None, min_w: Optional[IntFloat]=None,
min_sd_width: Optional[IntFloat]=None,
min_polyactive_extension: Optional[IntFloat]=None,
min_gateimplant_enclosure: SingleOrMulti[prp.Enclosure].T,
min_gate_space: Optional[IntFloat]=None,
contact: Optional[Via]=None,
min_contactgate_space: Optional[IntFloat]=None,
model: Optional[str]=None,
):
super().__init__(name=name)
implant = _util.v2t(implant)
type_ = None
for impl in implant:
if not (impl.type_ == "adjust"):
if type_ is None:
type_ = impl.type_
elif type_ != impl.type_:
raise ValueError(
"both n and p type implants for same MOSFET are not allowed"
)
if type_ is None:
raise ValueError(
"at least one n or p type implant has to be specified"
)
wrong = tuple(filter(
lambda impl: impl not in gate.active.implant,
implant
))
if wrong:
names = tuple(impl.name for impl in wrong)
raise ValueError(
f"implants {names} not valid for gate.active '{gate.active.name}'"
)
if well is None:
if not gate.active.allow_in_substrate:
raise ValueError(
f"well needed as gate active '{gate.active.name}'"
" can't be put in substrate"
)
else:
if well not in gate.active.well:
raise ValueError(
f"well '{well.name}' not valid for gate.active '{gate.active.name}'"
)
if type_ == well.type_:
raise ValueError("well and implant(s) have to be of different type")
self.gate = gate
self.implant = implant
self.well = well
if min_l is not None:
min_l = _util.i2f(min_l)
if min_l <= gate.computed.min_l:
raise ValueError("min_l has to be bigger than gate min_l if not 'None'")
self.min_l = min_l
if min_w is not None:
min_w = _util.i2f(min_w)
if min_w <= gate.computed.min_w:
raise ValueError("min_w has to be bigger than gate min_w if not 'None'")
self.min_w = min_w
if min_sd_width is not None:
min_sd_width = _util.i2f(min_sd_width)
elif gate.min_sd_width is None:
raise ValueError(
"min_sd_width neither provided for the transistor gate or the transistor",
)
self.min_sd_width = min_sd_width
if min_polyactive_extension is not None:
min_polyactive_extension = _util.i2f(min_polyactive_extension)
elif gate.min_polyactive_extension is None:
raise ValueError(
"min_polyactive_extension neither provided for the transistor gate"
" or the transistor",
)
self.min_polyactive_extension = min_polyactive_extension
self.min_gateimplant_enclosure = min_gateimplant_enclosure = _util.v2t(
min_gateimplant_enclosure, n=len(implant),
)
if min_gate_space is not None:
min_gate_space = _util.i2f(min_gate_space)
self.min_gate_space = min_gate_space
if min_contactgate_space is not None:
min_contactgate_space = _util.i2f(min_contactgate_space)
if contact is None:
if gate.contact is None:
raise ValueError(
"no contact layer provided for min_contactgate_space specification",
)
contact = gate.contact
elif contact is not None:
raise ValueError(
"contact layer provided without min_contactgate_space specification",
)
self.min_contactgate_space = min_contactgate_space
self.contact = contact
self.model = model
# MOSFET is symmetric so both diffusion regions can be source or drain
bulknet = (
_PrimitiveNet(prim=self, name="bulk") if well is not None
else wfr.SubstrateNet(name="bulk")
)
self.ports += (
_PrimitiveNet(prim=self, name="sourcedrain1"),
_PrimitiveNet(prim=self, name="sourcedrain2"),
_PrimitiveNet(prim=self, name="gate"),
bulknet,
)
for impl in implant:
try:
idx = gate.active.implant.index(impl)
except: # pragma: no cover
continue
else:
impl_act_enc = gate.active.min_implant_enclosure[idx]
break
else: # pragma: no cover
raise AssertionError("Internal error")
self.params += (
_Param(primitive=self, name="l", default=self.computed.min_l),
_Param(primitive=self, name="w", default=self.computed.min_w),
_EnclosureParam(
primitive=self, name="activeimplant_enclosure",
default=impl_act_enc,
),
_Param(primitive=self, name="sd_width", default=self.computed.min_sd_width),
_Param(
primitive=self, name="polyactive_extension",
default=self.computed.min_polyactive_extension,
),
_EnclosuresParam(
primitive=self, name="gateimplant_enclosures", n=len(implant),
default=min_gateimplant_enclosure,
),
)
spc = self.computed.min_gate_space
if spc is not None:
self.params += _Param(primitive=self, name="gate_space", default=spc)
if self.computed.contact is not None:
spc = self.computed.min_contactgate_space
assert spc is not None
self.params += _Param(primitive=self, name="contactgate_space", default=spc)
@property
def gate_prim(self) -> _Intersect:
"""gate_prim attribute is the primitive representing the gate of the MOSFET
object. Main reason it exists is to use it in rules; for example a minimum spacing
to the gate of a transistor.
"""
prims: Tuple[_MaskPrimitive, ...] = (self.gate, *self.implant)
if self.well is not None:
prims += (self.well,)
return _Intersect(prims=prims)
@property
def gate_mask(self):
return self._gate_mask
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
markers = (self.well.mask if self.well is not None else tech.substrate,)
if self.implant is not None:
markers += tuple(impl.mask for impl in self.implant)
derivedgate_mask = msk.Intersect((self.gate.mask, *markers)).alias(
f"gate:mosfet:{self.name}",
)
self._gate_mask = derivedgate_mask
derivedgate_edge = edg.MaskEdge(derivedgate_mask)
poly_mask = self.gate.poly.mask
poly_edge = edg.MaskEdge(poly_mask)
channel_edge = edg.Intersect((derivedgate_edge, poly_edge))
active_mask = self.gate.active.mask
active_edge = edg.MaskEdge(active_mask)
fieldgate_edge = edg.Intersect((derivedgate_edge, active_edge))
yield derivedgate_mask
if self.min_l is not None:
yield edg.Intersect(
(derivedgate_edge, active_edge),
).length >= self.min_l
if self.min_w is not None:
yield edg.Intersect(
(derivedgate_edge, poly_edge),
).length >= self.min_w
if self.min_sd_width is not None:
yield (
active_mask.extend_over(derivedgate_mask) >= self.min_sd_width
)
if self.min_polyactive_extension is not None:
yield (
poly_mask.extend_over(derivedgate_mask)
>= self.min_polyactive_extension
)
for i in range(len(self.implant)):
impl_mask = self.implant[i].mask
enc = self.min_gateimplant_enclosure[i]
if not enc.is_assymetric:
yield derivedgate_mask.enclosed_by(impl_mask) >= enc
else:
yield channel_edge.enclosed_by(impl_mask) >= enc.spec[0]
yield fieldgate_edge.enclosed_by(impl_mask) >= enc.spec[1]
if self.min_gate_space is not None:
yield derivedgate_mask.space >= self.min_gate_space
if self.min_contactgate_space is not None:
assert self.contact is not None
yield (
msk.Spacing(derivedgate_mask, self.contact.mask)
>= self.min_contactgate_space
)
@property
def designmasks(self):
yield from super().designmasks
yield from self.gate.designmasks
if self.implant is not None:
for impl in self.implant:
yield from impl.designmasks
if self.well is not None:
yield from self.well.designmasks
if self.contact is not None:
if (self.gate.contact is None) or (self.contact != self.gate.contact):
yield from self.contact.designmasks
[docs]class Bipolar(_Primitive):
"""The Bipolar primitive represents the bipolar injunction transistors.
It's thus a PNP or a NPN device.
For more info see https://en.wikipedia.org/wiki/Bipolar_junction_transistor
Currently no layout generation for this device is implemented and the
technology will need to provide fixed layout implementations. Bipolar
devices are assumed to have fixed layouts for each technology.
Arguments:
name: name of the Bipolar device
type_: the bipolar type; has to be 'npn' or 'pnp'
model: the optional (spice) model name for the device
indicator: the layer(s) to mark a certain structure as a bipolar device
API Notes:
Bipolar does not have a fixed API yet. Backwards incompatible changes
are reserved for implemting more general layout generation.
"""
# TODO: add the specification for WaferWire and implants with which the
# collector, base and emittor of the device are made.
def __init__(self, *,
name: str, type_: str, model: Optional[str]=None, is_subcircuit=False,
indicator: SingleOrMulti[Marker].T,
):
if type_ not in ("npn", "pnp"):
raise ValueError(f"type_ hos to be 'pnp' or 'npn' not '{type_}'")
super().__init__(name=name)
self.type_ = type_
self.model = model
self.is_subcircuit = is_subcircuit
self.indicator = _util.v2t(indicator)
self.ports += (
_PrimitiveNet(prim=self, name="collector"),
_PrimitiveNet(prim=self, name="base"),
_PrimitiveNet(prim=self, name="emitter"),
)
def _generate_rules(self, *, tech: tch.Technology) -> Iterable[rle._Rule]:
return super()._generate_rules(tech=tech)
@property
def designmasks(self) -> Iterable[msk.DesignMask]:
yield from super().designmasks
for indicator in self.indicator:
yield from indicator.designmasks
class _RulePrimitive(_Primitive):
"""Subclasses of _RulePrimitive represent extra design rules without further
physical representation of a Primitive. They thus don't have a layout etc.
It's a base class that needs to be subclassed.
"""
pass
[docs]class Spacing(_RulePrimitive):
"""A _RulePrimitive that allows to define extra minimum space requirement
that is not derived from the parameters from any of the primitives in the
a technology
Argumnets:
primitives1: first set of primitives
If primitives2 is not provided this set has to contain more than
one primitive and the minimum space requirement is for the
combined shape of joining all shapes in all the layers in this
set.
primitives2: optinal second set of primitives
If this set is provided the minimum space specification is for
each shape on a layer in primitives1 to each shape on a layer
in primitives2.
min_space: the minimum space specifcation
"""
def __init__(self, *,
primitives1: SingleOrMulti[_MaskPrimitive].T,
primitives2: Optional[SingleOrMulti[_MaskPrimitive].T]=None,
min_space: IntFloat,
):
primitives1 = _util.v2t(primitives1)
primitives2 = _util.v2t(primitives2) if primitives2 is not None else None
min_space = _util.i2f(min_space)
if primitives2 is not None:
name = "Spacing({},{:.6})".format(
",".join(
(
prims[0].name if len(prims) == 1
else "({})".format(",".join(prim.name for prim in prims))
) for prims in (primitives1, primitives2)
),
min_space,
)
else:
s_prim1 = (
primitives1[0].name if len(primitives1) == 1
else "({})".format(",".join(prim.name for prim in primitives1))
)
name = f"Spacing({s_prim1},None,{min_space:.6})"
super().__init__(name=name)
self.primitives1 = primitives1
self.primitives2 = primitives2
self.min_space = min_space
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
if self.primitives2 is None:
joined = msk.Join(prim1.mask for prim1 in self.primitives1)
yield joined.space >= self.min_space
else:
yield from (
msk.Spacing(prim1.mask, prim2.mask) >= self.min_space
for prim1, prim2 in product(self.primitives1, self.primitives2)
)
@property
def designmasks(self):
yield from super().designmasks
if self.primitives2 is not None:
for prim in (*self.primitives1, *self.primitives2):
yield from prim.designmasks
else:
for prim in self.primitives1:
yield from prim.designmasks
def __repr__(self):
return self.name
class Enclosure(_RulePrimitive):
"""A _RulePrimitive that allows to define extra minimum enclosure
requirement that is not derived from the parameters from any of the
primitives in a technology.
Argumnets:
prim: the base primitive
by: the enclosing primitive
min_enclosure: the minimum `Enclosure` of `prim` by `by`
"""
def __init__(self, *,
prim: _MaskPrimitive, by: _MaskPrimitive, min_enclosure: prp.Enclosure,
):
name = f"Enclosure(prim={prim!r},by={by!r},min_enclosure={min_enclosure!r})"
super().__init__(name=name)
self.prim = prim
self.by = by
self.min_enclosure = min_enclosure
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
yield self.prim.mask.enclosed_by(self.by.mask) >= self.min_enclosure
@property
def designmasks(self) -> Generator[msk.DesignMask, None, None]:
yield from super().designmasks
yield from self.prim.designmasks
yield from self.by.designmasks
def __repr__(self) -> str:
return self.name
class NoOverlap(_RulePrimitive):
"""A _RulePrimitive that allows to define extra no overlap
requirement that is not derived from the parameters from any of the
primitives in a technology.
Argumnets:
prim1, prim2: the two primitives where none of the shape may
have a overlapping part.
"""
def __init__(self, *, prim1: _MaskPrimitive, prim2: _MaskPrimitive):
name = f"NoOverlap(prim1={prim1!r},prim2={prim2!r})"
super().__init__(name=name)
self.prim1 = prim1
self.prim2 = prim2
def _generate_rules(self, *,
tech: tch.Technology,
) -> Generator[rle._Rule, None, None]:
yield from super()._generate_rules(tech=tech)
intersect = _Intersect(prims=(self.prim1, self.prim2))
yield intersect.mask.area == 0.0
@property
def designmasks(self) -> Generator[msk.DesignMask, None, None]:
yield from super().designmasks
yield from self.prim1.designmasks
yield from self.prim2.designmasks
def __repr__(self) -> str:
return self.name
class Primitives(_util.TypedListStrMapping[_Primitive]):
"""A collection of `_Primitive` objects"""
@property
def _elem_type_(self):
return _Primitive
def __iadd__(self, x: SingleOrMulti[_Primitive].T) -> "Primitives":
x = _util.v2t(x)
for elem in x:
if isinstance(elem, _DerivedPrimitive):
raise TypeError(
f"_DerivedPrimitive '{elem.name}' can't be added to 'Primitives'",
)
if elem in self:
raise ValueError(
f"Adding primitive with name '{elem.name}' twice"
)
return cast("Primitives", super().__iadd__(x))
[docs]class UnusedPrimitiveError(Exception):
"""Exception used by `Technology` when checking the primitives list
of a technology"""
def __init__(self, primitive: _Primitive):
super().__init__(
f"primitive '{primitive.name}' defined but not used"
)
class UnusedMIMTop(Exception):
"""Exception used by `Technology` when checking the primitives list
of a technology"""
def __init__(self, mimtop: MIMTop):
super().__init__(
f"MIMTop '{mimtop.name}' not used as a top in a MIMCapacitor"
)
[docs]class UnconnectedPrimitiveError(Exception):
"""Exception used by `Technology` when checking the primitives list
of a technology"""
def __init__(self, primitive: _Primitive):
super().__init__(
f"primitive '{primitive.name}' is not connected"
)