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 kfactory.utils.fill import fill_tiled
from kfactory.utils.violations import (
    fix_spacing_tiled,
    fix_width_minkowski_tiled,
)
from matplotlib.figure import Figure
from pydantic import Field
from trimesh.scene.scene import Scene
from typing_extensions import override

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


class AddPortError(ValueError):
    """Error raised when adding a port fails."""


if TYPE_CHECKING:
    from gdsfactory.cross_section import CrossSection, CrossSectionSpec
    from gdsfactory.get_netlist import (
        ComponentNamer,
        ErrorBehavior,
        InstanceNamer,
        NetlistNamer,
        PortMatcher,
    )
    from gdsfactory.technology.layer_stack import LayerStack
    from gdsfactory.technology.layer_views import LayerViews
    from gdsfactory.typings import (
        AngleInDegrees,
        ComponentSpec,
        Coordinates,
        CornerMode,
        Layer,
        LayerSpec,
        LayerSpecs,
        PathType,
        PixelBufferOptions,
        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, kdb.Polygon | kdb.DPolygon | kdb.DSimplePolygon | kdb.Region):
        return points
    points = ensure_tuple_of_tuples(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
        )

    @override
    def add_port(  # pyright: ignore[reportIncompatibleMethodOverride]
        self,
        name: str | None = None,
        *,
        port: ProtoPort[Any] | None = None,
        center: Position | kdb.DPoint | None = None,
        width: float | None = None,
        orientation: AngleInDegrees | None = None,
        layer: LayerSpec | None = None,
        port_type: str | None = None,
        keep_mirror: bool = False,
        cross_section: CrossSectionSpec | None = None,
        register_cross_section: bool = False,
    ) -> 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. If None and port is provided, preserves the original port's orientation. If None and port is not provided, defaults to 0.
            layer: layer spec to add port on.
            port_type: port type (optical, electrical, …). If None and port is provided, preserves the original port's type. If None and port is not provided, defaults to "optical".
            keep_mirror: if True, keeps the mirror of the port.
            cross_section: cross_section of the port.
            register_cross_section: registers the CrossSection factory
        """
        if self.locked:
            raise LockedError(self)

        from gdsfactory.pdk import get_active_pdk, get_cross_section, get_layer

        # Resolve initial values and determine if we need to override the transformation
        override_transformation = False
        if port:
            override_transformation = (center is not None) or (orientation is not None)
            center = center if center is not None else port.center
            width = width if width is not None else port.width
            orientation = orientation if orientation is not None else port.orientation
            layer = layer if layer is not None else port.layer
            port_type = port_type if port_type is not None else port.port_type
            name = name if name is not None else port.name
            _xs = port.info.get("cross_section")
            _xs_is_registered = (
                isinstance(_xs, str) and _xs in get_active_pdk().cross_sections
            )
            cross_section = (
                cross_section
                if cross_section is not None
                else _xs
                if _xs_is_registered
                else getattr(port, "cross_section", _xs)
            )

        # Apply CrossSection overrides
        xs_name = None
        if cross_section:
            xs = get_cross_section(cross_section)
            xs_name = xs.name
            if layer is None:
                layer = xs.layer
            if width is None:
                width = xs.width

        # Apply defaults if None
        if port_type is None:
            port_type = "optical"
        if orientation is None:
            orientation = 0

        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.",
                stacklevel=3,
            )

        if layer is None:
            raise AddPortError("Must specify layer or cross_section")
        if width is None:
            raise AddPortError("Must specify width or cross_section")
        if center is None:
            raise AddPortError("Must specify center or port")

        # Prefer port.dcplx_trans if port is provided and no overriding parameters are given
        # Otherwise, construct a new transformation based on the provided or inherited parameters
        if not port or override_transformation:
            if isinstance(center, kdb.DPoint):
                trans = kdb.DCplxTrans(1, orientation, False, center.to_v())
            else:
                x, y = float(center[0]), float(center[1])
                trans = kdb.DCplxTrans(1, float(orientation), False, x, y)
        else:
            trans = port.dcplx_trans
            if not keep_mirror:
                trans.mirror = False

        layer = get_layer(layer)

        _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 xs_name:
            _port.info["cross_section"] = xs_name
            if register_cross_section:
                from gdsfactory.pdk import get_active_pdk

                pdk = get_active_pdk()
                if xs_name in pdk.cross_sections:
                    xs_registered = get_cross_section(xs_name)
                    xs_new = xs
                    if xs_registered != xs_new:
                        raise KeyError(
                            f"Found a different CrossSection named {xs_name} in pdk.cross_sections, cannot register {xs_new}"
                        )
                else:
                    pdk.register_cross_sections(**{xs_name: lambda: xs})

        return _port

    def copy(self) -> Component:
        """Copy the full cell."""
        return self.dup()  # type: ignore[return-value]

    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: kf.ProtoTKCell[Any]) -> 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,
        exclude_layers: Sequence[LayerSpec] | None = None,
        no_empty_cells: bool = False,
    ) -> 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.
            exlude_layers: list of layers to exclude from the GDS file.
            no_empty_cells: if True, does not save empty cells.
        """
        from gdsfactory.pdk import get_layer

        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(no_empty_cells=no_empty_cells)

        exclude_layers = exclude_layers or CONF.exclude_layers

        if exclude_layers:
            save_options.deselect_all_layers()
            selected_layers = set(self.kcl.layer_indexes()) - {
                get_layer(drop_layer) for drop_layer in exclude_layers
            }
            for layer in selected_layers:
                save_options.add_layer(layer, kf.kdb.LayerInfo())

        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.
        """
        yaml_string = yaml.safe_dump(netlist)
        if filepath:
            filepath = pathlib.Path(filepath)
            filepath.write_text(yaml_string)
        return 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

    def get_netlist(
        self,
        recursive: bool = False,
        *,
        on_multi_connect: ErrorBehavior = "error",
        on_dangling_port: ErrorBehavior = "warn",
        instance_namer: InstanceNamer | None = None,
        component_namer: ComponentNamer | None = None,
        netlist_namer: NetlistNamer | None = None,
        port_matcher: PortMatcher | None = None,
        serialization_max_digits: int = DEFAULT_SERIALIZATION_MAX_DIGITS,
    ) -> 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.
            on_multi_connect: What to do when more than two ports overlap.
                "ignore": silently allow, "warn": allow with warning, "error": raise.
            on_dangling_port: What to do when an instance port is not connected.
                "ignore": silently allow, "warn": allow with warning, "error": raise.
            instance_namer: Callable to name instances.
                Defaults to SmartNamer(component_namer).
            component_namer: Callable to name components.
                Defaults to function_namer.
            netlist_namer: Callable to name cells in recursive netlists.
                Defaults to CountedNetlistNamer(component_namer). Only used when
                recursive=True.
            port_matcher: Callable to determine if two ports are connected.
                Defaults to SmartPortMatcher().
            serialization_max_digits: How many float digits to preserve.
                Defaults to DEFAULT_SERIALIZATION_MAX_DIGITS
        """
        from gdsfactory.get_netlist import (
            function_namer,
            get_netlist,
            get_netlist_recursive,
        )

        if component_namer is None:
            component_namer = function_namer

        if recursive:
            return get_netlist_recursive(
                self,  # type: ignore[arg-type]
                on_multi_connect=on_multi_connect,
                on_dangling_port=on_dangling_port,
                instance_namer=instance_namer,
                component_namer=component_namer,
                netlist_namer=netlist_namer,
                port_matcher=port_matcher,
                serialization_max_digits=serialization_max_digits,
            )

        return get_netlist(
            self,  # type: ignore[arg-type]
            on_multi_connect=on_multi_connect,
            on_dangling_port=on_dangling_port,
            instance_namer=instance_namer,
            component_namer=component_namer,
            port_matcher=port_matcher,
            serialization_max_digits=serialization_max_digits,
        )

    def add_ref_off_grid(
        self, component: kf.ProtoTKCell[Any], name: str | None = None
    ) -> VInstance:
        """Adds a component instance reference to a Component without snapping to grid.

        Args:
            component: The referenced component.
            name: Name of the reference.
        """
        if self.locked:
            raise LockedError(self)

        ref = self.create_vinst(component)
        if name:
            ref.name = name
        return ref


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 __lshift__(self, cell: kf.ProtoTKCell[Any]) -> ComponentReference: """Convenience function for adding instances/references to a Component. Args: cell: The cell to be added as an instance """ return self.add_ref(cell) def add_ref( self, component: kf.ProtoTKCell[Any], 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 isinstance(component, ComponentAllAngle): raise ValueError( f"Use Component.add_ref_off_grid() for all angle {component.name!r}" ) if not isinstance(component, kf.ProtoTKCell): raise ValueError(f"Expected a Component, got {type(component)}") 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 recursive: self.locked = False 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(): was_locked = self.kcl[ci].locked self.kcl[ci].locked = False self.kcl[ci].kdb_cell.copy(src_layer_index, dst_layer_index) if was_locked: self.kcl[ci].locked = True 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 and temporarily unlocks components. """ from gdsfactory import get_layer if recursive: self.locked = False if self.locked: raise LockedError(self) layer_indexes = self.kcl.layer_indexes() layer_indexes_to_remove = [get_layer(layer) for layer in layers] layers = [layer for layer in layer_indexes_to_remove if layer in layer_indexes] for layer_index in layers: assert isinstance(layer_index, int) self.kdb_cell.shapes(layer_index).clear() if recursive: for ci in self.kdb_cell.called_cells(): for layer_idx in layers: assert isinstance(layer_idx, int) was_locked = self.kcl[ci].locked if recursive: self.kcl[ci].locked = False self.kcl[ci].kdb_cell.shapes(layer_idx).clear() if recursive and was_locked: self.kcl[ci].locked = True 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, corner_mode: int | CornerMode = 2, ) -> 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. corner_mode: determines behavior around corners """ 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, +distance_dbu, corner_mode).size( -distance_dbu, -distance_dbu, corner_mode ) if remove_old_layer: self.remove_layers([layer]) self.kdb_cell.shapes(layer_index).insert(region) self.kcl.layout.end_changes() def fix_spacing( self, layer: LayerSpec, min_space: float = 0.2, size_bias: float = 0.0, ) -> None: """Fixes layer spacing in the Component. Args: layer: layer to fix spacing on. min_space: minimum space in um. size_bias: optional geometry bias applied after spacing fix (um). """ import gdsfactory as gf from gdsfactory.pdk import get_layer layer = get_layer(layer) layer_info = gf.kcl.get_info(layer) fix = fix_spacing_tiled( self.to_itype(), min_space=self.kcl.to_dbu(min_space), layer=layer_info ) if size_bias: size_offset_dbu = self.kcl.to_dbu(size_bias) fix = fix.sized(+size_offset_dbu) fix = fix.sized(-size_offset_dbu) self.shapes(layer).insert(fix) def fix_width( self, layer: LayerSpec, min_width: float = 0.2, n_threads: int | None = None, tile_size: tuple[float, float] | None = None, overlap: int = 1, smooth: int | None = None, flatten: bool = True, ) -> None: """Fixes layer min width in the Component. Args: layer: layer to fix width on. min_width: minimum width in um. n_threads: number of threads to use for processing. tile_size: size of the tiles to use for processing. overlap: overlap between tiles. smooth: smooth the polygons by this amount in um. flatten: if True, flattens the Component before fixing width. """ import gdsfactory as gf from gdsfactory.pdk import get_layer if flatten: self.flatten() layer = get_layer(layer) layer_info = gf.kcl.get_info(layer) fix = fix_width_minkowski_tiled( self.to_itype(), min_width=self.kcl.to_dbu(min_width), ref=layer_info, n_threads=n_threads, tile_size=tile_size, overlap=overlap, smooth=smooth, ) self.shapes(layer).clear() self.shapes(layer).insert(fix) def offset( self, layer: LayerSpec, distance: float, flatten: bool = False, corner_mode: int | CornerMode = 2, ) -> 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. flatten: if True, flattens the Component before offsetting. corner_mode: determines behavior around corners """ from gdsfactory import get_layer if self.locked: raise LockedError(self) if flatten: self.flatten() 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, distance_dbu, corner_mode) 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) if isinstance(polygon, kdb.DPolygon | kdb.DSimplePolygon): polygon = polygon.to_itype(self.kcl.dbu) # type: ignore[assignment] 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, pixel_buffer_options: PixelBufferOptions | None = None, 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, pixel_buffer_options: PixelBufferOptions | None = None, 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, pixel_buffer_options: PixelBufferOptions | None = None, 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. pixel_buffer_options: options for KLayout's get_pixels_with_options. If None, uses default values (width=800, height=600, linewidth=0, oversampling=0, resolution=0). 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( **({"width": 800, "height": 600} | (pixel_buffer_options or {})) ) 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 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 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} 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} 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"], ) def fill( self, fill_cell: ComponentSpec, fill_layers: Iterable[tuple[LayerSpec, float]] = [], fill_regions: Iterable[tuple[kdb.Region, float]] = [], exclude_layers: Iterable[tuple[LayerSpec, float]] = [], exclude_regions: Iterable[tuple[kdb.Region, float]] = [], n_threads: int | None = None, tile_size: tuple[float, float] | None = None, row_step: kdb.DVector | None = None, col_step: kdb.DVector | None = None, x_space: float = 0.0, y_space: float = 0.0, tile_border: tuple[float, float] = (20, 20), multi: bool = False, ) -> None: """Fill a [KCell][kfactory.kcell.KCell]. Args: fill_cell: The cell used as a cell to fill the regions. fill_layers: Tuples of layer and keepout in um. fill_regions: Specific regions to fill. Also tuples like the layers. exclude_layers: Layers to ignore. Tuples like the fill layers exclude_regions: Specific regions to ignore. Tuples like the fill layers. n_threads: Max number of threads used. Defaults to number of cores of the machine. tile_size: Size of the tiles in um. row_step: DVector for steping to the next instance position in the row. x-coordinate must be >= 0. col_step: DVector for steping to the next instance position in the column. y-coordinate must be >= 0. x_space: Spacing between the fill cell bounding boxes in x-direction. y_space: Spacing between the fill cell bounding boxes in y-direction. tile_border: The tile border to consider for excludes multi: Use the region_fill_multi strategy instead of single fill. """ from gdsfactory.pdk import get_component, get_layer_info fill_cell = get_component(fill_cell) fill_layers_converted = [ (get_layer_info(layer), int(spacing)) for layer, spacing in fill_layers ] fill_regions_converted = [ (region, int(spacing)) for region, spacing in fill_regions ] exclude_layers_converted = [ (get_layer_info(layer), int(spacing)) for layer, spacing in exclude_layers ] exclude_regions_converted = [ (region, int(spacing)) for region, spacing in exclude_regions ] fill_tiled( self, fill_cell=fill_cell, fill_layers=fill_layers_converted, fill_regions=fill_regions_converted, exclude_layers=exclude_layers_converted, exclude_regions=exclude_regions_converted, n_threads=n_threads, tile_size=tile_size, row_step=row_step, col_step=col_step, x_space=x_space, y_space=y_space, tile_border=tile_border, multi=multi, )
[docs] 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) -> ComponentAllAngle: """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) c._base.vinsts = self.vinsts.dup() 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, copy_ports: bool = True, **kwargs: Any, ) -> Component: """Returns new component with a component reference. Args: component: to add to container. function: function to apply to component. copy_ports: if True, copies ports from component to container. kwargs: keyword arguments to pass to function. """ import gdsfactory as gf component = gf.get_component(component) c = Component() cref = c << component if copy_ports: c.add_ports(cref.ports) if function: function(component=c, **kwargs) c.copy_child_info(component) return c def nets_to_connections( nets: list[dict[str, Any]], connections: dict[str, Any] ) -> dict[str, str]: # Use the given connections; create a shallow copy to avoid mutating the input. connections = dict(connections) # Flat set of all used ports for O(1) membership check. used = set(connections.keys()) used.update(connections.values()) for net in nets: p = net["p1"] q = net["p2"] if p in used: # Find the already connected q (if any) _q = ( connections[p] if p in connections else next(k for k, v in connections.items() if v == p) ) raise ValueError( "SAX currently does not support multiply connected ports. " f"Got {p}<->{q} and {p}<->{_q}" ) if q in used: _p = ( connections[q] if q in connections else next(k for k, v in connections.items() if v == q) ) raise ValueError( "SAX currently does not support multiply connected ports. " f"Got {p}<->{q} and {_p}<->{q}" ) connections[p] = q used.add(p) used.add(q) return connections