"""Layout verification results.
``drc_check`` and ``lvs_check`` run the engine's rules file, then read its
results database into JSON (KLayout through a bundled macro, Calibre's ASCII
database in compiled code); ``DRCResults._from_dump`` and
``LVSResults._from_dump`` build the result objects from that JSON, whichever
engine wrote it. Coordinates are in micrometers.
"""
from __future__ import annotations
import base64
import math
import subprocess
import time
import warnings
from collections import Counter
from collections.abc import Callable
from dataclasses import dataclass, field, replace
from typing import Any, Literal
from .extension import Component, Label, Port, Rectangle
Point = tuple[float, float]
Polygon = tuple[Point, ...]
Edge = tuple[Point, Point]
LVSStatus = Literal["match", "warning", "mismatch", "nomatch", "skipped"]
LVSVerdict = Literal["match", "match_with_warnings", "mismatch"]
LVSSeverity = Literal["info", "warning", "error"]
# An unknown status is a failure: a pass never comes from a status this module does not know
_FAILED = ("mismatch", "nomatch", "skipped", None)
_SPINNER = "-\\|/"
_SUMMARY_LIMIT = 20
def _point(p: list) -> Point:
return (p[0], p[1])
def _polygon(points: list) -> Polygon:
return tuple(_point(p) for p in points)
def _edge(e: list) -> Edge:
return (_point(e[0]), _point(e[1]))
def _run_process(
argv: list[str],
phase: str,
directory: str | None,
timeout: float | None,
show_progress: bool,
) -> tuple[int, str]:
"""Run ``argv`` in ``directory`` (the current one if ``None``) to completion,
showing ``phase`` and the elapsed time every 0.3 s.
Returns the exit code and the merged output. ``TimeoutError`` after
``timeout`` seconds; the process never outlives this call.
"""
start = time.monotonic()
spinner = 0
with subprocess.Popen(
argv,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
errors="replace",
cwd=directory,
) as process:
try:
while True:
try:
output, _ = process.communicate(timeout=0.3)
return process.returncode, output
except subprocess.TimeoutExpired:
elapsed = time.monotonic() - start
if timeout is not None and elapsed > timeout:
raise TimeoutError(
f"{phase} did not finish within {timeout:g} s."
) from None
if show_progress:
print(f"\r{phase}: {elapsed:.0f} s {_SPINNER[spinner]}", end="", flush=True)
spinner = (spinner + 1) % 4
except BaseException:
process.kill() # the context closes the pipes and reaps it
raise
[docs]
@dataclass(frozen=True)
class Placement:
"""Magnification, then mirroring about x, then counter-clockwise rotation, then displacement."""
x: float
y: float
rotation: float = 0.0
mirror: bool = False
scale: float = 1.0
def _place(p: Placement, point: Point) -> Point:
x, y = point[0], -point[1] if p.mirror else point[1]
angle = math.radians(p.rotation)
c, s = math.cos(angle), math.sin(angle)
return (round(p.x + p.scale * (c * x - s * y), 9), round(p.y + p.scale * (s * x + c * y), 9))
def _compose(outer: Placement, inner: Placement) -> Placement:
"""The placement equal to ``inner`` followed by ``outer``."""
x, y = _place(outer, (inner.x, inner.y))
rotation = (outer.rotation + (-inner.rotation if outer.mirror else inner.rotation)) % 360
return Placement(x, y, rotation, inner.mirror != outer.mirror, inner.scale * outer.scale)
# DRC
[docs]
@dataclass(frozen=True)
class DRCMarker:
"""Marker of one violation, with the cell it was found in."""
cell: str
[docs]
@dataclass(frozen=True)
class EdgeMarker(DRCMarker):
"""One edge, for rules that report edges."""
edge: Edge
[docs]
@dataclass(frozen=True)
class EdgePairMarker(DRCMarker):
"""Two edges; ``symmetric`` for same-layer rules (width, space), directed otherwise,
and ``None`` when the engine does not record it (Calibre)."""
edge_pair: tuple[Edge, Edge]
symmetric: bool | None = True
[docs]
@dataclass(frozen=True)
class PolygonMarker(DRCMarker):
"""A polygon with optional holes; boxes and paths are reported as polygons."""
hull: Polygon
holes: tuple[Polygon, ...] = ()
[docs]
@dataclass(frozen=True)
class TextMarker(DRCMarker):
"""A text output and its position."""
text: str
position: Point
[docs]
@dataclass(frozen=True)
class ValueMarker(DRCMarker):
"""Any other marker value, as the engine prints it (KLayout only)."""
value: str
[docs]
@dataclass(frozen=True)
class DRCCellReference:
"""An example placement of a cell in another cell; ``parent`` is that cell's key."""
parent: str
placement: Placement
[docs]
@dataclass(frozen=True)
class DRCCell:
"""A cell of the results.
A hierarchical run (KLayout's deep mode, Calibre's ``-hier``) reports
each marker in the cell where it occurs, in that cell's coordinates,
with one example placement of the cell in the top cell. KLayout also
splits a cell whose result depends on how it is placed into variants,
which markers name ``name:variant``; Calibre has no variants.
"""
name: str
variant: str = ""
references: tuple[DRCCellReference, ...] = ()
@property
def key(self) -> str:
"""The name markers use for this cell."""
return f"{self.name}:{self.variant}" if self.variant else self.name
def _marker(item: dict[str, Any]) -> DRCMarker:
kind, cell = item["kind"], item["cell"]
if kind == "edge_pair":
return EdgePairMarker(
cell, (_edge(item["edges"][0]), _edge(item["edges"][1])), item["symmetric"]
)
if kind == "edge":
return EdgeMarker(cell, _edge(item["edge"]))
if kind == "polygon":
return PolygonMarker(
cell, _polygon(item["hull"]), tuple(_polygon(h) for h in item["holes"])
)
if kind == "text":
return TextMarker(cell, item["text"], _point(item["position"]))
return ValueMarker(cell, item["value"])
[docs]
@dataclass(frozen=True)
class DRCViolation:
"""The markers of one rule (category)."""
category: str
markers: tuple[DRCMarker, ...]
description: str = ""
@property
def count(self) -> int:
return len(self.markers)
@property
def violated_cells(self) -> tuple[str, ...]:
return tuple(dict.fromkeys(marker.cell for marker in self.markers))
@property
def violations_by_cell(self) -> dict[str, DRCViolation]:
return {
cell: DRCViolation(
self.category, tuple(m for m in self.markers if m.cell == cell), self.description
)
for cell in self.violated_cells
}
def __str__(self) -> str:
return f"{self.category}: {self.count}"
[docs]
@dataclass(frozen=True)
class DRCResults:
"""Results of a DRC run, from any engine."""
violations_by_category: dict[str, DRCViolation]
log: str = ""
exit_code: int = 0
cells: dict[str, DRCCell] = field(default_factory=dict)
top_cell: str | None = None
@property
def is_clean(self) -> bool:
return all(v.count == 0 for v in self.violations_by_category.values())
@property
def violation_counts(self) -> dict[str, int]:
return {name: v.count for name, v in self.violations_by_category.items()}
@property
def violations_by_cell(self) -> dict[str, list[DRCViolation]]:
by_cell: dict[str, list[DRCViolation]] = {}
for violation in self.violations_by_category.values():
for cell, scoped in violation.violations_by_cell.items():
by_cell.setdefault(cell, []).append(scoped)
return by_cell
@property
def violated_cells(self) -> tuple[str, ...]:
return tuple(self.violations_by_cell)
@property
def categories(self) -> tuple[str, ...]:
return tuple(self.violations_by_category)
def __getitem__(self, category: str) -> DRCViolation:
return self.violations_by_category[category]
[docs]
def placement(self, cell: str) -> Placement:
"""Placement of a cell in the top cell, composed from the first reference of each
cell on the way up, as KLayout's marker browser does. Both engines place each cell
directly in the top cell."""
result = Placement(0.0, 0.0)
if cell not in self.cells:
if self.cells:
raise KeyError(cell)
return result
for _ in self.cells:
references = self.cells[cell].references
if not references or references[0].parent not in self.cells:
break
result = _compose(references[0].placement, result)
cell = references[0].parent
return result
[docs]
def in_top_cell(self, marker: DRCMarker) -> DRCMarker:
"""The marker with its coordinates moved into the top cell through the example
placement of its cell. A cell placed several times is reported once, so the
marker lands at that example placement only."""
p = self.placement(marker.cell)
def polygon(points: Polygon) -> Polygon:
return tuple(_place(p, q) for q in points)
if isinstance(marker, EdgeMarker):
return replace(marker, edge=polygon(marker.edge))
if isinstance(marker, EdgePairMarker):
return replace(marker, edge_pair=tuple(polygon(e) for e in marker.edge_pair))
if isinstance(marker, PolygonMarker):
return replace(
marker, hull=polygon(marker.hull), holes=tuple(map(polygon, marker.holes))
)
if isinstance(marker, TextMarker):
return replace(marker, position=_place(p, marker.position))
return marker
def __str__(self) -> str:
lines = [
"DRC results summary",
"--------------------------------",
f"Total violations: {sum(self.violation_counts.values())}",
"",
"Violations by category:",
]
lines.extend(str(v) for v in self.violations_by_category.values())
return "\n".join(lines) + "\n"
@classmethod
def _from_dump(cls, data: dict[str, Any], log: str, exit_code: int) -> DRCResults:
markers: dict[str, list[DRCMarker]] = {c["name"]: [] for c in data["categories"]}
for item in data["markers"]:
markers.setdefault(item["category"], []).append(_marker(item))
loaded = len(data["markers"])
if loaded < data["total"]:
warnings.warn(
f"DRC results hold {data['total']} markers; only the first {loaded} were loaded "
"because of 'max_results'.",
RuntimeWarning,
stacklevel=2,
)
descriptions = {c["name"]: c["description"] for c in data["categories"]}
cells = (
DRCCell(
c["name"],
c["variant"],
tuple(DRCCellReference(r["parent"], _placement(r)) for r in c["references"]),
)
for c in data["cells"]
)
return cls(
{
name: DRCViolation(name, tuple(items), descriptions.get(name, ""))
for name, items in markers.items()
},
log,
exit_code,
{cell.key: cell for cell in cells},
data["top_cell"],
)
# LVS
[docs]
@dataclass(frozen=True)
class LVSLabel:
"""A text label KLayout attached to a layout net."""
layer: str
text: str
position: Point
[docs]
@dataclass(frozen=True)
class LVSConnection:
"""What a net connects to: a circuit pin, a subcircuit pin, or a device terminal."""
kind: Literal["pin", "subcircuit_pin", "device_terminal"]
pin: int | None = None
subcircuit: int | None = None
device: int | None = None
name: str | None = None
[docs]
@dataclass(frozen=True)
class LVSNet:
"""A net of a circuit, with its connections and, for the layout, its geometry per layer."""
id: int
name: str | None = None
connections: tuple[LVSConnection, ...] = ()
shapes: dict[str, tuple[Polygon, ...]] = field(default_factory=dict)
labels: tuple[LVSLabel, ...] = ()
properties: dict[Any, Any] = field(default_factory=dict)
[docs]
@dataclass(frozen=True)
class LVSPin:
"""A circuit pin and the net it connects to."""
index: int
name: str | None = None
net: int | None = None
properties: dict[Any, Any] = field(default_factory=dict)
[docs]
@dataclass(frozen=True)
class LVSSubcircuit:
"""An instance of another circuit; ``pins`` maps its pin indices to net ids."""
id: int
circuit: str
name: str | None = None
placement: Placement | None = None
pins: dict[int, int] = field(default_factory=dict)
properties: dict[Any, Any] = field(default_factory=dict)
[docs]
@dataclass(frozen=True)
class LVSDevicePart:
"""One of the extracted devices combined into a device."""
abstract: str
placement: Placement
[docs]
@dataclass(frozen=True)
class LVSDevice:
"""An extracted or schematic device; ``terminals`` maps terminal names to net ids."""
id: int
device_class: str
name: str | None = None
abstract: str | None = None
placement: Placement | None = None
parameters: dict[str, float] = field(default_factory=dict)
terminals: dict[str, int | None] = field(default_factory=dict)
parts: tuple[LVSDevicePart, ...] = ()
terminal_routes: tuple[tuple[int, str, str], ...] = ()
properties: dict[Any, Any] = field(default_factory=dict)
[docs]
@dataclass(frozen=True)
class LVSParameterDefinition:
"""A parameter of a device class; ``primary`` parameters are compared by LVS."""
name: str
primary: bool
default: float
[docs]
@dataclass(frozen=True)
class LVSDeviceClass:
"""A device class; ``template`` names KLayout's built-in class it derives from."""
name: str
template: str | None = None
terminals: tuple[str, ...] = ()
parameters: tuple[LVSParameterDefinition, ...] = ()
[docs]
@dataclass(frozen=True)
class LVSDeviceAbstract:
"""The geometry of an extracted device, per terminal and layer."""
name: str
device_class: str
terminal_shapes: dict[str, dict[str, tuple[Polygon, ...]]] = field(default_factory=dict)
[docs]
@dataclass(frozen=True)
class LVSLayer:
"""A layer of the extraction and the layers and global nets it connects to."""
name: str
source: str | None = None
connected_to: tuple[str, ...] = ()
soft_connected_to: tuple[str, ...] = ()
global_nets: tuple[str, ...] = ()
soft_global_nets: tuple[str, ...] = ()
def _find(items: tuple, attribute: str, value: int) -> Any:
for item in items:
if getattr(item, attribute) == value:
return item
raise KeyError(value)
[docs]
@dataclass(frozen=True)
class LVSCircuit:
"""A circuit of a netlist, with lookups by id or index that raise ``KeyError``."""
name: str
boundary: Polygon | None = None
nets: tuple[LVSNet, ...] = ()
pins: tuple[LVSPin, ...] = ()
devices: tuple[LVSDevice, ...] = ()
subcircuits: tuple[LVSSubcircuit, ...] = ()
properties: dict[Any, Any] = field(default_factory=dict)
def net(self, net_id: int) -> LVSNet:
return _find(self.nets, "id", net_id)
def device(self, device_id: int) -> LVSDevice:
return _find(self.devices, "id", device_id)
def subcircuit(self, subcircuit_id: int) -> LVSSubcircuit:
return _find(self.subcircuits, "id", subcircuit_id)
def pin(self, index: int) -> LVSPin:
return _find(self.pins, "index", index)
[docs]
@dataclass(frozen=True)
class LVSNetlist:
"""The layout or the schematic netlist; ``circuits`` is in file order, children first."""
circuits: dict[str, LVSCircuit] = field(default_factory=dict)
device_classes: dict[str, LVSDeviceClass] = field(default_factory=dict)
device_abstracts: dict[str, LVSDeviceAbstract] = field(default_factory=dict)
layers: tuple[LVSLayer, ...] = ()
top_circuit: str | None = None
[docs]
@dataclass(frozen=True)
class LVSGeometry:
"""A polygon attached to a message."""
hull: Polygon
holes: tuple[Polygon, ...] = ()
[docs]
@dataclass(frozen=True)
class LVSMessage:
"""A comparison or extraction message."""
severity: LVSSeverity | None = None
text: str = ""
layout_circuit: str | None = None
schematic_circuit: str | None = None
cell: str | None = None
net: str | None = None
category: str | None = None
category_description: str | None = None
geometry: LVSGeometry | None = None
[docs]
@dataclass(frozen=True)
class LVSPair:
"""Two objects KLayout paired; either side is ``None`` when it found no partner."""
kind: Literal["net", "pin", "device", "subcircuit"]
circuit: str
status: LVSStatus | None = None
description: str | None = None
layout: LVSNet | LVSPin | LVSDevice | LVSSubcircuit | None = None
schematic: LVSNet | LVSPin | LVSDevice | LVSSubcircuit | None = None
[docs]
@dataclass(frozen=True)
class LVSComparison:
"""The comparison of a layout circuit with a schematic circuit."""
circuit: str
status: LVSStatus | None = None
description: str | None = None
layout: LVSCircuit | None = None
schematic: LVSCircuit | None = None
messages: tuple[LVSMessage, ...] = ()
nets: tuple[LVSPair, ...] = ()
pins: tuple[LVSPair, ...] = ()
devices: tuple[LVSPair, ...] = ()
subcircuits: tuple[LVSPair, ...] = ()
@property
def pairs(self) -> tuple[LVSPair, ...]:
return (*self.nets, *self.pins, *self.devices, *self.subcircuits)
def _with_status(
comparisons: tuple[LVSComparison, ...], statuses: tuple[str | None, ...]
) -> tuple[LVSPair | LVSComparison, ...]:
return tuple(p for c in comparisons for p in (c, *c.pairs) if p.status in statuses)
def _verdict(
comparisons: tuple[LVSComparison, ...], messages: tuple[LVSMessage, ...]
) -> LVSVerdict:
if _with_status(comparisons, _FAILED) or any(m.severity == "error" for m in messages):
return "mismatch"
if _with_status(comparisons, ("warning",)) or any(m.severity == "warning" for m in messages):
return "match_with_warnings"
return "match"
[docs]
@dataclass(frozen=True)
class LVSResults:
"""Results of a KLayout LVS run."""
verdict: LVSVerdict
comparisons: tuple[LVSComparison, ...]
layout: LVSNetlist
schematic: LVSNetlist
messages: tuple[LVSMessage, ...] = ()
extraction_messages: tuple[LVSMessage, ...] = ()
unit: float = 0.001
log: str = ""
exit_code: int = 0
@property
def top(self) -> LVSComparison | None:
for comparison in self.comparisons:
if comparison.layout is not None and comparison.layout.name == self.layout.top_circuit:
return comparison
return None
@property
def mismatches(self) -> tuple[LVSPair | LVSComparison, ...]:
"""Circuit pairs and pairs that did not match, including those without a known status."""
return _with_status(self.comparisons, _FAILED)
@property
def warnings(self) -> tuple[LVSPair | LVSComparison, ...]:
"""Circuit pairs and pairs that matched with a warning."""
return _with_status(self.comparisons, ("warning",))
def comparison(self, name: str) -> LVSComparison:
for comparison in self.comparisons:
for circuit in (comparison.layout, comparison.schematic):
if circuit is not None and circuit.name == name:
return comparison
raise KeyError(name)
def __str__(self) -> str:
return _summary(self)
@classmethod
def _from_dump(cls, data: dict[str, Any], log: str, exit_code: int) -> LVSResults:
layout = _netlist(data["layout"])
schematic = _netlist(data["schematic"])
comparisons = tuple(_comparison(c, layout, schematic) for c in data["comparisons"])
messages = tuple(_message(m) for m in data["messages"]) + tuple(
m for c in comparisons for m in c.messages
)
return cls(
_verdict(comparisons, messages),
comparisons,
layout,
schematic,
messages,
tuple(_message(m) for m in data["extraction_messages"]),
data["unit"],
log,
exit_code,
)
def _value(value: Any) -> Any:
if isinstance(value, list):
return [_value(v) for v in value]
if isinstance(value, dict):
((tag, content),) = value.items()
if tag == "bytes":
return base64.b64decode(content)
if tag == "float":
return float(content)
return {_key(k): _value(v) for k, v in content}
return value
def _key(value: Any) -> Any:
value = _value(value)
if isinstance(value, list):
return tuple(_key(v) for v in value)
if isinstance(value, dict):
return tuple((k, _key(v)) for k, v in value.items())
return value
def _properties(pairs: list) -> dict[Any, Any]:
return {_key(k): _value(v) for k, v in pairs}
def _placement(p: dict[str, Any] | None) -> Placement | None:
return None if p is None else Placement(p["x"], p["y"], p["rotation"], p["mirror"], p["scale"])
def _shapes(shapes: dict[str, list]) -> dict[str, tuple[Polygon, ...]]:
return {layer: tuple(_polygon(p) for p in polygons) for layer, polygons in shapes.items()}
def _net(n: dict[str, Any]) -> LVSNet:
return LVSNet(
n["id"],
n["name"],
tuple(LVSConnection(**c) for c in n["connections"]),
_shapes(n["shapes"]),
tuple(LVSLabel(t["layer"], t["text"], _point(t["position"])) for t in n["labels"]),
_properties(n["properties"]),
)
def _device(d: dict[str, Any]) -> LVSDevice:
return LVSDevice(
d["id"],
d["device_class"],
d["name"],
d["abstract"],
_placement(d["placement"]),
d["parameters"],
d["terminals"],
tuple(LVSDevicePart(p["abstract"], _placement(p["placement"])) for p in d["parts"]),
tuple(tuple(r) for r in d["terminal_routes"]),
_properties(d["properties"]),
)
def _circuit(c: dict[str, Any]) -> LVSCircuit:
return LVSCircuit(
c["name"],
None if c["boundary"] is None else _polygon(c["boundary"]),
tuple(_net(n) for n in c["nets"]),
tuple(
LVSPin(p["index"], p["name"], p["net"], _properties(p["properties"])) for p in c["pins"]
),
tuple(_device(d) for d in c["devices"]),
tuple(
LVSSubcircuit(
s["id"],
s["circuit"],
s["name"],
_placement(s["placement"]),
dict(s["pins"]),
_properties(s["properties"]),
)
for s in c["subcircuits"]
),
_properties(c["properties"]),
)
def _netlist(n: dict[str, Any]) -> LVSNetlist:
return LVSNetlist(
{c["name"]: _circuit(c) for c in n["circuits"]},
{
d["name"]: LVSDeviceClass(
d["name"],
d["template"],
tuple(d["terminals"]),
tuple(LVSParameterDefinition(*p) for p in d["parameters"]),
)
for d in n["device_classes"]
},
{
a["name"]: LVSDeviceAbstract(
a["name"],
a["device_class"],
{t: _shapes(s) for t, s in a["terminal_shapes"].items()},
)
for a in n["device_abstracts"]
},
tuple(
LVSLayer(
layer["name"],
layer["source"],
tuple(layer["connected_to"]),
tuple(layer["soft_connected_to"]),
tuple(layer["global_nets"]),
tuple(layer["soft_global_nets"]),
)
for layer in n["layers"]
),
n["top_circuit"],
)
def _message(
m: dict[str, Any], layout: str | None = None, schematic: str | None = None
) -> LVSMessage:
geometry = m["geometry"]
return LVSMessage(
m["severity"],
m["text"],
layout,
schematic,
m["cell"],
m["net"],
m["category"],
m["category_description"],
None
if geometry is None
else LVSGeometry(_polygon(geometry["hull"]), tuple(_polygon(h) for h in geometry["holes"])),
)
def _comparison(c: dict[str, Any], layout: LVSNetlist, schematic: LVSNetlist) -> LVSComparison:
sides = (layout.circuits.get(c["layout"]), schematic.circuits.get(c["schematic"]))
label = c["layout"] if c["layout"] is not None else c["schematic"] or ""
def pairs(kind: str) -> tuple[LVSPair, ...]:
key = "index" if kind == "pin" else "id"
lookups = [
None if side is None else {getattr(x, key): x for x in getattr(side, kind + "s")}
for side in sides
]
return tuple(
LVSPair(
kind,
label,
status,
description,
*(
None if i is None else lookup[i]
for lookup, i in zip(lookups, (a, b), strict=True)
),
)
for a, b, status, description in c[kind + "s"]
)
return LVSComparison(
label,
c["status"],
c["description"],
*sides,
tuple(_message(m, c["layout"], c["schematic"]) for m in c["messages"]),
pairs("net"),
pairs("pin"),
pairs("device"),
pairs("subcircuit"),
)
# Summary
def _number(value: float) -> str:
return f"{value:.10g}"
def _label(item: Any) -> str:
if item is None:
return "(none)"
if isinstance(item, LVSPin):
return item.name or f"${item.index}"
return item.name or f"${item.id}"
def _connection(connection: LVSConnection, circuit: LVSCircuit) -> str:
if connection.kind == "pin":
return f"pin {connection.name or connection.pin}"
if connection.kind == "device_terminal":
device = circuit.device(connection.device)
return f"{device.device_class} {device.name or f'${device.id}'} {connection.name}"
subcircuit = circuit.subcircuit(connection.subcircuit)
pin = connection.name or connection.pin
return f"{subcircuit.circuit} {subcircuit.name or f'${subcircuit.id}'} {pin}"
def _placed(subcircuit: LVSSubcircuit) -> str:
text, p = subcircuit.circuit, subcircuit.placement
if p is not None:
text += f" at ({_number(p.x)}, {_number(p.y)})"
if p.rotation:
text += f", rotated {_number(p.rotation)}°"
if p.mirror:
text += ", mirrored"
if p.scale != 1:
text += f", scaled by {_number(p.scale)}"
return text
def _device_text(device: LVSDevice, netlist: LVSNetlist) -> str:
device_class = netlist.device_classes.get(device.device_class)
names = (
[p.name for p in device_class.parameters if p.primary]
if device_class
else list(device.parameters)
)
values = " ".join(
f"{n}={_number(device.parameters[n])}" for n in names if n in device.parameters
)
return f"{device.device_class} {values}".rstrip()
def _pair_lines(pair: LVSPair, comparison: LVSComparison, results: LVSResults) -> list[str]:
line = f" {pair.kind} {_label(pair.layout)} <-> {_label(pair.schematic)}: {pair.status}"
details = []
if pair.kind == "net":
for side, net, circuit in (
("layout", pair.layout, comparison.layout),
("schematic", pair.schematic, comparison.schematic),
):
if net is not None and circuit is not None:
joins = ", ".join(_connection(c, circuit) for c in net.connections)
details.append(f" {side + ':':<10} {joins or 'nothing'}")
elif pair.kind == "subcircuit":
side, subcircuit = (
("layout", pair.layout) if pair.layout is not None else ("schematic", pair.schematic)
)
if subcircuit is not None:
line += f" ({side}: {_placed(subcircuit)})"
elif pair.kind == "device":
for side, device, netlist in (
("layout", pair.layout, results.layout),
("schematic", pair.schematic, results.schematic),
):
if device is not None:
details.append(f" {side + ':':<10} {_device_text(device, netlist)}")
if pair.description:
details.append(f" {pair.description}")
return [line, *details]
def _message_line(message: LVSMessage) -> str:
where = message.layout_circuit or message.schematic_circuit or message.cell
prefix = message.severity or "message"
return f" {prefix} in {where}: {message.text}" if where else f" {prefix}: {message.text}"
def _summary(results: LVSResults) -> str:
comparisons = results.comparisons
counts = Counter(c.status for c in comparisons)
parts = []
for status in ("match", "warning", *_FAILED):
if not counts[status]:
continue
part = f"{counts[status]} {status or 'without status'}"
if status != "match":
names = [c.circuit for c in comparisons if c.status == status]
shown = ", ".join(names[:_SUMMARY_LIMIT])
if len(names) > _SUMMARY_LIMIT:
shown += f", and {len(names) - _SUMMARY_LIMIT} more"
part += f" ({shown})"
parts.append(part)
lines = [
f"LVS verdict: {results.verdict}",
f"Circuits: {len(comparisons)} compared: {', '.join(parts) or 'none'}",
]
def section(title: str, entries: list[list[str]]) -> None:
if entries:
lines.extend(("", title))
for entry in entries[:_SUMMARY_LIMIT]:
lines.extend(entry)
if len(entries) > _SUMMARY_LIMIT:
lines.append(f" ... and {len(entries) - _SUMMARY_LIMIT} more")
for title, statuses in (("Mismatches", _FAILED), ("Warnings", ("warning",))):
for comparison in comparisons:
section(
f"{title} in {comparison.circuit}:",
[
_pair_lines(p, comparison, results)
for p in comparison.pairs
if p.status in statuses
],
)
section("Messages:", [[_message_line(m)] for m in results.messages])
section("Extraction messages:", [[_message_line(m)] for m in results.extraction_messages])
lines.extend(("", f"KLayout exit code: {results.exit_code}"))
return "\n".join(lines)
[docs]
def layout_pins(
design: Component,
core_layer: tuple[int, int],
*,
outline_layer: tuple[int, int] | None,
label_layer: tuple[int, int] | None,
pin_layer: tuple[int, int] | None,
pin_length: float = 0.1,
) -> Callable[[Component], None]:
"""Pre-export function that draws pins over :class:`Port`.
The exported cells of ``design`` are marked by their place in its
hierarchy:
- A device, a cell whose references carry no ports or terminals,
gets its shapes on ``core_layer``, at every depth, copied to
``outline_layer``, so that the runset can keep its waveguide from
joining its ports; a box on ``pin_layer`` straddling each planar
port, as wide as the port's core profile; and the port name on
``label_layer`` at the port center.
- A circuit, a cell with references to cells with ports, gets the
port names, and a box at each port of its references: a port no
instance connects is then a pin too, and a connection the intent
draws to it is reported.
- A route from :mod:`photonforge.parametric`, and the sections inside
it, get nothing: routes are wires in the derived schematic, and the
runset's ``align`` flattens them.
Fiber and Gaussian ports have no layout, and a port whose spec has
no profile on ``core_layer``, such as an electrical port, is not one
the runset connects: neither is marked. A ``core_layer`` no port of
``design`` uses is refused, since nothing would be drawn. These
layers and shapes are one convention; a runset that reads another
takes a function of your own, for which this one is a template.
Args:
design: Component to be checked. Its hierarchy decides which
cells are devices and which are circuits.
core_layer: Waveguide core layer, which the runset connects.
outline_layer: Layer of the device outlines, or ``None`` for
none.
label_layer: Layer of the port names, or ``None`` for none.
pin_layer: Layer of the port boxes, or ``None`` for none.
pin_length: Length of the pin boxes, along the port direction.
Returns:
Function for ``pre_export_function`` of :func:`lvs_check`.
"""
core_layer = tuple(core_layer)
def core_widths(port: object) -> list[float]:
if not isinstance(port, Port):
return []
return [w for w, _, layer in port.spec.path_profiles_list() if layer == core_layer]
ports = [port for cell in (design, *design.dependencies()) for port in cell.ports.values()]
if any(isinstance(port, Port) for port in ports) and not any(map(core_widths, ports)):
raise ValueError(f"No port of '{design.name}' has a path profile on layer {core_layer}.")
def pinned(component: Component) -> bool:
return bool(component.terminals) or any(
isinstance(port, Port) for port in component.ports.values()
)
def route(component: Component) -> bool:
return (component.parametric_function or "").startswith("photonforge.parametric.route")
# The export marks a copy of each cell, under its name before duplicates are renamed, whose
# references are the original children: a cell is known by its name and its children. The
# table keeps the children, so that their wrappers, and so their ids, live as long as mark.
cells: dict[tuple[str, tuple[int, ...]], tuple[bool, list[Component]]] = {}
def classify(component: Component) -> None:
if route(component):
return
children = [reference.component for reference in component.references]
key = (component.name, tuple(id(child) for child in children))
if key in cells:
return
device = not any(pinned(child) for child in children)
cells[key] = (device, children)
if not device:
for child in children:
classify(child)
classify(design)
def mark(cell: Component) -> None:
entry = cells.get((cell.name, tuple(id(r.component) for r in cell.references)))
if entry is None:
return
device, _ = entry
if device and outline_layer is not None:
shapes = [s.copy() for s in cell.get_structures(core_layer)]
if shapes:
cell.add(outline_layer, *shapes)
if pin_layer is not None:
# A circuit boxes the ports of its references: KLayout makes a pin only of a net some
# instance connects, and a connection the intent draws to a port left open must show
boxed = (
cell.ports.values()
if device
else [
port
for reference in cell.references
if not route(reference.component)
for ports in reference.get_ports().values()
for port in ports
]
)
for port in boxed:
widths = core_widths(port)
if widths:
box = Rectangle(
center=tuple(port.center),
size=(pin_length, min(widths)),
rotation=port.input_direction,
)
cell.add(pin_layer, box)
if label_layer is not None:
for name, port in cell.ports.items():
if core_widths(port):
cell.add(label_layer, Label(name, tuple(port.center)))
return mark