Source code for photonforge.verification

"""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