Source code for gdsfactory.component

"""Component is a canvas for geometry."""

from __future__ import annotations

import pathlib
import warnings
from abc import ABC, abstractmethod
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias, cast, overload

import kfactory as kf
import klayout.lay as lay
import networkx as nx
import numpy as np
import numpy.typing as npt
import yaml
from graphviz import Digraph
from kfactory import (
    DInstance,
    DInstances,
    DPort,
    DPorts,
    VInstance,
    cell,
    kdb,
    save_layout_options,
)
from kfactory.exceptions import LockedError
from kfactory.kcell import BaseKCell, ProtoKCell
from kfactory.port import ProtoPort
from matplotlib.figure import Figure
from pydantic import Field
from trimesh.scene.scene import Scene

from gdsfactory.config import CONF, GDSDIR_TEMP
from gdsfactory.serialization import clean_value_json, convert_tuples_to_lists
from gdsfactory.utils import to_kdb_dpoints

if TYPE_CHECKING:
    from gdsfactory.cross_section import CrossSection, CrossSectionSpec
    from gdsfactory.technology.layer_stack import LayerStack
    from gdsfactory.technology.layer_views import LayerViews
    from gdsfactory.typings import (
        AngleInDegrees,
        ComponentSpec,
        Coordinates,
        Layer,
        LayerSpec,
        LayerSpecs,
        PathType,
        Port,
        Position,
    )

cell_without_validator = cell

_PolygonPoints: TypeAlias = "npt.NDArray[np.floating[Any]] | kdb.DPolygon | kdb.Polygon | kdb.DSimplePolygon | kdb.Region | Coordinates"


def ensure_tuple_of_tuples(points: Any) -> tuple[tuple[float, float], ...]:
    # Convert a single NumPy array to a tuple of tuples
    if isinstance(points, np.ndarray):
        points = tuple(map(tuple, points.tolist()))
    elif isinstance(points, list):
        # If it's a list, check if the first element is an np.ndarray or a list to decide on conversion
        if len(points) > 0 and isinstance(points[0], np.ndarray | list):
            points = tuple(tuple(point) for point in points)
    return cast(tuple[tuple[float, float], ...], points)


def points_to_polygon(
    points: _PolygonPoints,
) -> kdb.Polygon | kdb.DPolygon | kdb.DSimplePolygon | kdb.Region:
    if isinstance(points, tuple | list | np.ndarray):
        points = ensure_tuple_of_tuples(points)
        polygon = kdb.DPolygon()
        polygon.assign_hull(to_kdb_dpoints(points))
    elif isinstance(
        points, kdb.Polygon | kdb.DPolygon | kdb.DSimplePolygon | kdb.Region
    ):
        return points
    return kdb.DPolygon(to_kdb_dpoints(points))


def size(region: kdb.Region, offset: float, dbu: float = 1e3) -> kdb.Region:
    return region.dup().size(int(offset * dbu))


def boolean_or(region1: kdb.Region, region2: kdb.Region) -> kdb.Region:
    return (region1.__or__(region2)).merge()


def boolean_not(region1: kdb.Region, region2: kdb.Region) -> kdb.Region:
    return kdb.Region.__sub__(region1, region2)


def boolean_xor(region1: kdb.Region, region2: kdb.Region) -> kdb.Region:
    return kdb.Region.__xor__(region1, region2)


def boolean_and(region1: kdb.Region, region2: kdb.Region) -> kdb.Region:
    return kdb.Region.__and__(region1, region2)


boolean_operations = {
    "or": boolean_or,
    "|": boolean_or,
    "not": boolean_not,
    "-": boolean_not,
    "^": boolean_xor,
    "xor": boolean_xor,
    "&": boolean_and,
    "and": boolean_and,
    "A-B": boolean_not,
}


def copy(region: kdb.Region) -> kdb.Region:
    return region.dup()


ComponentReference: TypeAlias = DInstance
ComponentReferences: TypeAlias = DInstances


class ComponentBase(ProtoKCell[float, BaseKCell], ABC):
    """Canvas where you add polygons, instances and ports.

    - stores settings that you use to build the component
    - stores info that you want to use
    - can return ports by type (optical, electrical ...)
    - can return netlist for circuit simulation
    - can write to GDS, OASIS
    - can show in KLayout, matplotlib or 3D

    Properties:
        info: dictionary that includes derived properties, simulation_settings, settings (test_protocol, docs, ...)
    """

    @property
    def layers(self) -> list[Layer]:
        return [
            (info.layer, info.datatype)
            for info in self.kcl.layout.layer_infos()
            if not self.bbox(self.kcl.layout.layer(info)).empty()
        ]

    @abstractmethod
    def add_polygon(
        self, points: _PolygonPoints, layer: LayerSpec
    ) -> kdb.Shape | None: ...

    def bbox_np(self) -> npt.NDArray[np.float64]:
        """Returns the bounding box of the Component as a numpy array."""
        return np.array(
            [[self.xmin, self.ymin], [self.xmax, self.ymax]], dtype=np.float64
        )

    def add_port(
        self,
        name: str | None = None,
        *,
        port: ProtoPort[Any] | None = None,
        center: Position | kdb.DPoint | None = None,
        width: float | None = None,
        orientation: AngleInDegrees = 0,
        layer: LayerSpec | None = None,
        port_type: str = "optical",
        keep_mirror: bool = False,
        cross_section: "CrossSectionSpec | None" = None,
    ) -> DPort:
        """Adds a Port to the Component.

        Args:
            name: name of the port.
            port: port to add.
            center: center of the port.
            width: width of the port.
            orientation: orientation of the port.
            layer: layer spec to add port on.
            port_type: port type (optical, electrical, ...)
            keep_mirror: if True, keeps the mirror of the port.
            cross_section: cross_section of the port.
        """
        if self.locked:
            raise LockedError(self)

        if port:
            return DPort(
                base=super()
                .add_port(port=port, name=name, keep_mirror=keep_mirror)
                .base
            )

        from gdsfactory.config import CONF
        from gdsfactory.pdk import get_cross_section, get_layer

        if port_type not in CONF.port_types:
            warnings.warn(
                f"Port type {port_type} not in {CONF.port_types}. "
                "Please add it to the port_types list in the config gf.CONF.port_types."
            )

        if layer is None:
            if cross_section is None:
                raise ValueError("Must specify layer or cross_section")
            xs = get_cross_section(cross_section)
            layer = xs.layer

        if width is None:
            if cross_section is None:
                raise ValueError("Must specify width or cross_section")
            xs = get_cross_section(cross_section)
            width = xs.width

        if center is None:
            raise ValueError("Must specify center")

        elif isinstance(center, kdb.DPoint):
            layer = get_layer(layer)
            trans = kdb.DCplxTrans(1, orientation, False, center.to_v())
        else:
            layer = get_layer(layer)
            x = float(center[0])
            y = float(center[1])
            trans = kdb.DCplxTrans(1, float(orientation), False, x, y)

        _port = DPorts(kcl=self.kcl, bases=self.ports.bases).create_port(
            name=name, width=width, layer=layer, port_type=port_type, dcplx_trans=trans
        )
        if cross_section:
            xs = get_cross_section(cross_section)
            _port.info["cross_section"] = xs.name

        return _port

    def copy(self) -> Self:
        """Copy the full cell."""
        return self.dup()

    def add_label(
        self,
        text: str = "hello",
        position: Position | kf.kdb.DPoint = (0.0, 0.0),
        layer: LayerSpec = "TEXT",
    ) -> None:
        """Adds Label to the Component.

        Args:
            text: Label text.
            position: x-, y-coordinates of the Label location.
            layer: Specific layer(s) to put Label on.
        """
        from gdsfactory.pdk import get_layer

        if self.locked:
            raise LockedError(self)

        layer = get_layer(layer)
        if isinstance(position, kf.kdb.DPoint):
            x, y = position.x, position.y
        else:
            x, y = position

        trans = kdb.DTrans(0, False, x, y)
        self.shapes(layer).insert(kf.kdb.DText(text, trans))

    def get_ports_list(self, **kwargs: Any) -> "list[Port]":
        """Returns list of ports.

        Args:
            kwargs: Additional kwargs.

        Keyword Args:
            layer: select ports with GDS layer.
            prefix: select ports with prefix in port name.
            suffix: select ports with port name suffix.
            orientation: select ports with orientation in degrees.
            orientation: select ports with orientation in degrees.
            width: select ports with port width.
            layers_excluded: List of layers to exclude.
            port_type: select ports with port_type (optical, electrical, vertical_te).
            clockwise: if True, sort ports clockwise, False: counter-clockwise.
        """
        from gdsfactory.port import select_ports

        return select_ports(ports=self.ports.to_dtype(), **kwargs)

    def add_route_info(
        self,
        cross_section: "CrossSection | str",
        length: float,
        length_eff: float | None = None,
        taper: bool = False,
        **kwargs: Any,
    ) -> None:
        """Adds route information to a component.

        Args:
            cross_section: CrossSection or name of the cross_section.
            length: length of the route.
            length_eff: effective length of the route.
            taper: if True adds taper information.
            kwargs: extra information to add to the component.
        """
        from gdsfactory.pdk import get_active_pdk

        if self.locked:
            raise LockedError(self)

        pdk = get_active_pdk()

        length_eff = length_eff or length
        xs_name = (
            cross_section
            if isinstance(cross_section, str)
            else pdk.get_cross_section_name(cross_section)
        )

        info = self.info
        if taper:
            info[f"route_info_{xs_name}_taper_length"] = length

        info["route_info_type"] = xs_name
        info["route_info_length"] = length_eff
        info["route_info_weight"] = length_eff
        info[f"route_info_{xs_name}_length"] = length_eff
        for key, value in kwargs.items():
            info[f"route_info_{key}"] = value

    def copy_child_info(self, component: Component) -> None:
        """Copy and settings info from child component into parent.

        Parent components can access child cells settings.
        """
        if self.locked:
            raise LockedError(self)

        info = dict(component.info)

        for k, v in info.items():
            if k not in self.info:
                self.info[k] = v

    def write_gds(
        self,
        gdspath: "PathType | None" = None,
        gdsdir: "PathType | None" = None,
        save_options: "kdb.SaveLayoutOptions | None" = None,
        with_metadata: bool = True,
    ) -> pathlib.Path:
        """Write component to GDS and returns gdspath.

        Args:
            gdspath: GDS file path to write to.
            gdsdir: directory for the GDS file. Defaults to /tmp/randomFile/gdsfactory.
            save_options: klayout save options.
            with_metadata: if True, writes metadata (ports, settings) to the GDS file.
        """
        if gdspath and gdsdir:
            warnings.warn(
                "gdspath and gdsdir have both been specified. "
                "gdspath will take precedence and gdsdir will be ignored.",
                stacklevel=3,
            )
        gdsdir = gdsdir or GDSDIR_TEMP
        gdsdir = pathlib.Path(gdsdir)
        gdsdir.mkdir(parents=True, exist_ok=True)
        name = self.name or ""
        gdspath = gdspath or gdsdir / f"{name[: CONF.max_cellname_length]}.gds"
        gdspath = pathlib.Path(gdspath)

        gdspath.parent.mkdir(parents=True, exist_ok=True)

        if save_options is None:
            save_options = save_layout_options()

        if not with_metadata:
            save_options.write_context_info = False

        self.write(filename=gdspath, save_options=save_options)
        return pathlib.Path(gdspath)

    def pprint_ports(self, **kwargs: Any) -> None:
        """Pretty prints ports.

        Args:
            kwargs: keyword arguments to filter ports.

        Keyword Args:
            layer: select ports with GDS layer.
            prefix: select ports with prefix in port name.
            suffix: select ports with port name suffix.
            orientation: select ports with orientation in degrees.
            orientation: select ports with orientation in degrees.
            width: select ports with port width.
            layers_excluded: List of layers to exclude.
            port_type: select ports with port_type (optical, electrical, vertical_te).
            clockwise: if True, sort ports clockwise, False: counter-clockwise.
        """
        ports = self.get_ports_list(**kwargs)
        from gdsfactory.port import pprint_ports

        pprint_ports(ports)

    def write_netlist(
        self, netlist: dict[str, Any], filepath: str | pathlib.Path | None = None
    ) -> str:
        """Returns netlist as YAML string.

        Args:
            netlist: netlist to write.
            filepath: Optional file path to write to.
        """
        netlist_converted = convert_tuples_to_lists(netlist)
        yaml_string = yaml.dump(netlist_converted)
        if filepath:
            filepath = pathlib.Path(filepath)
            filepath.write_text(yaml_string)
        return str(yaml_string)

    def to_dict(self, with_ports: bool = False) -> dict[str, Any]:
        """Returns a dictionary representation of the Component."""
        from gdsfactory.port import to_dict

        d = {
            "name": self.name,
            "info": self.info.model_dump(exclude_none=True),
            "settings": self.settings.model_dump(exclude_none=True),
        }

        if with_ports:
            d["ports"] = {
                port.name: to_dict(port) for port in self.ports if port.name is not None
            }
        res = clean_value_json(d)
        assert isinstance(res, dict)
        return res


Route: TypeAlias = (
    kf.routing.generic.ManhattanRoute | kf.routing.aa.optical.OpticalAllAngleRoute
)


[docs] class Component(ComponentBase, kf.DKCell): """Canvas where you add polygons, instances and ports. - stores settings that you use to build the component - stores info that you want to use - can return ports by type (optical, electrical ...) - can return netlist for circuit simulation - can write to GDS, OASIS - can show in KLayout, matplotlib or 3D Properties: info: dictionary that includes derived properties, simulation_settings, settings (test_protocol, docs, ...) """ routes: "dict[str, Route]" = Field(default_factory=dict) @property def layers(self) -> list[Layer]: return [ (info.layer, info.datatype) for info in self.kcl.layout.layer_infos() if not self.bbox(self.kcl.layout.layer(info)).empty() ] def add(self, instances: Iterable[ComponentReference] | ComponentReference) -> None: if self.locked: raise LockedError(self) if not isinstance(instances, Iterable): instance_list = [instances] else: instance_list = list(instances) for instance in instance_list: self.kdb_cell.insert(instance.instance) def absorb(self, reference: ComponentReference) -> Self: """Absorbs polygons from ComponentReference into Component. Destroys the reference in the process but keeping the polygon geometry. Args: reference: Instance to be absorbed into the Component. """ if self.locked: raise LockedError(self) if reference not in self.insts: raise ValueError( "The reference you asked to absorb does not exist in this Component." ) reference.flatten() return self def trim( self, left: float, bottom: float, right: float, top: float, flatten: bool = False, ) -> None: """Trims the Component to a bounding box. Args: left: left coordinate of the bounding box. bottom: bottom coordinate of the bounding box. right: right coordinate of the bounding box. top: top coordinate of the bounding box. flatten: if True, flattens the Component. """ if self.locked: raise LockedError(self) c = self domain_box = kdb.DBox(left, bottom, right, top) if not c.dbbox().inside(domain_box): kdb_cell = c.kcl.layout.clip(c.kdb_cell, kdb.DBox(left, bottom, right, top)) c.kdb_cell.clear() c.kdb_cell.copy_tree(kdb_cell) kdb_cell.delete() if flatten: c.flatten() def add_ref( self, component: Component, name: str | None = None, columns: int = 1, rows: int = 1, column_pitch: float = 0.0, row_pitch: float = 0.0, ) -> ComponentReference: """Adds a component instance reference to a Component. Args: component: The referenced component. name: Name of the reference. columns: Number of columns in the array. rows: Number of rows in the array. column_pitch: column pitch. row_pitch: row pitch. """ if self.locked: raise LockedError(self) if rows > 1 or columns > 1: if rows > 1 and row_pitch == 0: raise ValueError(f"rows = {rows} > 1 require {row_pitch=} > 0") if columns > 1 and column_pitch == 0: raise ValueError(f"columns = {columns} > 1 require {column_pitch} > 0") a = kf.kdb.DVector(column_pitch, 0) b = kf.kdb.DVector(0, row_pitch) inst = self.create_inst(component, na=columns, nb=rows, a=a, b=b) else: inst = self.create_inst(component) if name is not None: inst.name = name return ComponentReference(kcl=self.kcl, instance=inst.instance) def get_paths( self, layer: "LayerSpec", recursive: bool = True ) -> list[kf.kdb.DPath]: """Returns a list of paths. Args: layer: layer to get paths from. recursive: if True, gets paths recursively. """ from gdsfactory import get_layer paths: list[kf.kdb.DPath] = [] layer = get_layer(layer) if recursive: iterator = self.kdb_cell.begin_shapes_rec(layer) iterator.shape_flags = kdb.Shapes.SPaths paths.extend( it.shape().dpath.transformed(it.dtrans()) for it in iterator.each() ) else: paths.extend( shape.dpath for shape in self.kdb_cell.shapes(layer).each(kdb.Shapes.SPaths) ) return paths def get_boxes( self, layer: "LayerSpec", recursive: bool = True ) -> list[kf.kdb.DBox]: """Returns a list of boxes. Args: layer: layer to get boxes from. recursive: if True, gets boxes recursively. """ from gdsfactory import get_layer boxes: list[kf.kdb.DBox] = [] layer = get_layer(layer) if recursive: iterator = self.kdb_cell.begin_shapes_rec(layer) iterator.shape_flags = kdb.Shapes.SBoxes boxes.extend( it.shape().dbox.transformed(it.dtrans()) for it in iterator.each() ) else: boxes.extend( shape.dbox for shape in self.kdb_cell.shapes(layer).each(kdb.Shapes.SBoxes) ) return boxes def get_labels( self, layer: "LayerSpec", recursive: bool = True ) -> list[kf.kdb.DText]: """Returns a list of labels from the Component. Args: layer: layer to get labels from. recursive: if True, gets labels recursively. """ from gdsfactory import get_layer texts: list[kf.kdb.DText] = [] layer_enum = get_layer(layer) if recursive: iterator = self.kdb_cell.begin_shapes_rec(layer_enum) iterator.shape_flags = kdb.Shapes.STexts texts.extend( it.shape().dtext.transformed(it.dtrans()) for it in iterator.each() ) else: texts.extend( shape.dtext for shape in self.kdb_cell.shapes(layer_enum).each(kdb.Shapes.STexts) ) return texts def area(self, layer: "LayerSpec") -> float: """Returns the area of the Component in um2.""" from gdsfactory import get_layer layer_index = get_layer(layer) r = kdb.Region(self.kdb_cell.begin_shapes_rec(layer_index)) r.merge() return float(sum(p.area2() / 2 * self.kcl.dbu**2 for p in r.each())) def get_polygons( self, merge: bool = False, by: Literal["index", "name", "tuple"] = "index", layers: "LayerSpecs | None" = None, smooth: float | None = None, ) -> dict[tuple[int, int] | str | int, list[kf.kdb.Polygon]]: """Returns a dict of Polygons per layer. Args: merge: if True, merges the polygons. by: the format of the resulting keys in the dictionary ('index', 'name', 'tuple') layers: list of layers to get polygons from. Defaults to all layers. smooth: if True, smooths the polygons. """ if merge and self.locked: raise LockedError(self) from gdsfactory.functions import get_polygons return get_polygons(self, merge=merge, by=by, layers=layers, smooth=smooth) def get_region( self, layer: "LayerSpec", merge: bool = False, smooth: float | None = None ) -> kdb.Region: """Returns a Region of the Component. Note that all operations that you do with the Region will be done in the database units. Where for most processes 1 dbu = 1 nm. Args: layer: layer to get region from. merge: if True, merges the region. smooth: if True, smooths the region by the specified amount (in um). """ from gdsfactory import get_layer layer_index = get_layer(layer) r = kdb.Region(self.kdb_cell.begin_shapes_rec(layer_index)) if smooth: r.smooth(self.kcl.to_dbu(smooth)) if merge: r.merge() return r def get_polygons_points( self, merge: bool = False, scale: float | None = None, by: Literal["index", "name", "tuple"] = "index", layers: "LayerSpecs | None" = None, ) -> dict[int | str | tuple[int, int], list[npt.NDArray[np.floating[Any]]]]: """Returns a dict with list of points per layer. Args: merge: if True, merges the polygons. scale: if True, scales the points. by: the format of the resulting keys in the dictionary ('index', 'name', 'tuple') layers: list of layers to get polygons from. Defaults to all layers. """ if merge and self.locked: raise LockedError(self) from gdsfactory.functions import get_polygons_points return get_polygons_points(self, merge=merge, scale=scale, by=by, layers=layers) def extract( self, layers: "LayerSpecs", recursive: bool = True, ) -> Component: """Extracts a list of layers and adds them to a new Component. Args: layers: list of layers to extract. recursive: if True, extracts layers recursively and returns a flattened Component. """ from gdsfactory.functions import extract return extract(self, layers=layers, recursive=recursive) def copy_layers( self, layer_map: "dict[LayerSpec, LayerSpec]", recursive: bool = False ) -> Self: """Remaps a list of layers and returns the same Component. Args: layer_map: dictionary of layers to copy. recursive: if True, remaps layers recursively. """ from gdsfactory import get_layer if self.locked: raise LockedError(self) for layer, new_layer in layer_map.items(): src_layer_index = get_layer(layer) dst_layer_index = get_layer(new_layer) self.kdb_cell.copy(src_layer_index, dst_layer_index) if recursive: for ci in self.kdb_cell.called_cells(): self.kcl[ci].kdb_cell.copy(src_layer_index, dst_layer_index) return self def remove_layers( self, layers: "LayerSpecs", recursive: bool = True, ) -> Self: """Removes a list of layers and returns the same Component. Args: layers: list of layers to remove. recursive: if True, removes layers recursively. """ from gdsfactory import get_layer if self.locked: raise LockedError(self) layers = [get_layer(layer) for layer in layers] for layer_index in layers: assert isinstance(layer_index, int) self.kdb_cell.shapes(layer_index).clear() if recursive: [ self.kcl[ci].kdb_cell.shapes(layer).clear() for ci in self.kdb_cell.called_cells() for layer in layers if isinstance(layer, int) ] return self def remap_layers( self, layer_map: "dict[LayerSpec, LayerSpec]", recursive: bool = False ) -> Self: """Remaps a list of layers and returns the same Component. Args: layer_map: dictionary of layers to remap. recursive: if True, remaps layers recursively. """ from gdsfactory import get_layer if self.locked: raise LockedError(self) for layer, new_layer in layer_map.items(): src_layer_index = get_layer(layer) dst_layer_index = get_layer(new_layer) self.kdb_cell.move(src_layer_index, dst_layer_index) if recursive: for ci in self.kdb_cell.called_cells(): self.kcl[ci].kdb_cell.move(src_layer_index, dst_layer_index) return self def to_3d( self, layer_views: "LayerViews | None" = None, layer_stack: "LayerStack | None" = None, exclude_layers: "Sequence[Layer] | None " = None, ) -> Scene: """Return Component 3D trimesh Scene. Args: component: to extrude in 3D. layer_views: layer colors from Klayout Layer Properties file. Defaults to active PDK.layer_views. layer_stack: contains thickness and zmin for each layer. Defaults to active PDK.layer_stack. exclude_layers: layers to exclude. """ from gdsfactory.export.to_3d import to_3d return to_3d( self, layer_views=layer_views, layer_stack=layer_stack, exclude_layers=exclude_layers, ) def over_under( self, layer: "LayerSpec", distance: float = 0.001, remove_old_layer: bool = True ) -> None: """Returns a Component over-under on a layer in the Component. For big components use tiled version. Args: layer: layer to perform over-under on. distance: distance to perform over-under in um. remove_old_layer: if True, removes the old layer. """ from gdsfactory import get_layer if self.locked: raise LockedError(self) distance_dbu = self.kcl.to_dbu(distance) layer_index = get_layer(layer) region = kdb.Region(self.kdb_cell.begin_shapes_rec(layer_index)) region.size(+distance_dbu).size(-distance_dbu) if remove_old_layer: self.remove_layers([layer]) self.kdb_cell.shapes(layer_index).insert(region) self.kcl.layout.end_changes() def offset(self, layer: "LayerSpec", distance: float) -> None: """Offsets a Component layer by a distance in um. Args: layer: layer to offset the Component on. distance: distance to offset the Component in um. """ from gdsfactory import get_layer if self.locked: raise LockedError(self) distance_dbu = self.kcl.to_dbu(distance) layer_index = get_layer(layer) region = kdb.Region(self.kdb_cell.begin_shapes_rec(layer_index)) region.size(distance_dbu) self.remove_layers([layer]) self.kdb_cell.shapes(layer_index).insert(region) self.kcl.layout.end_changes() def add_polygon( self, points: _PolygonPoints, layer: "LayerSpec" ) -> kdb.Shape | None: """Adds a Polygon to the Component and returns a klayout Shape. Args: points: Coordinates of the vertices of the Polygon. layer: layer spec to add polygon on. """ from gdsfactory.pdk import get_layer if self.locked: raise LockedError(self) _layer = get_layer(layer) polygon = points_to_polygon(points) return self.kdb_cell.shapes(_layer).insert(polygon) @overload def plot( self, lyrdb: pathlib.Path | str | None = None, display_type: Literal["image", "widget"] | None = None, *, show_labels: bool = True, show_ruler: bool = True, return_fig: Literal[True] = True, ) -> Figure: ... @overload def plot( self, lyrdb: pathlib.Path | str | None = None, display_type: Literal["image", "widget"] | None = None, *, show_labels: bool = True, show_ruler: bool = True, return_fig: Literal[False] = False, ) -> None: ... def plot( self, lyrdb: pathlib.Path | str | None = None, display_type: Literal["image", "widget"] | None = None, *, show_labels: bool = True, show_ruler: bool = True, return_fig: bool = False, ) -> Figure | None: """Plots the Component using klayout. Args: lyrdb: path to layer properties file. display_type: if "image", displays the image. show_labels: if True, shows labels. show_ruler: if True, shows ruler. return_fig: if True, returns the figure. """ from io import BytesIO import matplotlib.pyplot as plt from gdsfactory.pdk import get_layer_views self.insert_vinsts() lyp_path = GDSDIR_TEMP / "layer_properties.lyp" layer_views = get_layer_views() layer_views.to_lyp(filepath=lyp_path) layout_view = lay.LayoutView() cell_view_index = layout_view.create_layout(True) layout_view.active_cellview_index = cell_view_index cell_view = layout_view.cellview(cell_view_index) layout = cell_view.layout() layout.assign(kf.kcl.layout) assert self.name is not None, "Component name is None" cell_view.cell = layout.cell(self.name) layout_view.max_hier() layout_view.load_layer_props(str(lyp_path)) layout_view.add_missing_layers() layout_view.zoom_fit() layout_view.set_config("text-visible", "true" if show_labels else "false") layout_view.set_config("grid-show-ruler", "true" if show_ruler else "false") pixel_buffer = layout_view.get_pixels_with_options(800, 600) png_data = pixel_buffer.to_png_data() # Convert PNG data to NumPy array and display with matplotlib with BytesIO(png_data) as f: img_array = plt.imread(f) # Compute the figure dimensions based on the image size and desired DPI dpi = 80 fig_width = img_array.shape[1] / dpi fig_height = img_array.shape[0] / dpi fig, ax = plt.subplots(figsize=(fig_width, fig_height), dpi=dpi) # Remove margins and display the image ax.imshow(img_array) ax.axis("off") # Hide axes ax.set_position((0, 0, 1, 1)) # Set axes to occupy the full figure space plt.subplots_adjust( left=0, right=1, top=1, bottom=0, wspace=0, hspace=0 ) # Remove any padding plt.tight_layout(pad=0) # Ensure no space is wasted return fig if return_fig else None def get_netlist(self, recursive: bool = False, **kwargs: Any) -> dict[str, Any]: """Returns a place-aware netlist for circuit simulation. It includes not only the connectivity information (nodes and connections) but also the specific placement coordinates for each component or cell in the layout. Args: recursive: if True, returns a recursive netlist. kwargs: keyword arguments to get_netlist. """ from gdsfactory.get_netlist import get_netlist, get_netlist_recursive if recursive: return get_netlist_recursive(self, **kwargs) return get_netlist(self, **kwargs) def plot_netlist( self, recursive: bool = False, with_labels: bool = True, font_weight: str = "normal", **kwargs: Any, ) -> nx.Graph: """Plots a netlist graph with networkx. Args: recursive: if True, returns a recursive netlist. with_labels: add label to each node. font_weight: normal, bold. kwargs: keyword arguments to get_netlist. Keyword Args: tolerance: tolerance in grid_factor to consider two ports connected. exclude_port_types: optional list of port types to exclude from netlisting. get_instance_name: function to get instance name. allow_multiple: False to raise an error if more than two ports share the same connection. \ if True, will return key: [value] pairs with [value] a list of all connected instances. """ import matplotlib.pyplot as plt import networkx as nx from gdsfactory.get_netlist import nets_to_connections plt.figure() netlist = self.get_netlist(recursive=recursive, **kwargs) G = nx.Graph() if recursive: pos: dict[str, tuple[float, float]] = {} labels: dict[str, str] = {} for net in netlist.values(): nets = net.get("nets", []) connections = net.get("connections", {}) connections = nets_to_connections(nets, connections) placements = net["placements"] G.add_edges_from( [ (",".join(k.split(",")[:-1]), ",".join(v.split(",")[:-1])) for k, v in connections.items() ] ) pos |= {k: (v["x"], v["y"]) for k, v in placements.items()} labels |= {k: ",".join(k.split(",")[:1]) for k in placements.keys()} else: nets = netlist.get("nets", []) connections = netlist.get("connections", {}) connections = nets_to_connections(nets, connections) placements = netlist["placements"] G.add_edges_from( [ (",".join(k.split(",")[:-1]), ",".join(v.split(",")[:-1])) for k, v in connections.items() ] ) pos = {k: (v["x"], v["y"]) for k, v in placements.items()} labels = {k: ",".join(k.split(",")[:1]) for k in placements.keys()} nx.draw( G, with_labels=with_labels, font_weight=font_weight, labels=labels, pos=pos, ) return G def plot_netlist_graphviz( self, recursive: bool = False, interactive: bool = False, splines: str = "ortho" ) -> None: """Plots a netlist graph with graphviz. Args: recursive: if True, returns a recursive netlist. interactive: if True, opens the graph in a browser. splines: ortho, spline, polyline, line, curved. """ from gdsfactory.schematic import plot_graphviz n = self.to_graphviz( recursive=recursive, ) plot_graphviz(n, splines=splines, interactive=interactive) def to_graphviz(self, recursive: bool = False) -> Digraph: """Returns a netlist graph with graphviz. Args: recursive: if True, returns a recursive netlist. """ from gdsfactory.schematic import to_graphviz netlist = self.get_netlist(recursive=recursive) return to_graphviz( netlist["instances"], placements=netlist["placements"], nets=netlist["nets"], )
class ComponentAllAngle(ComponentBase, kf.VKCell): def plot(self, **kwargs: Any) -> None: """Plots the Component using klayout.""" c = Component() if self.name is not None: c.name = self.name VInstance(self).insert_into_flat(c, levels=0) c.plot(**kwargs) def dup(self, new_name: str | None = None) -> Self: """Copy the full cell.""" c = self.__class__( kcl=self.kcl, name=new_name or self.name + "$1" if self.name else None ) c.ports = self.ports.copy() c.settings = self.settings.model_copy() c.settings_units = self.settings_units.model_copy() c.info = self.info.model_copy() for layer, shapes in self.shapes().items(): for shape in shapes: c.shapes(layer).insert(shape) return c def add_polygon(self, points: _PolygonPoints, layer: "LayerSpec") -> None: """Adds a Polygon to the Component and returns a klayout Shape. Args: points: Coordinates of the vertices of the Polygon. layer: layer spec to add polygon on. """ from gdsfactory.pdk import get_layer if self.locked: raise LockedError(self) _layer = get_layer(layer) polygon = points_to_polygon(points) return self.shapes(_layer).insert(polygon) def get_polygons(self, layer: "LayerSpec") -> list[kf.kdb.DPolygon]: """Returns a list of polygons from the Component.""" from gdsfactory import get_layer return [x for x in self.shapes(get_layer(layer)) if isinstance(x, kdb.DPolygon)] def container( component: ComponentSpec, function: Callable[..., Any] | None = None, **kwargs: Any, ) -> Component: """Returns new component with a component reference. Args: component: to add to container. function: function to apply to component. kwargs: keyword arguments to pass to function. """ import gdsfactory as gf component = gf.get_component(component) c = Component() cref = c << component c.add_ports(cref.ports) if function: function(component=c, **kwargs) c.copy_child_info(component) return c if __name__ == "__main__": import gdsfactory as gf from gdsfactory.generic_tech import LAYER c = gf.components.circle() c2 = gf.Component() region = c.get_region(layer=LAYER.WG, smooth=1) region2 = region.sized(100) region3 = region2 - region c2.add_polygon(region3, layer=LAYER.WG) c2.show() # polygons = c.get_polygons(smooth=1)[LAYER.WG] # c2.add_polygon(region, layer=LAYER.WG) # c2