# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-or-later OR CERN-OHL-S-2.0+ OR Apache-2.0
from textwrap import dedent
from itertools import product
from xml.etree import ElementTree as ET
from typing import Any, Tuple, List, Dict, Set, Iterable, Optional, Union, cast, overload
from ... import _util, dispatch as _dsp
from ...typing import GDSLayerSpecDict
from ...technology import (
property_ as _prp, rule as _rle, wafer_ as _wfr, mask as _msk, edge as _edg,
geometry as _geo, primitive as _prm, net as _net, technology_ as _tch,
)
from ...design import layout as _lay, cell as _cell, library as _lbry
from ...design.layout import layout_ as _laylay
from ..spice import SpicePrimParamsT, SpicePrimsParamSpec
pya: Any # Silence pylance
import pya
__all__= ["FileExporter", "export2db"]
class _MaskConverter(_dsp.MaskDispatcher):
def __init__(self, *, tech: _tch.Technology) -> None:
super().__init__()
self.tech = tech
def __call__(self, mask: _msk.MaskT) -> str:
return super().__call__(mask)
@staticmethod
def __legalized_maskname(name: str) -> str:
if name[0] in "0123456789":
name = "_" + name
return name.replace(".", "_").replace(":", "__")
def DesignMask(self, mask: _msk.DesignMask) -> str:
return self.__legalized_maskname(mask.name)
def _MaskAlias(self, mask: _msk._MaskAlias) -> str:
return self.__legalized_maskname(mask.name)
# Handled on higher level
# def _PartsWith(self, pw: msk._PartsWith):
# ...
def Join(self, join: _msk.Join) -> str:
return f"({'+'.join(self(m) for m in join.masks)})"
def Intersect(self, intersect: _msk.Intersect) -> str:
return f"({'&'.join(self(m) for m in intersect.masks)})"
def _MaskRemove(self, mr: _msk._MaskRemove):
return f"({self(mr.from_)}-{self(mr.what)})"
def _Wafer(self, wafer: _wfr._Wafer, *args, **kwargs):
# Size box to be sure it is big enough for substrate enclosure check
bias = 0.0
for act in self.tech.primitives.__iter_type__(_prm.WaferWire):
for enc in (act.min_substrate_enclosure, act.min_substrate_enclosure_same_type):
if enc is not None:
bias = max(bias, enc.max())
return "extent" if bias < self.tech.grid/100 else f"extent.sized({bias:.6})"
_mask_conv: Any = None
class _EdgeConverter(_dsp.EdgeDispatcher):
def __call__(self, edge: _edg.EdgeT) -> str:
return super().__call__(edge)
def MaskEdge(self, edge: _edg.MaskEdge) -> str:
return f"{_mask_conv(edge.mask)}.edges"
def _DualEdgeOperation(self, op: _edg._DualEdgeOperation) -> str:
s_edge1 = self(op.edge1)
if isinstance(op.edge2, _msk.MaskT):
s_edge2 = _mask_conv(op.edge2)
elif isinstance(op.edge2, _edg.EdgeT):
s_edge2 = self(op.edge2)
else: # pragma: no cover
raise TypeError(f"[Internal error]Unexpected type for edge2 of {str(op)}")
if op.operation == "interact_with":
return f"{s_edge1}.interacting({s_edge2})"
else: # pragma: no cover
raise NotImplementedError(f"[Internal error]Operation: {op.operation}")
def Join(self, join: _edg.Join) -> str:
s_join = "+".join(
_mask_conv(e) if isinstance(e, _msk.MaskT) else self(e)
for e in join.edges
)
return f"({s_join})"
def Intersect(self, intersect: _edg.Intersect) -> str:
s_intersect = "&".join(
_mask_conv(e) if isinstance(e, _msk.MaskT) else self(e)
for e in intersect.edges
)
return f"({s_intersect})"
_edge_conv = _EdgeConverter()
def _str_designmask(mask: _msk.DesignMask, *, gds_layers: GDSLayerSpecDict):
gds_layer = gds_layers[mask.name]
if not isinstance(gds_layer, tuple):
gds_layer = (gds_layer, 0)
return f"{_mask_conv(mask)} = input{gds_layer}\n"
def _str_alias(mask: _msk._MaskAlias):
return f"{_mask_conv(mask)} = {_mask_conv(mask.mask)}\n"
def _str_grid(mask: _msk.MaskT, grid: float):
s_mask = _mask_conv(mask)
return dedent(f"""
{s_mask}.ongrid({grid}).output(
"{s_mask} grid", "{s_mask} grid: {grid}µm"
)
"""[1:])
class _RuleConverter(_dsp.RuleDispatcher):
def __call__(self, rule: _rle.RuleT, *, allow_unimplented: bool=False) -> str:
if allow_unimplented:
try:
s = super().__call__(rule)
except NotImplementedError:
s = "# Not supported\n"
else:
s = super().__call__(rule)
return f"# {rule}\n{s}"
def GreaterEqual(self, ge: _prp.Operators.GreaterEqual):
left = ge.left
right = ge.right
if isinstance(left, _msk._MaskProperty):
s_mask = _mask_conv(left.mask)
prop = left.prop_name
if prop in {"width", "space"}:
return dedent(f"""
{s_mask}.{prop}({right}).output(
"{s_mask} {prop}", "{s_mask} minimum {prop}: {right}µm"
)
"""[1:])
elif left.prop_name == "area":
return dedent(f"""
{s_mask}.with_area(nil, {right}).output(
"{s_mask} area", "{s_mask} minimum area: {right}µm"
)
"""[1:])
elif left.prop_name == "density":
assert isinstance(right, float)
return dedent(f"""
{s_mask}_mindens = polygon_layer
dens_check({s_mask}_mindens, {s_mask}, {right}, 1)
{s_mask}_mindens.output(
"{s_mask} density", "{s_mask} minimum density: {round(100*right)}%"
)
"""[1:])
elif isinstance(left, _msk._DualMaskProperty):
prop = left.prop_name
if (
(prop == "space")
and (
isinstance(left.mask1, _msk._PartsWith)
or isinstance(left.mask2, _msk._PartsWith)
)
):
# Special code for handling width based spacing rules
# Main objective if to support space tabels specified for primitives
# Other application area are mainly untested
if isinstance(left.mask1, _msk._PartsWith):
assert len(left.mask1.condition) == 1
cond = left.mask1.condition[0]
assert left.mask1.mask == left.mask2
s_mask = _mask_conv(left.mask1.mask)
else:
assert isinstance(left.mask2, _msk._PartsWith)
assert len(left.mask2.condition) == 1
cond = left.mask2.condition[0]
assert left.mask2.mask == left.mask1
s_mask = _mask_conv(left.mask2.mask)
assert isinstance(cond, _prp.Operators.GreaterEqual)
assert isinstance(cond.left, _msk._MaskProperty)
assert cond.left.prop_name == "width"
return dedent(f"""
space4width_check({s_mask}, {cond.right}, {right}).output(
"{s_mask} table spacing",
"Minimum {s_mask} spacing for {cond.right}µm width: {right}µm"
)
"""[1:])
s_mask1 = _mask_conv(left.mask1)
s_mask2 = _mask_conv(left.mask2)
if prop == "space":
return dedent(f"""
{s_mask1}.separation({s_mask2}, {right}, square).output(
"{s_mask1}:{s_mask2} spacing",
"Minimum spacing between {s_mask1} and {s_mask2}: {right}µm"
)
"""[1:])
elif prop == "extend_over":
return dedent(f"""
extend_check({s_mask2}, {s_mask1}, {right}).output(
"{s_mask1}:{s_mask2} extension",
"Minimum extension of {s_mask1} of {s_mask2}: {right}µm"
)
"""[1:])
elif isinstance(left, _msk._DualMaskEnclosureProperty):
s_mask1 = _mask_conv(left.mask1)
s_mask2 = _mask_conv(left.mask2)
prop = left.prop_name
if prop == "enclosed_by":
# TODO: Proper typing for Property
assert isinstance(right, _prp.Enclosure)
if not right.is_assymetric:
return dedent(f"""
{s_mask2}.enclosing({s_mask1}, {right.first}).output(
"{s_mask2}:{s_mask1} enclosure",
"Minimum enclosure of {s_mask2} around {s_mask1}: {right.first}µm"
)
"""[1:])
else:
s_desc = (
f"Minimum enclosure of {s_mask2} around {s_mask1}: "
f"{right.min()}µm minimum, {right.max()}µm opposite"
)
return dedent(f"""
oppenc_check({s_mask1}, {s_mask2}, {right.min()}, {right.max()}).output(
"{s_mask2}:{s_mask1} asymmetric enclosure",
"{s_desc}"
)
"""[1:])
elif isinstance(left, _edg._EdgeProperty):
s_edge = _edge_conv(left.edge)
prop = left.prop_name
if prop == "length":
return dedent(f"""
{s_edge}.with_length(nil, {right}).output(
"{s_edge} length",
"Minimum length of {s_edge}: {right}µm"
)
"""[1:])
elif isinstance(left, _edg._DualEdgeProperty):
s_edge1 = _edge_conv(left.edge1)
s_edge2 = (
_mask_conv(left.edge2)+".edges" if isinstance(left.edge2, _msk.MaskT)
else _edge_conv(left.edge2)
)
prop = left.prop_name
if prop == "enclosed_by":
return dedent(f"""
{s_edge2}.enclosing({s_edge1}, {right}).output(
"{s_edge2}:{s_edge1} enclosure",
"Minimum enclosure of {s_edge2} around {s_edge1}: {right}µm"
)
"""[1:])
raise NotImplementedError(f"GreateEqual rule '{ge}'") # pragma: no cover
def Equal(self, eq: _prp.Operators.Equal) -> str:
left = eq.left
right = eq.right
assert isinstance(right, left.value_type)
if isinstance(left, _msk._MaskProperty):
s_mask = _mask_conv(left.mask)
prop = left.prop_name
if prop == "width":
return dedent(f"""
width_check({s_mask}, {right}).output(
"{s_mask} width", "{s_mask} width: {right}µm"
)
"""[1:])
elif prop == "area":
if round(right, 6) != 0.0:
raise ValueError("For area equal check value can only be 0.0")
return f'{s_mask}.output("{s_mask} empty")\n'
elif isinstance(left, _edg._EdgeProperty):
s_edge = _edge_conv(left.edge)
prop = left.prop_name
if prop == "length":
if round(right, 6) != 0.0:
raise ValueError("For length equal check value can only be 0.0")
return f'{s_edge}.output("{s_edge} empty")\n'
raise NotImplementedError(f"Equal rule '{eq}'") # pragma: no cover
def _MaskAlias(self, alias: _msk._MaskAlias) -> str:
return f"{_mask_conv(alias)} = {_mask_conv(alias.mask)}\n"
def Connect(self, conn: _msk.Connect) -> str:
return "".join(
f"connect({_mask_conv(mask1)}, {_mask_conv(mask2)})\n"
for mask1, mask2 in product(conn.mask1, conn.mask2)
)
_rule_conv = _RuleConverter()
def _str_lvsresistor(res: _prm.Resistor, *, params: SpicePrimParamsT):
s = f"# {res.name}\n"
s_res = _mask_conv(res.mask)
s_conn = _mask_conv(res.wire.conn_mask)
sheetres = ["sheetres"]
if sheetres is not None:
s += dedent(f"""
extract_devices(resistor("{res.name}", {sheetres}), {{
"R" => {s_res}, "C" => {s_conn},
}})
same_device_classes("{res.name}", "RES")
"""[1:])
return s
def _str_lvsdiode(tech: _tch.Technology, diode: _prm.Diode, spice_params: SpicePrimParamsT):
s = f"# {diode.name}\n"
is_n = any(impl.type_ == _prm.nImpl for impl in diode.implant)
s_diode = _mask_conv(diode.mask)
s_conn = _mask_conv(diode.wire.conn_mask)
s_well = _mask_conv(
diode.well.mask if diode.well is not None
else tech.substrate_prim.mask
)
if is_n:
s_p = s_well
s_n = s_diode
s_conn_port = "tC"
else:
s_n = s_well
s_p = s_diode
s_conn_port = "tA"
s += dedent(f"""
extract_devices(diode("{spice_params['model']}"), {{
"P" => {s_p}, "N" => {s_n}, "{s_conn_port}" => {s_conn}
}})
"""[1:])
return s
def _str_lvsmosfet(tech: _tch.Technology, mosfet: _prm.MOSFET, spice_params: SpicePrimParamsT):
s = f"# {mosfet.name}\n"
s_sd = _mask_conv(mosfet.gate.active.conn_mask)
s_gate = _mask_conv(mosfet.gate4mosfet.mask)
s_bulk = _mask_conv(
mosfet.well.mask if mosfet.well is not None
else tech.substrate_prim.mask
)
s_poly = _mask_conv(mosfet.gate.poly.conn_mask)
s += dedent(f"""
extract_devices(mos4("{spice_params['model']}"), {{
"SD" => {s_sd}, "G" => {s_gate}, "tG" => {s_poly}, "W" => {s_bulk},
}})
"""[1:])
return s
[docs]class FileExporter:
def __init__(self, *,
tech: _tch.Technology, export_name: Optional[str]=None,
gds_layers: GDSLayerSpecDict, prims_spiceparams: SpicePrimsParamSpec,
):
self.tech = tech
self.export_name = tech.name if export_name is None else export_name
self.gds_layers = gds_layers
self.prims_spiceparams = prims_spiceparams
global _mask_conv
_mask_conv = _MaskConverter(tech=tech)
def __call__(self):
return {
"drc": self._s_drc(),
"ly_drc": self._ly_drc(),
"extract": self._s_extract(),
"ly_extract": self._ly_extract(),
"lvs": self._s_lvs(),
"ly_tech": self._ly_tech(),
}
def _s_drc(self):
s = dedent(f"""
# Autogenerated file. Changes will be overwritten.
source(ENV["SOURCE_FILE"])
report("{self.export_name} DRC", ENV["REPORT_FILE"])
"""[1:])
return s + self._s_drcrules()
def _ly_drc(self):
ly_drc = ET.Element("klayout-macro")
ET.SubElement(ly_drc, "description")
ET.SubElement(ly_drc, "version")
ET.SubElement(ly_drc, "category").text = "drc"
ET.SubElement(ly_drc, "prolog")
ET.SubElement(ly_drc, "epilog")
ET.SubElement(ly_drc, "doc")
ET.SubElement(ly_drc, "autorun").text = "false"
ET.SubElement(ly_drc, "autorun-early").text = "false"
ET.SubElement(ly_drc, "shortcut")
ET.SubElement(ly_drc, "show-in-menu").text = "true"
ET.SubElement(ly_drc, "group-name").text = "drc_scripts"
ET.SubElement(ly_drc, "menu-path").text = "tools_menu.drc.end"
ET.SubElement(ly_drc, "interpreter").text = "dsl"
ET.SubElement(ly_drc, "dsl-interpreter-name").text = "drc-dsl-xml"
s = dedent(f"""
# Autogenerated file. Changes will be overwritten.
report("{self.export_name} DRC")
"""[1:]) + self._s_drcrules()
ET.SubElement(ly_drc, "text").text = s
return ly_drc
def _s_drcrules(self):
s = dedent(f"""
def width_check(layer, w)
small = layer.width(w).polygons
big = layer.sized(-0.5*w).size(0.5*w)
small | big
end
def space4width_check(layer, w, s)
big = layer.sized(-0.5*w).size(0.5*w)
big.edges.separation(layer.edges, s)
end
def oppenc_check(inner, outer, min, max)
toosmall = outer.enclosing(inner, min).second_edges
smallenc = outer.enclosing(inner, max - 1.dbu, projection).second_edges
# These edges may not touch each other
touching = smallenc.width(1.dbu, angle_limit(100)).edges
inner.interacting(toosmall + touching)
end
def extend_check(base, extend, e)
extend.enclosing(base, e).first_edges.not_interacting(base)
end
def dens_check(output, input, min, max)
tp = RBA::TilingProcessor::new
tp.output("res", output.data)
tp.input("input", input.data)
tp.dbu = 1.dbu # establish the real database unit
tp.var("vmin", min)
tp.var("vmax", max)
tp.queue("_tile && (var d = to_f(input.area(_tile.bbox)) / to_f(_tile.bbox.area); (d < vmin || d > vmax) && _output(res, _tile.bbox))")
tp.execute("Density check")
end
"""[1:])
s += "\n# Define layers\n"
assert isinstance(self.tech.rules, _rle.Rules), "Internal error"
dms = tuple(self.tech.rules.__iter_type__(_msk.DesignMask))
s += "".join(_str_designmask(dm, gds_layers=self.gds_layers) for dm in dms)
s += "\n# Grid check\n"
gridrules = cast(Tuple[_prp.Operators.Equal, ...], tuple(filter(
lambda rule: (
isinstance(rule, _prp.Operators.Equal)
and isinstance(rule.left, _msk._MaskProperty)
and (rule.left.prop_name == "grid")
),
self.tech.rules,
)
))
gridspecs = {
cast(_msk._MaskProperty, gridrule.left).mask: gridrule.right
for gridrule in gridrules
}
globalgrid = gridspecs[_wfr.wafer]
s += "".join(
_str_grid(dm, cast(float, gridspecs.get(dm, globalgrid)))
for dm in dms
)
s += "\n# Derived layers\n"
aliases = tuple(self.tech.rules.__iter_type__(_msk._MaskAlias))
s += "".join(_rule_conv(alias) for alias in aliases)
s += "\n# Connectivity\n"
conns = tuple(self.tech.rules.__iter_type__(_msk.Connect))
s += "".join(_rule_conv(conn) for conn in conns)
s += "\n# DRC rules\n" + "".join(
_rule_conv(rule) for rule in filter(
lambda rule: rule not in dms + gridrules + conns + aliases,
self.tech.rules
)
)
return s
def _s_extract(self):
s = dedent(f"""
# Autogenerated file. Changes will be overwritten
source(ENV["SOURCE_FILE"])
target_netlist(ENV["SPICE_FILE"], write_spice(true, true))
"""[1:])
s += self._s_extractrules()
return s
def _ly_extract(self):
ly_extract = ET.Element("klayout-macro")
ET.SubElement(ly_extract, "description")
ET.SubElement(ly_extract, "version")
ET.SubElement(ly_extract, "category").text = "lvs"
ET.SubElement(ly_extract, "prolog")
ET.SubElement(ly_extract, "epilog")
ET.SubElement(ly_extract, "doc")
ET.SubElement(ly_extract, "autorun").text = "false"
ET.SubElement(ly_extract, "autorun-early").text = "false"
ET.SubElement(ly_extract, "shortcut")
ET.SubElement(ly_extract, "show-in-menu").text = "true"
ET.SubElement(ly_extract, "group-name").text = "lvs_scripts"
ET.SubElement(ly_extract, "menu-path").text = "tools_menu.lvs.end"
ET.SubElement(ly_extract, "interpreter").text = "dsl"
ET.SubElement(ly_extract, "dsl-interpreter-name").text = "lvs-dsl-xml"
s = dedent(f"""
# Autogenerated file. Changes will be overwritten
report_netlist
"""[1:])
s += self._s_extractrules()
ET.SubElement(ly_extract, "text").text = s
return ly_extract
def _s_lvs(self):
s = dedent(f"""
# Autogenerated file. Changes will be overwritten
source(ENV["SOURCE_FILE"])
schematic(ENV["SPICE_FILE"])
report_lvs(ENV["REPORT_FILE"])
"""[1:])
s += self._s_extractrules() + dedent(f"""
align
ok = compare
if ok then
print("LVS OK\\n")
else
abort "LVS Failed!"
end
""")
return s
def _s_extractrules(self):
# TODO: bug report for failing LVS on hierarchical LVS and diodes
s = "flat\n\n# Define layers\n"
assert isinstance(self.tech.rules, _rle.Rules), "Internal error"
dms = tuple(self.tech.rules.__iter_type__(_msk.DesignMask))
s += "".join(_str_designmask(dm, gds_layers=self.gds_layers) for dm in dms)
aliases = tuple(self.tech.rules.__iter_type__(_msk._MaskAlias))
s += "".join(_str_alias(alias) for alias in aliases)
s += "\n# Connectivity\n"
conns = tuple(self.tech.rules.__iter_type__(_msk.Connect))
s += "".join(_rule_conv(conn) for conn in conns)
s += "\n# Resistors\n"
resistors = tuple(self.tech.primitives.__iter_type__(_prm.Resistor))
s += "".join(
_str_lvsresistor(res, params=self.prims_spiceparams[res])
for res in resistors
)
s += "\n# Diodes\n"
diodes = tuple(self.tech.primitives.__iter_type__(_prm.Diode))
s += "".join(
_str_lvsdiode(self.tech, diode, self.prims_spiceparams[diode])
for diode in diodes
)
s += "\n# Transistors\n"
mosfets = tuple(self.tech.primitives.__iter_type__(_prm.MOSFET))
s += "".join(
_str_lvsmosfet(self.tech, mosfet, self.prims_spiceparams[mosfet])
for mosfet in mosfets
)
s += "\nnetlist\n"
return s
def _ly_tech(self):
lyt = ET.Element("technology")
ET.SubElement(lyt, "name").text = self.export_name
ET.SubElement(lyt, "description").text = (
f"KLayout generated from {self.tech.name} PDKMaster technology"
)
ET.SubElement(lyt, "group")
ET.SubElement(lyt, "dbu").text = f"{self.tech.dbu}"
ET.SubElement(lyt, "layer-properties_file")
ET.SubElement(lyt, "add-other-layers").text = "true"
ropts = ET.SubElement(lyt, "reader-options")
roptscom = ET.SubElement(ropts, "common")
ET.SubElement(roptscom, "create-other-layers").text = "true"
def s_gds_layer(m: _msk.DesignMask):
try:
l = self.gds_layers[m.name]
except KeyError: # pragma: no cover
raise ValueError(
f"No gds_layer provided for mask '{m.name}'"
)
if isinstance(l, int):
return f"{l}/0"
else:
assert isinstance(l, tuple)
return f"{l[0]}/{l[1]}"
s_map = ";".join(
f"'{s_gds_layer(mask)} : {mask.name}'"
for mask in self.tech.designmasks
)
ET.SubElement(roptscom, "layer-map").text = f"layer_map({s_map})"
ET.SubElement(lyt, "writer-options")
ET.SubElement(lyt, "connectivity")
return lyt
class _ShapeExporter(_dsp.ShapeDispatcher):
"""Converts a _geo,_Shape object to KLayout database object"""
def __init__(self, *, export_fullshape: bool):
self._mps_exported: Optional[Set[_geo.MultiPartShape]]
self._mps_exported = set() if export_fullshape else None
def _pointsshapes(self, shape: _geo._Shape) -> Iterable[Any]:
# Helper to convert the individual pointsshapes
for pointshape in shape.pointsshapes:
conv = self(pointshape)
assert not _util.is_iterable(conv), "Internal error: unsupported"
yield conv
def _Shape(self, shape: _geo._Shape): # pragma: no cover
raise ValueError(f"Unsupported object of type {shape.__class__.__name__}")
def Point(self, point: _geo.Point) -> "pya.DPoint": # type: ignore
return pya.DPoint(point.x, point.y)
def Line(self, line: _geo.Line) -> "pya.DPath": # type: ignore
# We represent a Line by a path with zero width
points = (self.Point(line.point1), self.Point(line.point2))
return pya.DPath(points, 0.0)
def Polygon(self, polygon: _geo.Polygon, **_) -> "pya.DSimplePolygon": # type: ignore
# In PDKMaster polygon needs last point to be same as first point;
# in klayout this is not the case.
points = tuple(self.Point(point) for point in tuple(polygon.points)[:-1])
return pya.DSimplePolygon(points)
def Rect(self, rect: _geo.Rect) -> "pya.DBox": # type: ignore
return pya.DBox(rect.left, rect.bottom, rect.right, rect.top)
def RectRing(self, rs: _geo.RectRing) -> Iterable[Any]:
# TODO: Can repetition information be retained in KLayout ?
return self._pointsshapes(rs)
def MultiPartShape(self, mps: _geo.MultiPartShape):
if self._mps_exported is None:
return self(mps.fullshape)
else:
if mps in self._mps_exported:
return None
else:
self._mps_exported.add(mps)
return self(mps.fullshape)
def MultiPartShape__Part(self, part: _geo.MultiPartShape._Part):
if self._mps_exported is None:
return self(part.partshape)
else:
return self(part.multipartshape)
def MultiShape(self, ms: _geo.MultiShape) -> Iterable[Any]:
for shape in ms.shapes:
conv = self(shape)
if _util.is_iterable(conv):
yield from conv
else:
yield conv
def RepeatedShape(self, rs: _geo.RepeatedShape) -> Iterable[Any]:
# TODO: Can repetition information be retained in KLayout ?
return self._pointsshapes(rs)
# TODO: Does KLayout allow more efficient array representation
# ArrayShape -> RepeatedShape
class _MaskLayerDict(Dict[_msk.DesignMask, int]):
def __init__(self, *, layout: "pya.Layout", gds_layers: GDSLayerSpecDict): # type: ignore
self._layout = layout
self._gds_layers = gds_layers
def __getitem__(self, mask: _msk.DesignMask) -> int:
if mask not in self:
layer = self._gds_layers[mask.name]
if isinstance(layer, tuple):
layer, datatype = layer
else:
datatype = 0
self[mask] = self._layout.layer(layer, datatype, mask.name)
return super().__getitem__(mask)
_rotation_to_rot_mirr: Dict[_geo.Rotation, Tuple[int, bool]] = {
_geo.Rotation.No: (0, False),
_geo.Rotation.R90: (1, False),
_geo.Rotation.R180: (2, False),
_geo.Rotation.R270: (3, False),
_geo.Rotation.MX: (0, True),
_geo.Rotation.MX90:(1, True),
_geo.Rotation.MY: (2, True),
_geo.Rotation.MY90: (3, True),
}
class _LayoutExporter:
def __init__(self):
self._clear()
def _clear(self):
self.layout = None
self.layerdict = None
self.cell_lookup: Dict[str, Tuple[Optional[_cell.Cell], "pya.Layout"]] = {} # type: ignore
self.cells_todo: Set[_cell.Cell] = set()
self.cells_done: Set[_cell.Cell] = set()
self.cell = None
self.shapeexporter = None
self.pinmasks: Optional[Tuple[_msk.MaskT, ...]] = None
def __call__(self, *,
obj: Union[_geo.MaskShape, _geo.MaskShapes, _lay.LayoutT, _lbry.Library],
gds_layers: GDSLayerSpecDict, cell_name: Optional[str],
merge: bool, add_pin_label: bool, dbu: float=0.001,
) -> "pya.Layout": # type: ignore
self.layout = layout = pya.Layout()
layout.dbu = dbu
self.layerdict = _MaskLayerDict(layout=layout, gds_layers=gds_layers)
self.shapeexporter = _ShapeExporter(export_fullshape=True)
if isinstance(obj, _lbry.Library):
assert cell_name is None
# A cell will be created in add() function below for each cell of the library
else:
if cell_name is None:
cell_name = "anon"
self.cell = self._create_layout(cell_name)
# Define local function to allow recursive calling
self._add(obj, add_pin_label=add_pin_label, net=None)
while len(self.cells_todo) > 0:
cell = self.cells_todo.pop()
self.cells_done.add(cell)
self.cell = self.cell_lookup[cell.name][1]
# Don't reuse ShapeExporter between cells, otherwise MultiPartShapes
# can be wrongly marked as written when they are not.
self.shapeexporter = _ShapeExporter(export_fullshape=True)
self._add(cell.layout, add_pin_label=add_pin_label, net=None)
if merge:
for cell in layout.each_cell():
for layer_idx in self.layerdict.values():
# https://www.klayout.de/forum/discussion/697/merge-all-shapes-of-a-certain-layer-of-a-cell
old_shapes = cell.shapes(layer_idx)
region = pya.Region(old_shapes)
region.merge()
new_shapes = pya.Shapes()
new_shapes.insert(region)
# Copy texts over to avoid they are lost
for shape in old_shapes.each():
if shape.is_text():
new_shapes.insert(shape)
cell.shapes(layer_idx).assign(new_shapes)
self._clear()
return layout
def _create_layout(self, name: str):
assert self.layout is not None
assert len(self.cell_lookup) == 0
cell = self.layout.create_cell(name)
self.cell_lookup[name] = (None, cell)
return cell
def _register_cell(self, cell: _cell.Cell):
assert self.layout is not None
assert self.cell_lookup is not None
name = cell.name
if name in self.cell_lookup:
# Check if no two cells from different libraries with the same name are used
if self.cell_lookup[name][0] != cell: # pragma: no cover
raise ValueError(
"Export of hierarchy with two cells from different libraries with same name not supported"
)
else:
self.cell_lookup[name] = (cell, self.layout.create_cell(name))
if cell not in self.cells_done:
self.cells_todo.add(cell)
return self.cell_lookup[name][1]
def _add(self,
o: Union[_geo.MaskShape, _geo.MaskShapes, _lay.LayoutT, _cell.Cell, _lbry.Library],
add_pin_label: bool, net: Optional[_net.NetT],
) -> None:
assert self.shapeexporter is not None
if isinstance(o, _geo.MaskShape):
assert (self.cell is not None) and (self.layerdict is not None)
layer = self.layerdict[o.mask]
shapes = self.cell.shapes(layer)
exp = self.shapeexporter(o.shape)
if _util.is_iterable(exp):
for s in exp:
# s is None if MultiPartShape is already added
if s is not None:
shapes.insert(s)
else:
# exp is None if MultiPartShape has already been converted
if exp is not None:
shapes.insert(exp)
if add_pin_label:
assert self.pinmasks is not None
if o.mask in self.pinmasks:
assert net is not None
for ps in o.shape.pointsshapes:
if isinstance(ps, _geo.Rect):
point = ps.center
else:
point = _util.get_first_of(ps.points)
shapes.insert(pya.DText(net.name, point.x, point.y))
elif isinstance(o, _geo.MaskShapes):
for ms in o:
self._add(ms, add_pin_label=add_pin_label, net=net)
elif isinstance(o, _lay.LayoutT):
prims = o.fab.tech.primitives
pinmasks: List[_msk.DesignMask] = []
for prim in prims:
try:
pin: _prm.Marker = prim.pin # type: ignore
except AttributeError:
pass
else:
pinmasks.append(pin.mask)
self.pinmasks = tuple(pinmasks)
for sl in o._sublayouts:
if isinstance(sl, _laylay._MaskShapesSubLayout):
self._add(sl.shapes, add_pin_label=add_pin_label, net=sl.net)
elif isinstance(sl, _laylay._InstanceSubLayout):
assert self.cell is not None
instcell = self._register_cell(sl.inst.cell)
rot, mirr = _rotation_to_rot_mirr[sl.rotation]
dtrans = pya.DTrans(rot, mirr, sl.origin.x, sl.origin.y)
self.cell.insert(pya.DCellInstArray(instcell.cell_index(), dtrans))
else: # pragma: no cover
raise RuntimeError(
"Internal error: unsupported",
)
elif isinstance(o, _cell.Cell):
assert net is None
# Register cell to be exported
self._register_cell(o)
# Make use of self.cell after this an error
self.cell = None
elif isinstance(o, _lbry.Library):
# Initialize list of cells to tape-out, each of the cells will be
# added in a loop below this function.
assert net is None
self.cells_todo.update(o.cells)
# Register all cells to be exported
for lbrycell in o.cells:
self._register_cell(lbrycell)
# Make use of self.cell after this an error
self.cell = None
else:
raise TypeError(f"No support for exporting a '{type(o)}' object")
@overload
def export2db(
obj: _geo._Shape, *,
export_fullshape: Optional[bool]=None,
add_pin_label: bool=False,
gds_layers: None=None,
cell_name: None=None,
merge: bool=False,
) -> Any:
... # pragma: no cover
@overload
def export2db(
obj: Union[_geo.MaskShape, _geo.MaskShapes, _lay.LayoutT, _lbry.Library], *,
export_fullshape: None=None,
add_pin_label: bool=False,
gds_layers: GDSLayerSpecDict,
cell_name: Optional[str]=None,
merge: bool=False,
) -> "pya.Layout": # type: ignore
... # pragma: no cover
[docs]def export2db(
obj: Union[_geo._Shape, _geo.MaskShape, _geo.MaskShapes, _lay.LayoutT, _lbry.Library], *,
export_fullshape: Optional[bool]=None, add_pin_label: bool=False,
gds_layers: Optional[GDSLayerSpecDict]=None,
cell_name: Optional[str]=None,
merge: bool=False,
):
"""This function allows to export PDKMaster geometry/layout objects
to a klayout Layout object
Arguments:
obj: This is the object to export. There are two different call types.
1) a _Shape geometry object without mask provided
2) a MaskShape geometry or an object that contains maskshapes.
export_fullshape: only to be specified when obj is a _Shape object.
If `True` the full MultiPartShape shape will be exported when a
MultiPartShape._Part object is met. It defaults to `False`
add_pin_label: If `True` a label will exported on top of layers that are pin
layers of one of the technology's MetalWire primitives.
gds_layers: Has to be specified when `obj` is a MaskShape or a collection of them
and should not be provided otherwise.
It contains the lookup table to get the corresponding KLayout layer information
for PDKMaster _DesignMask objects.
cell_name: Only to be provided when `obj` is a MaskShape or a collection of them but
not a Library.
If specified the name of the cell in which the MaskShape(s) will be exported.
By default 'anon' will be used.
merge: Wether to merge the exported shapes or not.
Returns:
An equivalent KLayout database object if obj is a _Shape geometry or a KLayout
Layout object when obj is a MaskShape or a collection of MaskShapes. If obj is
a Library a Cell will be added for each _Cell in the Library. If there are
cell instances in the PDKMaster _Layout object these cells will also be exported
to the output KLayout Layout object as a cell even if it is from another PDKMaster
Library. An exception will be generated when two instances need to be exported to
the same cell name but in a different Library.
"""
# TODO: Provide facility to also export netlist information
if isinstance(obj, _geo._Shape):
assert (gds_layers is None) and (cell_name is None) and (not add_pin_label)
if export_fullshape is None:
export_fullshape = False
_exporter = _ShapeExporter(export_fullshape=export_fullshape)
return _exporter(obj)
else:
assert (export_fullshape is None) and (gds_layers is not None)
return _LayoutExporter()(
obj=obj, gds_layers=gds_layers, cell_name=cell_name, merge=merge,
add_pin_label=add_pin_label,
)