# SPDX-License-Identifier: GPL-2.0-or-later OR AGPL-3.0-or-later OR CERN-OHL-S-2.0+
"""The pdkmaster.design.layout module provides classes to represent layout shapes
in a PDKMaster technology. These classes are designed to only allow to create
layout that conform to the technology definition. In order to detect design
shorts as fast as possible shapes are put on nets.
A LayoutFactory class is provided to generate layouts for a certain technology and
it's primitives.
Internally the klayout API is used to represent the shapes and perform manipulations
on them.
"""
import abc
from pdkmaster.typing import IntFloat, SingleOrMulti
from typing import (
Any, Iterable, Generator, Sequence, Mapping, Tuple, Dict, Optional, Union,
Type, cast, overload,
)
from .. import _util, dispatch as dsp
from ..technology import (
property_ as prp, net as net_, mask as msk, geometry as geo,
primitive as prm, technology_ as tch,
)
from . import circuit as ckt
__all__ = [
"MaskShapesSubLayout",
"SubLayouts",
"LayoutFactory",
]
class NetOverlapError(Exception):
pass
def _rect(
left: float, bottom: float, right: float, top: float, *,
enclosure: Optional[Union[float, Sequence[float], prp.Enclosure]]=None,
) -> geo.Rect:
"""undocumented deprecated function;
see: https://gitlab.com/Chips4Makers/PDKMaster/-/issues/39
"""
if enclosure is not None:
if isinstance(enclosure, prp.Enclosure):
enclosure = enclosure.spec
if isinstance(enclosure, float):
left -= enclosure
bottom -= enclosure
right += enclosure
top += enclosure
else:
left -= enclosure[0]
bottom -= enclosure[1]
right += enclosure[0]
top += enclosure[1]
return geo.Rect(
left=left, bottom=bottom, right=right, top=top,
)
def _via_array(
left: float, bottom: float, width: float, pitch: float, rows: int, columns: int,
):
"""undocumented deprecated function;
see: https://gitlab.com/Chips4Makers/PDKMaster/-/issues/39
"""
via = geo.Rect.from_size(width=width, height=width)
xy0 = geo.Point(x=(left + 0.5*width), y=(bottom + 0.5*width))
if (rows == 1) and (columns == 1):
return via + xy0
else:
return geo.ArrayShape(
shape=via, offset0=xy0, rows=rows, columns=columns, pitch_x=pitch, pitch_y=pitch,
)
class _SubLayout(abc.ABC):
"""Internal `_Layout` support class"""
@abc.abstractmethod
def __init__(self):
... # pragma: no cover
@property
@abc.abstractmethod
def polygons(self) -> Iterable[geo.MaskShape]:
... # pragma: no cover
@abc.abstractmethod
def dup(self) -> "_SubLayout":
... # pragma: no cover
@abc.abstractmethod
def move(self, *,
dxy: geo.Point, move_context: Optional[geo.MoveContext]=None,
) -> None:
... # pragma: no cover
@abc.abstractmethod
def moved(self, *,
dxy: geo.Point, move_context: Optional[geo.MoveContext]=None,
) -> "_SubLayout":
... # pragma: no cover
@abc.abstractmethod
def rotate(self, *,
rotation: geo.Rotation, rot_context: Optional[geo.RotationContext]=None,
) -> None:
... # pragma: no cover
@abc.abstractmethod
def rotated(self, *,
rotation: geo.Rotation, rot_context: Optional[geo.RotationContext]=None,
) -> "_SubLayout":
... # pragma: no cover
@property
@abc.abstractmethod
def _hier_strs_(self) -> Generator[str, None, None]:
... # pragma: no cover
[docs]class MaskShapesSubLayout(_SubLayout):
"""Object representing the sublayout of a net consisting of geometry._Shape
objects.
Arguments:
net: The net of the SubLayout
`None` value represents no net for the shapes.
shapes: The maskshapes on the net.
"""
def __init__(self, *, net: Optional[net_.Net], shapes: geo.MaskShapes):
self._net = net
self._shapes = shapes
@property
def net(self) -> Optional[net_.Net]:
return self._net
@property
def shapes(self) -> geo.MaskShapes:
return self._shapes
@property
def polygons(self) -> Generator[geo.MaskShape, None, None]:
yield from self.shapes
[docs] def add_shape(self, *, shape: geo.MaskShape):
self._shapes += shape
[docs] def move(self, *,
dxy: geo.Point, move_context: Optional[geo.MoveContext]=None,
) -> None:
self._shapes = self.shapes.moved(dxy=dxy, context=move_context)
[docs] def moved(self, *,
dxy: geo.Point, move_context: Optional[geo.MoveContext]=None,
) -> "MaskShapesSubLayout":
return MaskShapesSubLayout(
net=self.net, shapes=self.shapes.moved(dxy=dxy, context=move_context),
)
[docs] def rotate(self, *,
rotation: geo.Rotation, rot_context: Optional[geo.RotationContext]=None,
) -> None:
self.shapes.rotate(rotation=rotation, context=rot_context)
[docs] def rotated(self, *,
rotation: geo.Rotation, rot_context: Optional[geo.RotationContext]=None,
) -> "MaskShapesSubLayout":
return MaskShapesSubLayout(
net=self.net,
shapes=self.shapes.rotated(rotation=rotation, context=rot_context),
)
[docs] def dup(self) -> "MaskShapesSubLayout":
return MaskShapesSubLayout(
net=self.net, shapes=geo.MaskShapes(self.shapes),
)
@property
def _hier_strs_(self) -> Generator[str, None, None]:
yield f"MaskShapesSubLayout net={self.net}"
for ms in self.shapes:
yield " " + str(ms)
def __hash__(self):
return hash((self.net, self.shapes))
def __eq__(self, other: object) -> bool:
if isinstance(other, MaskShapesSubLayout):
return (self.net == other.net) and (self.shapes == other.shapes)
else:
return False
class _InstanceSubLayout(_SubLayout):
"""Internal `_Layout` support class"""
def __init__(self, *,
inst: ckt._CellInstance, origin: geo.Point,
layoutname: Optional[str], rotation: geo.Rotation,
):
self.inst = inst
self.origin = origin
self.rotation = rotation
cell = inst.cell
if layoutname is None:
try:
# Create default layout
cell.layout
except:
raise ValueError(
f"Cell '{cell.name}' has no default layout and no layoutname"
" was specified"
)
else:
if layoutname not in cell.layouts.keys():
raise ValueError(
f"Cell '{cell.name}' has no layout named '{layoutname}'"
)
self.layoutname = layoutname
# layout is a property and will only be looked up the first time it is accessed.
# This is to support cell with delayed layout generation.
self._layout = None
@property
def layout(self) -> "_Layout":
if self._layout is None:
l: "_Layout" = (
self.inst.cell.layouts[self.layoutname]
if self.layoutname is not None
else self.inst.cell.layout
)
self._layout = l.rotated(rotation=self.rotation).moved(dxy=self.origin)
return self._layout
@property
def boundary(self) -> Optional[geo._Rectangular]:
l = (
self.inst.cell.layouts[self.layoutname]
if self.layoutname is not None
else self.inst.cell.layout
)
if l.boundary is None:
return None
else:
return l.boundary.rotated(rotation=self.rotation) + self.origin
@property
def polygons(self) -> Generator[geo.MaskShape, None, None]:
yield from self.layout.polygons
def dup(self):
return self
def move(self, *,
dxy: geo.Point, move_context: Optional[geo.MoveContext]=None,
) -> None:
self.origin += dxy
self._layout = None
def moved(self, *,
dxy: geo.Point, move_context: Optional[geo.MoveContext]=None,
) -> "_InstanceSubLayout":
orig = self.origin + dxy
return _InstanceSubLayout(
inst=self.inst, origin=orig, layoutname=self.layoutname, rotation=self.rotation,
)
def rotate(self, *,
rotation: geo.Rotation, rot_context: Optional[geo.RotationContext]=None,
) -> None:
self.origin = rotation*self.origin
self.rotation *= rotation
def rotated(self, *,
rotation: geo.Rotation, rot_context: Optional[geo.RotationContext]=None,
) -> "_InstanceSubLayout":
p = rotation*self.origin
rot = rotation*self.rotation
return _InstanceSubLayout(
inst=self.inst, origin=p, layoutname=self.layoutname, rotation=rot,
)
@property
def _hier_strs_(self) -> Generator[str, None, None]:
yield f"_InstanceSubLayout inst={self.inst}, origin={self.origin}, rot={self.rotation}"
for s in self.layout._hier_strs_:
yield " " + s
[docs]class SubLayouts(_util.TypedList[_SubLayout]):
"""Internal `_Layout` support class"""
@property
def _elem_type_(self):
return _SubLayout
def __init__(self, iterable: SingleOrMulti[_SubLayout].T=tuple()):
if isinstance(iterable, _SubLayout):
super().__init__((iterable,))
else:
super().__init__(iterable)
nets = tuple(sl.net for sl in self.__iter_type__(MaskShapesSubLayout))
if len(nets) != len(set(nets)):
raise ValueError("Multiple `MaskShapesSubLayout` for same net")
[docs] def dup(self) -> "SubLayouts":
return SubLayouts(l.dup() for l in self)
def __iadd__(self, other_: SingleOrMulti[_SubLayout].T) -> "SubLayouts":
other: Iterable[_SubLayout]
if isinstance(other_, _SubLayout):
other = (other_,)
else:
other = tuple(other_)
# Now try to add to other sublayouts
def add2other(other_sublayout):
if isinstance(other_sublayout, MaskShapesSubLayout):
for sublayout in self.__iter_type__(MaskShapesSubLayout):
if sublayout.net == other_sublayout.net:
for shape in other_sublayout.shapes:
sublayout.add_shape(shape=shape)
return True
else:
return False
elif not isinstance(other_sublayout, _InstanceSubLayout): # pragma: no cover
raise RuntimeError("Internal error")
other = tuple(filter(lambda sl: not add2other(sl), other))
if other:
# Append remaining sublayouts
self.extend(sl.dup() for sl in other)
return self
def __add__(self, other: SingleOrMulti[_SubLayout].T) -> "SubLayouts":
ret = self.dup()
ret += other
return ret
class _Layout:
"""A `_Layout` object contains the shapes making up the layout of a design.
Contrary to other EDA layout tools all shapes are put on a net or are netless.
Netless are only allowed on mask derived from certain primitives.
`LayoutFactory.new_layout()` needs to be used to generate new layouts.
Attributes:
fab: the factory with which this _layout is created
sublayouts: the sublayouts making up this layout
boundary: optional boundary of this layout
"""
def __init__(self, *,
fab: "LayoutFactory",
sublayouts: SubLayouts, boundary: Optional[geo._Rectangular]=None,
):
self.fab = fab
self.sublayouts = sublayouts
self.boundary = boundary
@property
def polygons(self) -> Iterable[geo.MaskShape]:
"""All the `MaskShape` polygons of this layout.
Typically use case is exporting to a format that has no net information.
"""
for sublayout in self.sublayouts:
yield from sublayout.polygons
def _net_sublayouts(self, *, net: net_.Net, depth: Optional[int]) -> Generator[
MaskShapesSubLayout, None, None,
]:
"""Filter polygons; API to be fixed.
see: https://gitlab.com/Chips4Makers/PDKMaster/-/issues/34
"""
for sl in self.sublayouts:
if isinstance(sl, _InstanceSubLayout): # pragma: no cover
# TODO: add code coverage when finalizing API
# see: https://gitlab.com/Chips4Makers/PDKMaster/-/issues/34
assert isinstance(net, ckt._CircuitNet)
if depth != 0:
for port in net.childports:
if (
isinstance(port, ckt._InstanceNet)
and (port.inst == sl.inst)
):
yield from sl.layout._net_sublayouts(
net=port.net,
depth=(None if depth is None else (depth - 1)),
)
elif isinstance(sl, MaskShapesSubLayout):
if net == sl.net:
yield sl
else: # pragma: no cover
raise AssertionError("Internal error")
def net_polygons(self, net: net_.Net, *, depth: Optional[int]=None) -> Generator[
geo.MaskShape, None, None
]:
"""Filter polygons; API to be fixed.
see: https://gitlab.com/Chips4Makers/PDKMaster/-/issues/34
"""
for sl in self._net_sublayouts(net=net, depth=depth):
yield from sl.shapes
def filter_polygons(self, *,
net: Optional[net_.Net]=None, mask: Optional[msk._Mask]=None,
split: bool=False, depth: Optional[int]=None,
) -> Generator[geo.MaskShape, None, None]:
"""Filter polygons; API to be fixed.
see: https://gitlab.com/Chips4Makers/PDKMaster/-/issues/34
"""
if net is None:
sls = self.sublayouts
else:
sls = self._net_sublayouts(net=net, depth=depth)
for sl in sls:
assert isinstance(sl, MaskShapesSubLayout)
if mask is None:
shapes = sl.shapes
else:
shapes = filter(lambda sh: sh.mask == mask, sl.shapes)
if not split:
yield from shapes
else:
for shape in shapes:
for shape2 in shape.shape.pointsshapes:
yield geo.MaskShape(mask=shape.mask, shape=shape2)
def dup(self) -> "_Layout":
"""Create a duplication of a layout."""
return _Layout(
fab=self.fab,
sublayouts=SubLayouts(sl.dup() for sl in self.sublayouts),
boundary=self.boundary,
)
def bounds(self, *,
mask: Optional[msk._Mask]=None, net: Optional[net_.Net]=None,
depth: Optional[int]=None,
) -> geo.Rect:
"""Return the rectangle enclosing selected shapes; filtering of the
shapes is done based on the given arguments.
Arguments:
mask: only shapes on this mask are selected
net: only shapes on this net are selected
depth: when specified only shapes until a certain hierarchy depth
are selected.
"""
if net is None:
if depth is not None:
raise TypeError(
f"depth has to 'None' if net is 'None'"
)
polygons = self.polygons
else:
polygons = self.net_polygons(net, depth=depth)
mps = polygons if mask is None else filter(
lambda mp: mp.mask == mask, polygons,
)
boundslist = tuple(mp.bounds for mp in mps)
return geo.Rect(
left=min(bds.left for bds in boundslist),
bottom=min(bds.bottom for bds in boundslist),
right=max(bds.right for bds in boundslist),
top=max(bds.top for bds in boundslist),
)
def __iadd__(self, other: Union["_Layout", _SubLayout, SubLayouts]):
if self.sublayouts._frozen_:
raise ValueError("Can't add sublayouts to a frozen 'Layout' object")
self.sublayouts += (
other.sublayouts if isinstance(other, _Layout) else other
)
return self
@overload
def add_primitive(self, prim: prm._Primitive, *,
origin: Optional[geo.Point]=None, x: None=None, y: None=None,
rotation: geo.Rotation=geo.Rotation.R0,
**prim_params,
) -> "_Layout":
... # pragma: no cover
@overload
def add_primitive(self, prim: prm._Primitive, *,
origin: None=None, x: IntFloat, y: IntFloat,
rotation: geo.Rotation=geo.Rotation.R0,
**prim_params,
) -> "_Layout":
... # pragma: no cover
def add_primitive(self, prim: prm._Primitive, *,
origin: Optional[geo.Point]=None,
x: Optional[IntFloat]=None, y: Optional[IntFloat]=None,
rotation: geo.Rotation=geo.Rotation.R0,
**prim_params,
) -> "_Layout":
"""Add the layout for a primitive to a layout. It uses the layout
generated with `_Layout.layout_primitive()` and places it in the current
layout at a specified location and with a specified rotation.
Arguments:
prim: the primitive for which the generate and place the layout
prim_params: the parameters for the primitive
This is passed to `_Layout.layout_primitive()`.
origin or x, y: origin where to place the primitive layout
rotation: the rotation to apply on the generated primitive layout
before it is placed. By default no rotation is done.
"""
if not (prim in self.fab.tech.primitives):
raise ValueError(
f"prim '{prim.name}' is not a primitive of technology"
f" '{self.fab.tech.name}'"
)
# Translate possible x/y specification to origin
if origin is None:
x = 0.0 if x is None else _util.i2f(x)
y = 0.0 if y is None else _util.i2f(y)
origin = geo.Point(x=x, y=y)
primlayout = self.fab.layout_primitive(prim, **prim_params)
primlayout.rotate(rotation=rotation)
primlayout.move(dxy=origin)
self += primlayout
return primlayout
def add_maskshape(self, *, net: Optional[net_.Net]=None, maskshape: geo.MaskShape):
"""Add a geometry MaskShape to a _Layout
This is a low-level layout manipulation method that does not do much checking.
"""
for sl in self.sublayouts.__iter_type__(MaskShapesSubLayout):
if sl.net == net:
sl.add_shape(shape=maskshape)
break
else:
self.sublayouts += MaskShapesSubLayout(
net=net, shapes=geo.MaskShapes(maskshape),
)
def add_shape(self, *,
prim: prm._DesignMaskPrimitive, net: Optional[net_.Net]=None, shape: geo._Shape,
):
"""Add a geometry _Shape to a _Layout
This is a low-level layout manipulation method that does not do much checking.
"""
self.add_maskshape(
net=net,
maskshape=geo.MaskShape(mask=cast(msk.DesignMask, prim.mask), shape=shape),
)
def move(self, *, dxy: geo.Point) -> None:
"""Move the shapes in the layout by the given displacement.
This method changes the layout on which this method is called.
Arguments:
dxy: the displacement to apply to all the shapes in this layout
"""
move_context = geo.MoveContext()
for sl in self.sublayouts:
sl.move(dxy=dxy, move_context=move_context)
if self.boundary is not None:
self.boundary += dxy
def moved(self, *, dxy: geo.Point,
) -> "_Layout":
"""Return _Layout with all shapes moved by the given displacement.
The original layout is not changed,
Arguments:
dxy: the displacement to apply to all the shapes in this layout
"""
move_context = geo.MoveContext()
if self.boundary is None:
bound = None
else:
bound = self.boundary.moved(dxy=dxy, context=move_context)
return _Layout(
fab=self.fab, sublayouts=SubLayouts(
sl.moved(dxy=dxy, move_context=move_context)
for sl in self.sublayouts
),
boundary=bound,
)
def rotate(self, *, rotation: geo.Rotation):
"""Rotate the shapes in the layout by the given rotation.
This method changes the layout on which this method is called.
Arguments:
rotation: the rotation to apply to all the shapes in this layout
"""
rot_context = geo.RotationContext()
if self.boundary is not None:
self.boundary = self.boundary.rotated(rotation=rotation, context=rot_context)
self.sublayouts=SubLayouts(
sl.rotated(rotation=rotation, rot_context=rot_context)
for sl in self.sublayouts
)
def rotated(self, *, rotation: geo.Rotation):
"""Return _Layout with all shapes rotated by the given rotation.
The original layout is not changed,
Arguments:
rotation: the rotation to apply to all the shapes in this layout
"""
rot_context = geo.RotationContext()
if self.boundary is None:
bound = None
else:
bound = self.boundary.rotated(rotation=rotation, context=rot_context)
return _Layout(
fab=self.fab, sublayouts=SubLayouts(
sl.rotated(rotation=rotation, rot_context=rot_context)
for sl in self.sublayouts
),
boundary=bound,
)
def freeze(self):
"""see: https://gitlab.com/Chips4Makers/PDKMaster/-/issues/37"""
self.sublayouts._freeze_()
@property
def _hier_str_(self) -> str:
"""Return a string representing the full hierarchy of the layout.
Indentation is used to represent the hierarchy.
API Notes:
This is for debuggin purposes only and user code should not depend
on the exact format of this string.
"""
return "\n ".join(("layout:", *(s for s in self._hier_strs_)))
@property
def _hier_strs_(self) -> Generator[str, None, None]:
for sl in self.sublayouts:
yield from sl._hier_strs_
def __eq__(self, other: Any):
if isinstance(other, _Layout):
return self.sublayouts == other.sublayouts
else:
return False
class _PrimitiveLayouter(dsp.PrimitiveDispatcher):
"""Support class to generate layout for a `_Primitive`.
TODO: Proper docs after fixing the API.
see https://gitlab.com/Chips4Makers/PDKMaster/-/issues/25
API Notes:
The API is not finalized yet; backwards incompatible changes are still
expected.
"""
def __init__(self, fab: "LayoutFactory"):
self.fab = fab
def __call__(self, prim: prm._Primitive, *args, **kwargs) -> _Layout:
return super().__call__(prim, *args, **kwargs)
@property
def tech(self):
return self.fab.tech
# Dispatcher implementation
def _Primitive(self, prim: prm._Primitive, **params):
raise NotImplementedError(
f"Don't know how to generate minimal layout for primitive '{prim.name}'\n"
f"of type '{prim.__class__.__name__}'"
)
def Marker(self, prim: prm.Marker, **params) -> _Layout:
if ("width" in params) and ("height" in params):
return self._WidthSpacePrimitive(cast(prm._WidthSpacePrimitive, prim), **params)
else:
return super().Marker(prim, **params)
def _WidthSpacePrimitive(self,
prim: prm._WidthSpacePrimitive, **widthspace_params,
) -> _Layout:
if len(prim.ports) != 0: # pragma: no cover
raise NotImplementedError(
f"Don't know how to generate minimal layout for primitive '{prim.name}'\n"
f"of type '{prim.__class__.__name__}'"
)
width = widthspace_params["width"]
height = widthspace_params["height"]
r = geo.Rect.from_size(width=width, height=height)
l = self.fab.new_layout()
assert isinstance(prim, prm._DesignMaskPrimitive)
l.add_shape(prim=prim, shape=r)
return l
def _WidthSpaceConductor(self,
prim: prm._WidthSpaceConductor, **conductor_params,
) -> _Layout:
assert (
(len(prim.ports) == 1) and (prim.ports[0].name == "conn")
), "Internal error"
width = conductor_params["width"]
height = conductor_params["height"]
r = geo.Rect.from_size(width=width, height=height)
try:
portnets = conductor_params["portnets"]
except KeyError:
net = prim.ports.conn
else:
net = portnets["conn"]
layout = self.fab.new_layout()
layout.add_shape(prim=prim, net=net, shape=r)
pin = conductor_params.get("pin", None)
if pin is not None:
layout.add_shape(prim=pin, net=net, shape=r)
return layout
def WaferWire(self, prim: prm.WaferWire, **waferwire_params) -> _Layout:
width = waferwire_params["width"]
height = waferwire_params["height"]
implant = waferwire_params.pop("implant")
implant_enclosure = waferwire_params.pop("implant_enclosure")
assert implant_enclosure is not None
well = waferwire_params.pop("well", None)
well_enclosure = waferwire_params.pop("well_enclosure", None)
oxide = waferwire_params.pop("oxide", None)
oxide_enclosure = waferwire_params.pop("oxide_enclosure", None)
layout = self._WidthSpaceConductor(prim, **waferwire_params)
layout.add_shape(prim=implant, shape=_rect(
-0.5*width, -0.5*height, 0.5*width, 0.5*height,
enclosure=implant_enclosure,
))
if well is not None:
try:
well_net = waferwire_params["well_net"]
except KeyError:
raise TypeError(
f"No well_net given for WaferWire '{prim.name}' in well '{well.name}'"
)
layout.add_shape(prim=well, net=well_net, shape=_rect(
-0.5*width, -0.5*height, 0.5*width, 0.5*height,
enclosure=well_enclosure,
))
if oxide is not None:
layout.add_shape(prim=oxide, shape=_rect(
-0.5*width, -0.5*height, 0.5*width, 0.5*height,
enclosure=oxide_enclosure,
))
return layout
def MIMTop(self, prim: prm.MIMTop, **_):
raise ValueError("No generation of MIMTop layer; use MIMCapacitor instead")
def Via(self, prim: prm.Via, **via_params) -> _Layout:
tech = self.tech
try:
portnets = via_params["portnets"]
except KeyError:
net = prim.ports["conn"]
else:
if set(portnets.keys()) != {"conn"}:
raise ValueError(f"Via '{prim.name}' needs one net for the 'conn' port")
net = portnets["conn"]
bottom = via_params["bottom"]
bottom_enc = via_params["bottom_enclosure"]
if (bottom_enc is None) or isinstance(bottom_enc, str):
idx = prim.bottom.index(bottom)
enc = prim.min_bottom_enclosure[idx]
if bottom_enc is None:
bottom_enc = enc
elif bottom_enc == "wide":
bottom_enc = enc.wide()
else:
assert bottom_enc == "tall"
bottom_enc = enc.tall()
assert isinstance(bottom_enc, prp.Enclosure)
bottom_enc_x = bottom_enc.spec[0]
bottom_enc_y = bottom_enc.spec[1]
top = via_params["top"]
top_enc = via_params["top_enclosure"]
if (top_enc is None) or isinstance(top_enc, str):
idx = prim.top.index(top)
enc = prim.min_top_enclosure[idx]
if top_enc is None:
top_enc = enc
elif top_enc == "wide":
top_enc = enc.wide()
else:
assert top_enc == "tall"
top_enc = enc.tall()
assert isinstance(top_enc, prp.Enclosure)
top_enc_x = top_enc.spec[0]
top_enc_y = top_enc.spec[1]
width = prim.width
space = via_params["space"]
pitch = width + space
rows = via_params["rows"]
bottom_height = via_params["bottom_height"]
top_height = via_params["top_height"]
if rows is None:
if bottom_height is None:
assert top_height is not None
rows = int(self.tech.on_grid(top_height - 2*top_enc_y - width)//pitch + 1)
via_height = rows*pitch - space
bottom_height = tech.on_grid(
via_height + 2*bottom_enc_y, mult=2, rounding="ceiling",
)
else:
rows = int(self.tech.on_grid(bottom_height - 2*bottom_enc_y - width)//pitch + 1)
if top_height is not None:
rows = min(
rows,
int(self.tech.on_grid(top_height - 2*top_enc_y - width)//pitch + 1),
)
via_height = rows*pitch - space
if top_height is None:
top_height = tech.on_grid(
via_height + 2*top_enc_y, mult=2, rounding="ceiling",
)
else:
assert (bottom_height is None) and (top_height is None)
via_height = rows*pitch - space
bottom_height = tech.on_grid(
via_height + 2*bottom_enc_y, mult=2, rounding="ceiling",
)
top_height = tech.on_grid(
via_height + 2*top_enc_y, mult=2, rounding="ceiling",
)
columns = via_params["columns"]
bottom_width = via_params["bottom_width"]
top_width = via_params["top_width"]
if columns is None:
if bottom_width is None:
assert top_width is not None
columns = int(self.tech.on_grid(top_width - 2*top_enc_x - width)//pitch + 1)
via_width = columns*pitch - space
bottom_width = tech.on_grid(
via_width + 2*bottom_enc_x, mult=2, rounding="ceiling",
)
else:
columns = int(self.tech.on_grid(bottom_width - 2*bottom_enc_x - width)//pitch + 1)
if top_width is not None:
columns = min(
columns,
int(self.tech.on_grid(top_width - 2*top_enc_x - width)//pitch + 1)
)
via_width = columns*pitch - space
if top_width is None:
top_width = tech.on_grid(
via_width + 2*top_enc_x, mult=2, rounding="ceiling",
)
else:
assert (bottom_width is None) and (top_width is None)
via_width = columns*pitch - space
bottom_width = tech.on_grid(
via_width + 2*bottom_enc_x, mult=2, rounding="ceiling",
)
top_width = tech.on_grid(
via_width + 2*top_enc_x, mult=2, rounding="ceiling",
)
bottom_left = tech.on_grid(-0.5*bottom_width, rounding="floor")
bottom_bottom = tech.on_grid(-0.5*bottom_height, rounding="floor")
bottom_right = bottom_left + bottom_width
bottom_top = bottom_bottom + bottom_height
bottom_rect = geo.Rect(
left=bottom_left, bottom=bottom_bottom,
right=bottom_right, top=bottom_top,
)
top_left = tech.on_grid(-0.5*top_width, rounding="floor")
top_bottom = tech.on_grid(-0.5*top_height, rounding="floor")
top_right = top_left + top_width
top_top = top_bottom + top_height
top_rect = geo.Rect(
left=top_left, bottom=top_bottom,
right=top_right, top=top_top,
)
via_bottom = tech.on_grid(-0.5*via_height)
via_left = tech.on_grid(-0.5*via_width)
layout = cast(_Layout, self.fab.new_layout())
layout.add_shape(prim=bottom, net=net, shape=bottom_rect)
layout.add_shape(prim=prim, net=net, shape=_via_array(
via_left, via_bottom, width, pitch, rows, columns,
))
layout.add_shape(prim=top, net=net, shape=top_rect)
try:
impl = via_params["bottom_implant"]
except KeyError:
impl = None
else:
if impl is not None:
enc = cast(prp.Enclosure, via_params["bottom_implant_enclosure"])
assert enc is not None, "Internal error"
layout.add_shape(prim=impl, shape=geo.Rect.from_rect(
rect=bottom_rect, bias=enc,
))
try:
oxide = via_params["bottom_oxide"]
except KeyError:
oxide = None
else:
if oxide is not None:
assert (
isinstance(bottom, prm.WaferWire) and (bottom.oxide is not None)
and (bottom.min_oxide_enclosure is not None)
)
enc = cast(prp.Enclosure, via_params["bottom_oxide_enclosure"])
if enc is None:
idx = bottom.oxide.index(oxide)
enc = bottom.min_oxide_enclosure[idx]
assert (enc is not None), "Unknown enclosure"
layout.add_shape(prim=oxide, shape=geo.Rect.from_rect(
rect=bottom_rect, bias=enc,
))
try:
well = via_params["bottom_well"]
except KeyError:
well = None
else:
if well is not None:
well_net = via_params.get("well_net", None)
enc = via_params["bottom_well_enclosure"]
assert enc is not None, "Internal error"
if (impl is not None) and (impl.type_ == well.type_):
if well_net is not None:
if well_net != net:
raise ValueError(
f"Net '{well_net}' for well '{well.name}' of WaferWire"
f" {bottom.name} is different from net '{net}''\n"
f"\tbut implant '{impl.name}' is same type as the well"
)
else:
well_net = net
elif well_net is None:
raise TypeError(
f"No well_net specified for WaferWire '{bottom.name}' in"
f" well '{well.name}'"
)
layout.add_shape(prim=well, net=well_net, shape=geo.Rect.from_rect(
rect=bottom_rect, bias=enc,
))
return layout
def DeepWell(self, prim: prm.DeepWell, **deepwell_params) -> _Layout:
raise NotImplementedError("layout generation for DeepWell primitive")
def Resistor(self, prim: prm.Resistor, **resistor_params) -> _Layout:
try:
portnets = resistor_params["portnets"]
except KeyError:
port1 = prim.ports.port1
port2 = prim.ports.port2
else:
if set(portnets.keys()) != {"port1", "port2"}:
raise ValueError(
f"Resistor '{prim.name}' needs two port nets ('port1', 'port2')"
)
port1 = portnets["port1"]
port2 = portnets["port2"]
if prim.contact is None:
raise NotImplementedError("Resistor layout without contact layer")
res_width = resistor_params["width"]
res_height = resistor_params["height"]
wire = prim.wire
cont = prim.contact
cont_space = prim.min_contact_space
assert cont_space is not None
try:
wire_idx = cont.bottom.index(wire)
except ValueError: # pragma: no cover
raise NotImplementedError("Resistor connected from the bottom")
try:
wire_idx = cont.top.index(wire)
except ValueError:
raise AssertionError("Internal error")
else:
cont_enc = cont.min_top_enclosure[wire_idx]
cont_args = {"top": wire, "x": 0.0, "top_width": res_width}
else:
cont_enc = cont.min_bottom_enclosure[wire_idx]
cont_args = {"bottom": wire, "x": 0.0, "bottom_width": res_width}
if (prim.implant is not None) and isinstance(wire, prm.WaferWire):
cont_args["bottom_implant"] = prim.implant
cont_y1 = -0.5*res_height - cont_space - 0.5*cont.width
cont_y2 = -cont_y1
wire_ext = cont_space + cont.width + cont_enc.min()
layout = self.fab.new_layout()
# Draw indicator layers
for idx, ind in enumerate(prim.indicator):
ext = prim.min_indicator_extension[idx]
layout += self(ind, width=(res_width + 2*ext), height=res_height)
# Draw wire layer
mp = geo.MultiPartShape(
fullshape=geo.Rect.from_size(
width=res_width, height=(res_height + 2*wire_ext),
),
parts = (
geo.Rect.from_floats(values=(
-0.5*res_width, -0.5*res_height - wire_ext,
0.5*res_width, -0.5*res_height,
)),
geo.Rect.from_floats(values=(
-0.5*res_width, -0.5*res_height,
0.5*res_width, 0.5*res_height,
)),
geo.Rect.from_floats(values=(
-0.5*res_width, 0.5*res_height,
0.5*res_width, 0.5*res_height + wire_ext,
)),
)
)
layout.add_shape(prim=wire, net=port1, shape=mp.parts[0])
layout.add_shape(prim=wire, shape=mp.parts[1])
layout.add_shape(prim=wire, net=port2, shape=mp.parts[2])
# Draw contacts
# Hack to make sure the bottom wire does not overlap with the resistor part
# TODO: Should be fixed in MultiPartShape handling
# layout.add_wire(net=port1, wire=cont, y=cont_y1, **cont_args)
# layout.add_wire(net=port2, wire=cont, y=cont_y2, **cont_args)
x = cont_args.pop("x")
_l_cont = self.fab.layout_primitive(
prim=cont, portnets={"conn": port1}, **cont_args
)
_l_cont.move(dxy=geo.Point(x=x, y=cont_y1))
for sl in _l_cont.sublayouts:
if isinstance(sl, MaskShapesSubLayout):
for msl in sl.shapes:
if msl.mask == wire.mask:
assert isinstance(msl.shape, geo.Rect)
msl._shape = geo.Rect.from_rect(
rect=msl.shape, top=(-0.5*res_height - self.tech.grid)
)
layout += _l_cont
_l_cont = self.fab.layout_primitive(
prim=cont, portnets={"conn": port2}, **cont_args
)
_l_cont.move(dxy=geo.Point(x=x, y=cont_y2))
for sl in _l_cont.sublayouts:
if isinstance(sl, MaskShapesSubLayout):
for msl in sl.shapes:
if msl.mask == wire.mask:
assert isinstance(msl.shape, geo.Rect)
msl._shape = geo.Rect.from_rect(
rect=msl.shape, bottom=(0.5*res_height + self.tech.grid)
)
layout += _l_cont
if prim.implant is not None:
impl = prim.implant
try:
enc = prim.min_implant_enclosure.max() # type: ignore
except AttributeError:
assert isinstance(wire, prm.WaferWire), "Internal error"
idx = wire.implant.index(impl)
enc = wire.min_implant_enclosure[idx].max()
impl_width = res_width + 2*enc
impl_height = res_height + 2*wire_ext + 2*enc
layout.add_shape(prim=impl, shape=geo.Rect.from_size(width=impl_width, height=impl_height))
return layout
def MIMCapacitor(self, prim: prm.MIMCapacitor, **mimcapargs) -> _Layout:
try:
portnets = mimcapargs.pop("portnets")
except KeyError:
top = prim.ports.top
bottom = prim.ports.bottom
else:
if set(portnets.keys()) != {"top", "bottom"}:
raise ValueError(
f"MIMCapacitor '{prim.name}' needs two port nets ('top', 'bottom')"
)
top = portnets["top"]
bottom = portnets["bottom"]
via = prim.via
# Params
top_width = mimcapargs["width"]
top_height = mimcapargs["height"]
connect_up = mimcapargs["bottom_connect_up"]
# TODO: Allow to specify top of the via layer
upper_metal = via.top[0]
assert isinstance(upper_metal, prm.MetalWire)
assert upper_metal.pin is not None
upper_pin = upper_metal.pin[0]
# Compute dimensions
if connect_up:
bottomvia_outerwidth = (
top_width + 2*prim.min_bottomvia_top_space + 2*via.width
)
bottomvia_outerheight = (
top_height + 2*prim.min_bottomvia_top_space + 2*via.width
)
bottomvia_outerbound = geo.Rect.from_size(
width=bottomvia_outerwidth, height=bottomvia_outerheight,
)
idx = via.bottom.index(prim.bottom)
enc = via.min_bottom_enclosure[idx].max()
bottom_width = bottomvia_outerwidth + 2*enc
bottom_height = bottomvia_outerheight + 2*enc
enc = via.min_top_enclosure[0].max()
bottomupper_outerwidth = bottomvia_outerwidth + 2*enc
bottomupper_outerheight = bottomvia_outerheight + 2*enc
bottomupper_ringwidth = via.width + 2*enc
topupper_width = (
bottomupper_outerwidth - 2*bottomupper_ringwidth - 2*upper_metal.min_space
)
topupper_height = (
bottomupper_outerheight - 2*bottomupper_ringwidth - 2*upper_metal.min_space
)
else:
enc = prim.min_bottom_top_enclosure.max()
bottom_width = top_width + 2*enc
bottom_height = top_height + 2*enc
topupper_width = None
topupper_height = None
# Draw the shapes
layout = self.fab.new_layout()
via_lay = layout.add_primitive(
prim=via, bottom=prim.top, portnets={"conn": top},
bottom_width=top_width, bottom_height=top_height,
top_width=topupper_width, top_height=topupper_height,
bottom_enclosure=prim.min_top_via_enclosure,
)
via_upmbb = via_lay.bounds(mask=upper_metal.mask)
layout.add_shape(prim=upper_pin, net=top, shape=via_upmbb)
shape = geo.Rect.from_size(width=bottom_width, height=bottom_height)
layout.add_shape(prim=prim.bottom, net=bottom, shape=shape)
if connect_up:
shape = geo.RectRing(
outer_bound=bottomvia_outerbound,
rect_width=via.width, min_rect_space=via.min_space,
)
layout.add_shape(prim=via, net=bottom, shape=shape)
shape = geo.Ring(
outer_bound=geo.Rect.from_size(
width=bottomupper_outerwidth, height=bottomupper_outerheight,
),
ring_width=bottomupper_ringwidth,
)
layout.add_shape(prim=upper_metal, net=bottom, shape=shape)
layout.add_shape(prim=upper_pin, net=bottom, shape=shape)
bottom_space = (
prim.min_bottom_space
if prim.min_bottom_space is not None
else 0.0
)
layout.boundary = geo.Rect.from_size(
width=(bottom_width + bottom_space),
height=(bottom_height + bottom_space),
)
return layout
def Diode(self, prim: prm.Diode, **diode_params) -> _Layout:
try:
portnets = diode_params.pop("portnets")
except KeyError:
an = prim.ports.anode
cath = prim.ports.cathode
else:
if set(portnets.keys()) != {"anode", "cathode"}:
raise ValueError(
f"Diode '{prim.name}' needs two port nets ('anode', 'cathode')"
)
an = portnets["anode"]
cath = portnets["cathode"]
wirenet_args = {
"implant": prim.implant,
"portnets": {"conn": an if prim.implant.type_ == "p" else cath},
}
if prim.min_implant_enclosure is not None:
wirenet_args["implant_enclosure"] = prim.min_implant_enclosure
if prim.well is not None:
wirenet_args.update({
"well": prim.well,
"well_net": cath if prim.implant.type_ == "p" else an,
})
layout = self.fab.new_layout()
layout.add_primitive(prim=prim.wire, **wirenet_args, **diode_params)
wireact_bounds = layout.bounds(mask=prim.wire.mask)
act_width = wireact_bounds.right - wireact_bounds.left
act_height = wireact_bounds.top - wireact_bounds.bottom
for i, ind in enumerate(prim.indicator):
enc = prim.min_indicator_enclosure[i].max()
layout += self(ind, width=(act_width + 2*enc), height=(act_height + 2*enc))
return layout
def MOSFET(self, prim: prm.MOSFET, **mos_params) -> _Layout:
l = mos_params["l"]
w = mos_params["w"]
impl_enc = mos_params["activeimplant_enclosure"]
gate_encs = mos_params["gateimplant_enclosures"]
sdw = mos_params["sd_width"]
try:
portnets = cast(Mapping[str, net_.Net], mos_params["portnets"])
except KeyError:
portnets = prim.ports
gate_left = -0.5*l
gate_right = 0.5*l
gate_top = 0.5*w
gate_bottom = -0.5*w
layout = self.fab.new_layout()
active = prim.gate.active
active_width = l + 2*sdw
active_left = -0.5*active_width
active_right = 0.5*active_width
active_bottom = gate_bottom
active_top = gate_top
mps = geo.MultiPartShape(
fullshape=geo.Rect.from_size(width=active_width, height=w),
parts=(
geo.Rect(
left=active_left, bottom=active_bottom,
right=gate_left, top=active_top,
),
geo.Rect(
left=gate_left, bottom =active_bottom,
right=gate_right, top=active_top,
),
geo.Rect(
left=gate_right, bottom =active_bottom,
right=active_right, top=active_top,
),
)
)
layout.add_shape(prim=active, net=portnets["sourcedrain1"], shape=mps.parts[0])
layout.add_shape(prim=active, net=portnets["bulk"], shape=mps.parts[1])
layout.add_shape(prim=active, net=portnets["sourcedrain2"], shape=mps.parts[2])
for impl in prim.implant:
if impl in active.implant:
layout.add_shape(prim=impl, shape=_rect(
active_left, active_bottom, active_right, active_top,
enclosure=impl_enc
))
poly = prim.gate.poly
ext = prim.computed.min_polyactive_extension
poly_left = gate_left
poly_bottom = gate_bottom - ext
poly_right = gate_right
poly_top = gate_top + ext
layout.add_shape(prim=poly, net=portnets["gate"], shape=geo.Rect(
left=poly_left, bottom=poly_bottom, right=poly_right, top=poly_top,
))
if prim.well is not None:
enc = active.min_well_enclosure[active.well.index(prim.well)]
layout.add_shape(prim=prim.well, net=portnets["bulk"], shape=_rect(
active_left, active_bottom, active_right, active_top, enclosure=enc,
))
oxide = prim.gate.oxide
if oxide is not None:
assert (active.oxide is not None) and (active.min_oxide_enclosure is not None)
enc = getattr(
prim.gate, "min_gateoxide_enclosure", prp.Enclosure(self.tech.grid),
)
layout.add_shape(prim=oxide, shape=_rect(
gate_left, gate_bottom, gate_right, gate_top, enclosure=enc,
))
idx = active.oxide.index(oxide)
enc = active.min_oxide_enclosure[idx]
if enc is not None:
layout.add_shape(prim=oxide, shape=_rect(
active_left, active_bottom, active_right, active_top,
enclosure=enc,
))
if prim.gate.inside is not None:
# TODO: Check is there is an enclosure rule from oxide around active
# and apply the if so.
for i, inside in enumerate(prim.gate.inside):
enc = (
prim.gate.min_gateinside_enclosure[i]
if prim.gate.min_gateinside_enclosure is not None
else prp.Enclosure(self.tech.grid)
)
layout.add_shape(prim=inside, shape=_rect(
gate_left, gate_bottom, gate_right, gate_top, enclosure=enc,
))
for i, impl in enumerate(prim.implant):
enc = gate_encs[i]
layout.add_shape(prim=impl, shape=_rect(
gate_left, gate_bottom, gate_right, gate_top, enclosure=enc,
))
return layout
def Bipolar(self, prim: prm.Bipolar, **deepwell_params) -> _Layout:
# Currently it is assumed that fixed layouts are provided by the
# technology
raise NotImplementedError("layout generation for Bipolar primitive")
class MOSFETInstSpec: # pragma: no cover
"""Class that provided the spec for the string of transistors generation.
Used by `_CircuitLayouter.transistors_layout()`
Arguments:
inst: the transistor instance to generate layout for in the string.
A ValueError will be raised in the it is not a MOSFET instance.
The inst parameters like l, w, etc with determine the layout of the transistor.
contact_left, contact_right: whether to place contacts left or right from the
transistor. This value needs to be the same between two neighbours.
API Notes:
This class is deprecated and will be removed before v1.0.0. See also
https://gitlab.com/Chips4Makers/PDKMaster/-/issues/25
"""
def __init__(self, *,
inst: ckt._PrimitiveInstance,
contact_left: Optional[prm.Via], contact_right: Optional[prm.Via],
):
self._inst = inst
self._contact_left = contact_left
self._contact_right = contact_right
if not isinstance(inst.prim, prm.MOSFET):
raise ValueError(f"inst is not a MOSFET instance")
mosfet = inst.prim
if contact_left is not None:
if len(contact_left.top) != 1:
raise NotImplementedError(
f"Multiple top layers for Via '{contact_left.name}'",
)
if contact_right is not None:
if len(contact_right.top) != 1:
raise NotImplementedError(
f"Multiple top layers for Via '{contact_right.name}'",
)
impls = tuple(filter(
lambda impl: isinstance(impl, prm.Implant) and impl.type_ != "adjust",
mosfet.implant,
))
if len(impls) != 1:
raise NotImplementedError(
f"Multiple implant for MOSFET '{inst.prim.name}'",
)
self._implant = impls[0]
@property
def inst(self) -> ckt._PrimitiveInstance:
return self._inst
@property
def contact_left(self) -> Optional[prm.Via]:
return self._contact_left
@property
def contact_right(self) -> Optional[prm.Via]:
return self._contact_right
class _CircuitLayouter: # pragma: no cover
"""_CircuitLayouter is deprecated class and undocumented.
see https://gitlab.com/Chips4Makers/PDKMaster/-/issues/25 for development of
replacement.
API Notes:
This class is deprecated and user code will fail in future.
"""
def __init__(self, *,
fab: "LayoutFactory", circuit: ckt._Circuit, boundary: Optional[geo._Rectangular]
):
self.fab = fab
self.circuit = circuit
self.layout = l = fab.new_layout()
l.boundary = boundary
@property
def tech(self) -> tch.Technology:
return self.circuit.fab.tech
def inst_layout(self, *,
inst: ckt._Instance, layoutname: Optional[str]=None,
rotation: geo.Rotation=geo.Rotation.R0,
) -> _Layout:
if isinstance(inst, ckt._PrimitiveInstance):
notfound = []
portnets = {}
for port in inst.ports:
try:
net = self.circuit.net_lookup(port=port)
except ValueError:
notfound.append(port.name)
else:
portnets[port.name] = net
if len(notfound) > 0:
raise ValueError(
f"Unconnected port(s) {notfound}"
f" for inst '{inst.name}' of primitive '{inst.prim.name}'"
)
l = self.fab.layout_primitive(
prim=inst.prim, portnets=portnets,
**inst.params,
)
if rotation != geo.Rotation.R0:
l.rotate(rotation=rotation)
return l
elif isinstance(inst, ckt._CellInstance):
# TODO: propoer checking of nets for instance
layout = None
if layoutname is None:
try:
circuitname = cast(Any, inst).circuitname
layout = inst.cell.layouts[circuitname]
except:
layout = inst.cell.layout
else:
layoutname = circuitname
else:
if not isinstance(layoutname, str):
raise TypeError(
"layoutname has to be 'None' or a string, not of type"
f" '{type(layoutname)}'"
)
layout = inst.cell.layouts[layoutname]
bb = None if layout.boundary is None else rotation*layout.boundary
return _Layout(
fab=self.fab,
sublayouts=SubLayouts(_InstanceSubLayout(
inst=inst, origin=geo.origin, layoutname=layoutname,
rotation=rotation,
)),
boundary=bb,
)
else:
raise AssertionError("Internal error")
def wire_layout(self, *,
net: ckt._CircuitNet, wire: prm._Primitive, **wire_params,
) -> _Layout:
if net not in self.circuit.nets:
raise ValueError(
f"net '{net.name}' is not a net of circuit '{self.circuit.name}'"
)
if not (
hasattr(wire, "ports")
and (len(wire.ports) == 1)
and (wire.ports[0].name == "conn")
):
raise TypeError(
f"Wire '{wire.name}' does not have exactly one port named 'conn'"
)
return self.fab.layout_primitive(
wire, portnets={"conn": net}, **wire_params,
)
def transistors_layout(self, *,
trans_specs: Iterable[MOSFETInstSpec]
) -> _Layout:
"""This method allows to generate a string of transistors.
Arguments:
trans_specs: the list of the spec for the transistors to generate. A
`MOSFETInstSpec` object needs to be provided for each transistor of the
striog. For more information refer to the `MOSFETInstSpec` reference.
Some compatibility checks are done on the specification between the right
specifation of a spec and the left specification of the next. Currently it
is checked that whether to generata a contact is the same and if the active
layer between the two transistors is the same.
Results:
The string of transistor according to the provided specs from left to right.
"""
specs = tuple(trans_specs)
# Check consistency of the specification
for i, spec in enumerate(specs[:-1]):
next_spec = specs[i+1]
mosfet = cast(prm.MOSFET, spec.inst.prim)
next_mosfet = cast(prm.MOSFET, next_spec.inst.prim)
if spec.contact_right != next_spec.contact_left:
raise ValueError(
f"Contact specification mismatch between transistor spec {i} and {i+1}",
)
if mosfet.gate.active != next_mosfet.gate.active:
raise ValueError(
f"Active specification mismatch between transistor spec {i} and {i+1}",
)
# Create the layout
layout = self.fab.new_layout()
x = 0.0
for i, spec in enumerate(specs):
prev_spec = specs[i - 1] if (i > 0) else None
next_spec = specs[i + 1] if (i < (len(specs) - 1)) else None
mosfet = cast(prm.MOSFET, spec.inst.prim)
# First generate, so the port net checks are run now.
l_trans = self.inst_layout(inst=spec.inst)
# Draw left sd
if spec.contact_left is not None:
w = spec.inst.params["w"]
if prev_spec is not None:
w = min(w, prev_spec.inst.params["w"])
if mosfet.well is None:
well_args = {}
else:
well_args = {
"bottom_well": mosfet.well,
"well_net": spec.inst.ports["bulk"],
}
if (i > 0):
# Add oxide layer on first contact
oxide_args = {}
else:
oxide_args = {"bottom_oxide": mosfet.gate.oxide}
l = self.wire_layout(
wire=spec.contact_left,
net=self.circuit.net_lookup(port=spec.inst.ports["sourcedrain1"]),
bottom_height=w, bottom=mosfet.gate.active,
bottom_implant=spec._implant, **oxide_args, **well_args,
).moved(dxy=geo.Point(x=x, y=0.0))
layout += l
x += (
0.5*spec.contact_left.width + spec.inst.params["contactgate_space"]
+ 0.5*spec.inst.params["l"]
)
else:
gate_space = spec.inst.params["gate_space"]
if prev_spec is not None:
gate_space = max(gate_space, prev_spec.inst.params["gate_space"])
x += 0.5*gate_space + 0.5*spec.inst.params["l"]
# Remember trans position
l_trans.move(dxy=geo.Point(x=x, y=0.0))
layout += l_trans
if spec.contact_right is not None:
x += (
0.5*spec.inst.params["l"] + spec.inst.params["contactgate_space"]
+ 0.5*spec.contact_right.width
)
else:
gate_space = spec.inst.params["gate_space"]
if next_spec is not None:
gate_space = max(gate_space, next_spec.inst.params["gate_space"])
x += 0.5*spec.inst.params["l"] + 0.5*gate_space
# Draw last contact if needed
spec = specs[-1]
if spec.contact_right is not None:
mosfet = cast(prm.MOSFET, spec.inst.prim)
if mosfet.well is None:
well_args = {}
else:
well_args = {
"bottom_well": mosfet.well,
"well_net": spec.inst.ports["bulk"],
}
l = self.wire_layout(
wire=spec.contact_right,
net=self.circuit.net_lookup(port=spec.inst.ports["sourcedrain2"]),
bottom_height=spec.inst.params["w"], bottom=mosfet.gate.active,
bottom_implant=spec._implant, bottom_oxide=mosfet.gate.oxide, **well_args,
).moved(dxy=geo.Point(x=x, y=0.0))
layout += l
return layout
@overload
def place(self, object_: ckt._Instance, *,
origin: geo.Point, x: None=None, y: None=None,
layoutname: Optional[str]=None, rotation: geo.Rotation=geo.Rotation.R0,
) -> _Layout:
...
@overload
def place(self, object_: ckt._Instance, *,
origin: None=None, x: IntFloat=0.0, y: IntFloat=0.0,
layoutname: Optional[str]=None, rotation: geo.Rotation=geo.Rotation.R0,
) -> _Layout:
...
@overload
def place(self, object_: _Layout, *,
origin: geo.Point, x: None=None, y: None=None,
layoutname: Optional[str]=None, rotation: geo.Rotation=geo.Rotation.R0,
) -> _Layout:
...
@overload
def place(self, object_: _Layout, *,
origin: None=None, x: IntFloat=0.0, y: IntFloat=0.0,
layoutname: Optional[str]=None, rotation: geo.Rotation=geo.Rotation.R0,
) -> _Layout:
...
def place(self, object_, *,
origin=None, x: Optional[IntFloat]=None, y: Optional[IntFloat]=None,
layoutname: Optional[str]=None, rotation: geo.Rotation=geo.Rotation.R0,
) -> _Layout:
# Translate possible x/y specification to origin
if origin is None:
x = 0.0 if x is None else _util.i2f(x)
y = 0.0 if y is None else _util.i2f(y)
origin = geo.Point(x=x, y=y)
if isinstance(object_, ckt._Instance):
inst = object_
if inst not in self.circuit.instances:
raise ValueError(
f"inst '{inst.name}' is not part of circuit '{self.circuit.name}'"
)
if isinstance(inst, ckt._PrimitiveInstance):
def _portnets():
for net in self.circuit.nets:
for port in net.childports:
if (inst == port.inst):
yield (port.name, net)
portnets = dict(_portnets())
portnames = set(inst.ports.keys())
portnetnames = set(portnets.keys())
if not (portnames == portnetnames):
raise ValueError(
f"Unconnected port(s) {portnames - portnetnames}"
f" for inst '{inst.name}' of primitive '{inst.prim.name}'"
)
return self.layout.add_primitive(
prim=inst.prim, origin=origin, rotation=rotation,
portnets=portnets, **inst.params,
)
elif isinstance(inst, ckt._CellInstance):
# TODO: propoer checking of nets for instance
if (
(layoutname is None)
and inst.circuitname is not None
and (inst.circuitname in inst.cell.layouts.keys())
):
layoutname = inst.circuitname
sl = _InstanceSubLayout(
inst=inst, origin=origin, layoutname=layoutname, rotation=rotation,
)
self.layout += sl
return _Layout(
fab=self.fab, sublayouts=SubLayouts(sl), boundary=sl.boundary,
)
else:
raise RuntimeError("Internal error: unsupported instance type")
elif isinstance(object_, _Layout):
layout = object_.rotated(rotation=rotation).moved(dxy=origin)
self.layout += layout
return layout
else:
raise AssertionError("Internal error")
@overload
def add_wire(self, *,
net: net_.Net, wire: prm._Conductor, shape: Optional[geo._Shape]=None,
origin: geo.Point, x: None=None, y: None=None,
**wire_params,
) -> _Layout:
...
@overload
def add_wire(self, *,
net: net_.Net, wire: prm._Conductor, shape: Optional[geo._Shape]=None,
origin: None=None, x: Optional[IntFloat]=None, y: Optional[IntFloat]=None,
**wire_params,
) -> _Layout:
...
def add_wire(self, *,
net: net_.Net, wire: prm._Conductor, shape: Optional[geo._Shape]=None,
origin: Optional[geo.Point]=None, x: Optional[IntFloat]=None, y: Optional[IntFloat]=None,
**wire_params,
) -> _Layout:
if net not in self.circuit.nets:
raise ValueError(
f"net '{net.name}' is not a net of circuit '{self.circuit.name}'"
)
if origin is None:
x = 0.0 if x is None else _util.i2f(x)
y = 0.0 if y is None else _util.i2f(y)
origin = geo.Point(x=x, y=y)
if isinstance(wire, prm.Via):
if shape is not None:
raise ValueError(
"shape paramter may not be provided for a Via object"
)
return self._add_viawire(net=net, via=wire, origin=origin, **wire_params)
layout = self.layout
if (shape is None) or isinstance(shape, geo.Rect):
if shape is not None:
# TODO: Add support in _PrimitiveLayouter for shape argument,
# e.g. non-rectangular shapes
origin += shape.center
wire_params.update({
"width": shape.width, "height": shape.height,
})
return layout.add_primitive(
portnets={"conn": net}, prim=wire, origin=origin,
**wire_params,
)
else: # (shape is not None) and not a Rect
pin = wire_params.pop("pin", None)
if len(wire_params) != 0:
raise TypeError(
f"params {wire_params.keys()} not supported for shape not of type 'Rect'",
)
l = self.fab.new_layout()
layout.add_shape(net=net, prim=wire, shape=shape)
l.add_shape(net=net, prim=wire, shape=shape)
if pin is not None:
layout.add_shape(net=net, prim=pin, shape=shape)
l.add_shape(net=net, prim=pin, shape=shape)
return l
def _add_viawire(self, *,
net: net_.Net, via: prm.Via, origin: geo.Point, **via_params,
) -> _Layout:
# For a Via allow to specify bottom and/or top edges
has_rows = "rows" in via_params
has_columns = "columns" in via_params
def pop_param(name: str, type_, *, keep: bool=False):
if keep:
param = via_params.get(name, None)
else:
param = via_params.pop(name, None)
return cast(Optional[type_], param)
# Get bottom paramter specification
bottom_left = pop_param("bottom_left", float)
bottom_bottom = pop_param("bottom_bottom", float)
bottom_right = pop_param("bottom_right", float)
bottom_top = pop_param("bottom_top", float)
has_bottomedge = (
(bottom_left is not None) or (bottom_bottom is not None)
or (bottom_right is not None) or (bottom_top is not None)
)
bottom_shape = pop_param("bottom_shape", geo._Shape)
if bottom_shape is not None:
if has_bottomedge:
raise ValueError(
"Both bottom_shape and at least one of bottom_left, bottom_bottom"
", bottom_rigth or bottom_top specified"
)
if not isinstance(bottom_shape, geo.Rect):
raise NotImplementedError(
f"bottom_shape not a 'Rect' but of type '{type(bottom_shape)}'"
)
bottom_left = bottom_shape.left
bottom_bottom = bottom_shape.bottom
bottom_right = bottom_shape.right
bottom_top = bottom_shape.top
has_bottomedge = True
bottom = pop_param("bottom", prm.ViaBottom, keep=True)
if bottom is None:
bottom = via.bottom[0]
assert not isinstance(bottom, prm.Resistor), "Unimplemented"
bottom_enc = pop_param(
"bottom_enclosure", Union[str, float, prp.Enclosure], keep=True,
)
if isinstance(bottom_enc, float):
bottom_enc = prp.Enclosure(bottom_enc)
if (bottom_enc is None) or isinstance(bottom_enc, str):
idx = via.bottom.index(bottom)
enc = via.min_bottom_enclosure[idx]
if bottom_enc is None:
bottom_enc = enc
elif bottom_enc == "wide":
bottom_enc = enc.wide()
else:
assert bottom_enc == "tall"
bottom_enc = enc.tall()
bottom_henc = bottom_enc.first
bottom_venc = bottom_enc.second
# Get bottom paramter specification
top = pop_param("top", prm.ViaBottom, keep=True)
if top is None:
top = via.top[0]
assert not isinstance(top, prm.Resistor), "Unimplemented"
top_left = pop_param("top_left", float)
top_bottom = pop_param("top_bottom", float)
top_right = pop_param("top_right", float)
top_top = pop_param("top_top", float)
has_topedge = (
(top_left is not None) or (top_bottom is not None)
or (top_right is not None) or (top_top is not None)
)
top_shape = pop_param("top_shape", geo._Shape)
if top_shape is not None:
if has_topedge:
raise ValueError(
"Both top_shape and at least one of top_left, top_bottom"
", top_rigth or top_top specified"
)
if not isinstance(top_shape, geo.Rect):
raise NotImplementedError(
f"top_shape not a 'Rect' but of type '{type(top_shape)}'"
)
top_left = top_shape.left
top_bottom = top_shape.bottom
top_right = top_shape.right
top_top = top_shape.top
has_topedge = True
top_enc = pop_param(
"top_enclosure", Union[str, float, prp.Enclosure], keep=True,
)
if isinstance(top_enc, float):
top_enc = prp.Enclosure(top_enc)
if (top_enc is None) or isinstance(top_enc, str):
idx = via.top.index(top)
enc = via.min_top_enclosure[idx]
if top_enc is None:
top_enc = enc
elif top_enc == "wide":
top_enc = enc.wide()
else:
assert top_enc == "tall"
top_enc = enc.tall()
top_henc = top_enc.first
top_venc = top_enc.second
if has_bottomedge or has_topedge:
width = via.width
space = pop_param("space", float, keep=True)
if space is None:
space = via.min_space
pitch = width + space
# Compute number of rows/columns and placement
if bottom_left is not None:
if top_left is not None:
via_left = max(bottom_left + bottom_henc, top_left + top_henc)
else:
via_left = bottom_left + bottom_henc
else:
if top_left is not None:
via_left = top_left + top_henc
else:
via_left = None
if bottom_bottom is not None:
if top_bottom is not None:
via_bottom = max(bottom_bottom + bottom_venc, top_bottom + top_venc)
else:
via_bottom = bottom_bottom + bottom_henc
else:
if top_bottom is not None:
via_bottom = top_bottom + top_venc
else:
via_bottom = None
if bottom_right is not None:
if top_right is not None:
via_right = min(bottom_right - bottom_henc, top_right - top_henc)
else:
via_right = bottom_right - bottom_henc
else:
if top_right is not None:
via_right = top_right + top_henc
else:
via_right = None
if bottom_top is not None:
if top_top is not None:
via_top = min(bottom_top - bottom_venc, top_top - top_venc)
else:
via_top = bottom_top - bottom_venc
else:
if top_top is not None:
via_top = top_top + top_venc
else:
via_top = None
if (via_left is None) != (via_right is None):
raise NotImplementedError(
"left or right edge specification of Via but not both"
)
if (via_bottom is None) != (via_top is None):
raise NotImplementedError(
"bottom or top edge specification of Via but not both"
)
via_x = 0.0
if (via_left is not None) and (via_right is not None):
if has_columns:
raise ValueError(
"Via left/right edge together with columns specifcation"
)
w = self.tech.on_grid(via_right - via_left, mult=2)
columns = int((w - width)/pitch) + 1
if columns < 1:
raise ValueError("Not enough width for fitting one column")
via_x = self.tech.on_grid((via_left + via_right)/2.0)
via_params["columns"] = columns
via_y = 0.0
if (via_bottom is not None) and (via_top is not None):
if has_rows:
raise ValueError(
"Via bottom/top edge together with rows specifcation"
)
h = self.tech.on_grid(via_top - via_bottom, mult=2)
rows = int((h - width)/pitch) + 1
if rows < 1:
raise ValueError("Not enough height for fitting one row")
via_y = self.tech.on_grid((via_bottom + via_top)/2.0)
via_params["rows"] = rows
origin += geo.Point(x=via_x, y=via_y)
via_lay = self.fab.layout_primitive(
portnets={"conn": net}, prim=via, **via_params,
)
via_lay.move(dxy=origin)
draw = False
shape = via_lay.bounds(mask=top.mask)
if top_left is not None:
shape = geo.Rect.from_rect(rect=shape, left=top_left)
draw = True
if top_bottom is not None:
shape = geo.Rect.from_rect(rect=shape, bottom=top_bottom)
draw = True
if top_right is not None:
shape = geo.Rect.from_rect(rect=shape, right=top_right)
draw = True
if top_top is not None:
shape = geo.Rect.from_rect(rect=shape, top=top_top)
draw = True
if draw:
via_lay.add_shape(prim=top, net=net, shape=shape)
self.layout += via_lay
draw = False
shape = via_lay.bounds(mask=bottom.mask)
if bottom_left is not None:
shape = geo.Rect.from_rect(rect=shape, left=bottom_left)
draw = True
if bottom_bottom is not None:
shape = geo.Rect.from_rect(rect=shape, bottom=bottom_bottom)
draw = True
if bottom_right is not None:
shape = geo.Rect.from_rect(rect=shape, right=bottom_right)
draw = True
if bottom_top is not None:
shape = geo.Rect.from_rect(rect=shape, top=bottom_top)
draw = True
if draw:
kwargs = {}
if "bottom_implant" in via_params:
kwargs["implant"] = via_params["bottom_implant"]
if "bottom_well" in via_params:
kwargs["well"] = via_params["bottom_well"]
kwargs["well_net"] = via_params["well_net"]
l = self.add_wire(wire=bottom, net=net, shape=shape, **kwargs)
via_lay += l
return via_lay
def add_portless(self, *,
prim: prm._DesignMaskPrimitive, shape: Optional[geo._Shape]=None, **prim_params,
):
if len(prim.ports) > 0:
raise ValueError(
f"prim '{prim.name}' should not have any port"
)
if shape is None:
return self.layout.add_primitive(prim=prim, **prim_params)
else:
if len(prim_params) != 0:
raise ValueError(
f"Parameters '{tuple(prim_params.keys())}' not supported for shape not 'None'",
)
self.layout.add_shape(prim=prim, net=None, shape=shape)
[docs]class LayoutFactory:
"""The user facing class for creating layouts. This class is also a base
class on which own factory classes can be built with specific extensions.
Parameters:
tech: the technology for which to create circuits. Created layout may
only contain shapes on masks defined by the technology.
API Notes:
The contract for making subclasses has not been finaziled. Backwards
incompatible changes are still expected for subclasses of this class.
"""
def __init__(self, *, tech: tch.Technology):
self.tech = tech
self.gen_primlayout = _PrimitiveLayouter(self)
[docs] def new_layout(self, *,
sublayouts: Optional[Union[_SubLayout, SubLayouts]]=None,
boundary: Optional[geo._Rectangular]=None,
):
"""Create a new layout.
Arguments:
sublayouts: optional list of sublayouts to add to this new layout
boundary: optional boundary of the new layout
"""
if sublayouts is None:
sublayouts = SubLayouts()
if isinstance(sublayouts, _SubLayout):
sublayouts = SubLayouts(sublayouts)
return _Layout(fab=self, sublayouts=sublayouts, boundary=boundary)
[docs] def layout_primitive(self, prim: prm._Primitive, **prim_params) -> _Layout:
"""Create the layout of a `_Primitive` object.
This will generate a default layout for a given primitive with the
provided paramters. This is a default layout
Arguments:
prim: the primitive to create a layout for
prim_params: the parameters for the primitive
API Notes:
User code can't depend on the exact layout generated for a certain
primitive. Future improvements to the layout generation code
may change the resulting layout.
"""
prim_params = prim.cast_params(prim_params)
return self.gen_primlayout(prim, **prim_params)
[docs] def new_circuitlayouter(self, *,
circuit:ckt._Circuit, boundary: Optional[geo._Rectangular],
) -> _CircuitLayouter:
"""Helper class to generate layout corresponding to a given `_Circuit`.
The returned layouter will start with an empty layout with optionally a
provided boundary. The layouter API can then be used to build up the
layout for the circuit.
Arguments:
circuit: the circuit for which to create a layouter
boundary: optional boundary of the created layout
API Notes:
The API of the returned layouter is not fixed yet and backwards
incompatible changes are still expected.
"""
return _CircuitLayouter(fab=self, circuit=circuit, boundary=boundary)