Skip to content

kfactory

KFactory package. Utilities for creating photonic devices.

Uses the klayout package as a backend.

cell module-attribute

cell = cell

Default kcl @cell decorator.

kcl module-attribute

kcl: KCLayout = KCLayout('DEFAULT')

Default library object.

Any KCell uses this object unless another one is specified in the constructor.

vcell module-attribute

vcell = vcell

Default kcl @vcell decorator.

BaseKCell pydantic-model

Bases: BaseModel, ABC

KLayout cell and change its class to KCell.

A KCell is a dynamic proxy for kdb.Cell. It has all the attributes of the official KLayout class. Some attributes have been adjusted to return KCell specific sub classes. If the function is listed here in the docs, they have been adjusted for KFactory specifically. This object will transparently proxy to kdb.Cell. Meaning any attribute not directly defined in this class that are available from the KLayout counter part can still be accessed. The pure KLayout object can be accessed with kdb_cell.

Attributes:

Name Type Description
yaml_tag

Tag for yaml serialization.

ports list[BasePort]

Manages the ports of the cell.

settings KCellSettings

A dictionary containing settings populated by the cell decorator.

info Info

Dictionary for storing additional info if necessary. This is not passed to the GDS and therefore not reversible.

d Info

UMKCell object for easy access to the KCell in um units.

kcl KCLayout

Library object that is the manager of the KLayout

boundary KCLayout

Boundary of the cell.

insts KCLayout

List of instances in the cell.

vinsts VInstances

List of virtual instances in the cell.

size_info VInstances

Size information of the cell.

function_name str | None

Name of the function that created the cell.

Fields:

Source code in kfactory/kcell.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
class BaseKCell(BaseModel, ABC, arbitrary_types_allowed=True):
    """KLayout cell and change its class to KCell.

    A KCell is a dynamic proxy for kdb.Cell. It has all the
    attributes of the official KLayout class. Some attributes have been adjusted
    to return KCell specific sub classes. If the function is listed here in the
    docs, they have been adjusted for KFactory specifically. This object will
    transparently proxy to kdb.Cell. Meaning any attribute not directly
    defined in this class that are available from the KLayout counter part can
    still be accessed. The pure KLayout object can be accessed with
    `kdb_cell`.

    Attributes:
        yaml_tag: Tag for yaml serialization.
        ports: Manages the ports of the cell.
        settings: A dictionary containing settings populated by the
            [cell][kfactory.layout.KCLayout.cell] decorator.
        info: Dictionary for storing additional info if necessary. This is not
            passed to the GDS and therefore not reversible.
        d: UMKCell object for easy access to the KCell in um units.
        kcl: Library object that is the manager of the KLayout
        boundary: Boundary of the cell.
        insts: List of instances in the cell.
        vinsts: List of virtual instances in the cell.
        size_info: Size information of the cell.
        function_name: Name of the function that created the cell.
    """

    ports: list[BasePort] = Field(default_factory=list)
    pins: list[BasePin] = Field(default_factory=list)
    settings: KCellSettings = Field(default_factory=KCellSettings)
    settings_units: KCellSettingsUnits = Field(default_factory=KCellSettingsUnits)
    vinsts: VInstances
    info: Info
    kcl: KCLayout
    function_name: str | None = None
    basename: str | None = None

    @property
    @abstractmethod
    def locked(self) -> bool:
        """If set the cell shouldn't be modified anymore."""
        ...

    @locked.setter
    @abstractmethod
    def locked(self, value: bool) -> None: ...

    def lock(self) -> None:
        """Lock the cell."""
        self.locked = True

    @property
    @abstractmethod
    def name(self) -> str | None: ...

    @name.setter
    @abstractmethod
    def name(self, value: str) -> None: ...

locked abstractmethod property writable

locked: bool

If set the cell shouldn't be modified anymore.

lock

lock() -> None

Lock the cell.

Source code in kfactory/kcell.py
180
181
182
def lock(self) -> None:
    """Lock the cell."""
    self.locked = True

Constants pydantic-model

Bases: BaseModel

Constant Model class.

Config:

  • arbitrary_types_allowed: True
Source code in kfactory/layout.py
 98
 99
100
101
class Constants(BaseModel):
    """Constant Model class."""

    model_config = ConfigDict(arbitrary_types_allowed=True)

DInstance

Bases: ProtoTInstance[float], UMGeometricObject

An Instance of a KCell.

An Instance is a reference to a KCell with a transformation.

Attributes:

Name Type Description
_instance

The internal kdb.Instance reference

ports DInstancePorts

Transformed ports of the KCell

kcl

Pointer to the layout object holding the instance

d

Helper that allows retrieval of instance information in um

Source code in kfactory/instance.py
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
class DInstance(ProtoTInstance[float], UMGeometricObject):
    """An Instance of a KCell.

    An Instance is a reference to a KCell with a transformation.

    Attributes:
        _instance: The internal `kdb.Instance` reference
        ports: Transformed ports of the KCell
        kcl: Pointer to the layout object holding the instance
        d: Helper that allows retrieval of instance information in um
    """

    yaml_tag: ClassVar[str] = "!Instance"

    def __init__(self, kcl: KCLayout, instance: kdb.Instance) -> None:
        """Create an instance from a KLayout Instance."""
        self.kcl = kcl
        self._instance = instance

    @functools.cached_property
    def ports(self) -> DInstancePorts:
        """Gets the transformed ports of the KCell."""
        from .instance_ports import DInstancePorts

        return DInstancePorts(self)

    @functools.cached_property
    def pins(self) -> DInstancePins:
        """Gets the transformed ports of the KCell."""
        from .instance_pins import DInstancePins

        return DInstancePins(self)

    @property
    def cell(self) -> DKCell:
        """Parent KCell  of the Instance."""
        return self.kcl.dkcells[self.cell_index]

    @cell.setter
    def cell(self, value: ProtoTKCell[Any]) -> None:
        self.cell_index = value.cell_index()

    @property
    def parent_cell(self) -> DKCell:
        """Gets the cell this instance is contained in."""
        return self.kcl.dkcells[self._instance.parent_cell.cell_index()]

    @parent_cell.setter
    def parent_cell(self, cell: KCell | DKCell | kdb.Cell) -> None:
        if isinstance(cell, KCell | DKCell):
            self.parent_cell.insts.remove(
                Instance(kcl=self.kcl, instance=self._instance)
            )
            self._instance.parent_cell = cell.kdb_cell
        else:
            self.parent_cell.insts.remove(
                Instance(kcl=self.kcl, instance=self._instance)
            )
            self._instance.parent_cell = cell

    def __getitem__(
        self, key: int | str | tuple[int | str | None, int, int] | None
    ) -> DPort:
        """Returns port from instance.

        The key can either be an integer, in which case the nth port is
        returned, or a string in which case the first port with a matching
        name is returned.

        If the instance is an array, the key can also be a tuple in the
        form of `c.ports[key_name, i_a, i_b]`, where `i_a` is the index in
        the `instance.a` direction and `i_b` the `instance.b` direction.

        E.g. `c.ports["a", 3, 5]`, accesses the ports of the instance which is
        3 times in `a` direction (4th index in the array), and 5 times in `b` direction
        (5th index in the array).
        """
        return DPort(base=self.ports[key].base)

cell property writable

cell: DKCell

Parent KCell of the Instance.

kcl instance-attribute

kcl = kcl

KCLayout object.

parent_cell property writable

parent_cell: DKCell

Gets the cell this instance is contained in.

pins cached property

pins: DInstancePins

Gets the transformed ports of the KCell.

ports cached property

ports: DInstancePorts

Gets the transformed ports of the KCell.

__getitem__

__getitem__(
    key: int
    | str
    | tuple[int | str | None, int, int]
    | None,
) -> DPort

Returns port from instance.

The key can either be an integer, in which case the nth port is returned, or a string in which case the first port with a matching name is returned.

If the instance is an array, the key can also be a tuple in the form of c.ports[key_name, i_a, i_b], where i_a is the index in the instance.a direction and i_b the instance.b direction.

E.g. c.ports["a", 3, 5], accesses the ports of the instance which is 3 times in a direction (4th index in the array), and 5 times in b direction (5th index in the array).

Source code in kfactory/instance.py
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
def __getitem__(
    self, key: int | str | tuple[int | str | None, int, int] | None
) -> DPort:
    """Returns port from instance.

    The key can either be an integer, in which case the nth port is
    returned, or a string in which case the first port with a matching
    name is returned.

    If the instance is an array, the key can also be a tuple in the
    form of `c.ports[key_name, i_a, i_b]`, where `i_a` is the index in
    the `instance.a` direction and `i_b` the `instance.b` direction.

    E.g. `c.ports["a", 3, 5]`, accesses the ports of the instance which is
    3 times in `a` direction (4th index in the array), and 5 times in `b` direction
    (5th index in the array).
    """
    return DPort(base=self.ports[key].base)

__init__

__init__(kcl: KCLayout, instance: Instance) -> None

Create an instance from a KLayout Instance.

Source code in kfactory/instance.py
597
598
599
600
def __init__(self, kcl: KCLayout, instance: kdb.Instance) -> None:
    """Create an instance from a KLayout Instance."""
    self.kcl = kcl
    self._instance = instance

DInstanceGroup

Bases: ProtoTInstanceGroup[float, DInstance], UMGeometricObject, DCreatePort

Group of DInstances.

The instance group can be treated similar to a single instance with regards to transformation functions and bounding boxes.

Parameters:

Name Type Description Default
insts Sequence[TInstance_co] | None

List of the dinstances of the group.

None
Source code in kfactory/instance_group.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
class DInstanceGroup(
    ProtoTInstanceGroup[float, DInstance], UMGeometricObject, DCreatePort
):
    """Group of DInstances.

    The instance group can be treated similar to a single instance
    with regards to transformation functions and bounding boxes.

    Args:
        insts: List of the dinstances of the group.
    """

    @cached_property
    def ports(self) -> DPorts:
        return DPorts(kcl=self.kcl, bases=self._base_ports)

    def add_port(
        self,
        *,
        port: ProtoPort[Any],
        name: str | None = None,
        keep_mirror: bool = False,
    ) -> DPort:
        return self.ports.add_port(port=port, name=name, keep_mirror=keep_mirror)

ports cached property

ports: DPorts

Ports of the instance.

DInstancePorts

Bases: ProtoTInstancePorts[float]

Source code in kfactory/instance_ports.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
class DInstancePorts(ProtoTInstancePorts[float]):
    def __init__(self, instance: DInstance) -> None:
        """Creates the virtual ports object.

        Args:
            instance: The related instance
        """
        self.instance = instance

    @property
    def cell_ports(self) -> DPorts:
        return DPorts(kcl=self.instance.cell.kcl, bases=self.instance.cell.ports.bases)

    def filter(
        self,
        angle: int | None = None,
        orientation: float | None = None,
        layer: LayerEnum | int | None = None,
        port_type: str | None = None,
        regex: str | None = None,
    ) -> Sequence[DPort]:
        return [
            DPort(base=p.base)
            for p in super().filter(angle, orientation, layer, port_type, regex)
        ]

    def __getitem__(
        self, key: int | str | tuple[int | str | None, int, int] | None
    ) -> DPort:
        return DPort(base=super().__getitem__(key).base)

    def __iter__(self) -> Iterator[DPort]:
        yield from (p.to_dtype() for p in self.each_port())

__getitem__

__getitem__(
    key: int
    | str
    | tuple[int | str | None, int, int]
    | None,
) -> DPort

Returns port from instance.

The key can either be an integer, in which case the nth port is returned, or a string in which case the first port with a matching name is returned.

If the instance is an array, the key can also be a tuple in the form of c.ports[key_name, i_a, i_b], where i_a is the index in the instance.a direction and i_b the instance.b direction.

E.g. c.ports["a", 3, 5], accesses the ports of the instance which is 3 times in a direction (4th index in the array), and 5 times in b direction (5th index in the array).

Source code in kfactory/instance_ports.py
371
372
373
374
def __getitem__(
    self, key: int | str | tuple[int | str | None, int, int] | None
) -> DPort:
    return DPort(base=super().__getitem__(key).base)

__init__

__init__(instance: DInstance) -> None

Creates the virtual ports object.

Parameters:

Name Type Description Default
instance DInstance

The related instance

required
Source code in kfactory/instance_ports.py
346
347
348
349
350
351
352
def __init__(self, instance: DInstance) -> None:
    """Creates the virtual ports object.

    Args:
        instance: The related instance
    """
    self.instance = instance

filter

filter(
    angle: int | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> Sequence[DPort]

Filter ports by name.

Parameters:

Name Type Description Default
angle int | None

Filter by angle. 0, 1, 2, 3.

None
orientation float | None

Filter by orientation in degrees.

None
layer LayerEnum | int | None

Filter by layer.

None
port_type str | None

Filter by port type.

None
regex str | None

Filter by regex of the name.

None
Source code in kfactory/instance_ports.py
358
359
360
361
362
363
364
365
366
367
368
369
def filter(
    self,
    angle: int | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> Sequence[DPort]:
    return [
        DPort(base=p.base)
        for p in super().filter(angle, orientation, layer, port_type, regex)
    ]

DInstances

Bases: ProtoTInstances[float]

Holder for instances.

Allows retrieval by name or index

Source code in kfactory/instances.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
class DInstances(ProtoTInstances[float]):
    """Holder for instances.

    Allows retrieval by name or index
    """

    def __iter__(self) -> Iterator[DInstance]:
        """Get instance iterator."""
        yield from (
            DInstance(kcl=self._tkcell.kcl, instance=inst) for inst in self._insts
        )

    def __getitem__(self, key: str | int) -> DInstance:
        """Retrieve instance by index or by name."""
        if isinstance(key, int):
            return DInstance(kcl=self._tkcell.kcl, instance=list(self._insts)[key])
        return DInstance(kcl=self._tkcell.kcl, instance=self._get_inst(key))

__getitem__

__getitem__(key: str | int) -> DInstance

Retrieve instance by index or by name.

Source code in kfactory/instances.py
161
162
163
164
165
def __getitem__(self, key: str | int) -> DInstance:
    """Retrieve instance by index or by name."""
    if isinstance(key, int):
        return DInstance(kcl=self._tkcell.kcl, instance=list(self._insts)[key])
    return DInstance(kcl=self._tkcell.kcl, instance=self._get_inst(key))

__iter__

__iter__() -> Iterator[DInstance]

Get instance iterator.

Source code in kfactory/instances.py
155
156
157
158
159
def __iter__(self) -> Iterator[DInstance]:
    """Get instance iterator."""
    yield from (
        DInstance(kcl=self._tkcell.kcl, instance=inst) for inst in self._insts
    )

DKCell

Bases: ProtoTKCell[float], UMGeometricObject, DCreatePort

Cell with floating point units.

Source code in kfactory/kcell.py
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
class DKCell(ProtoTKCell[float], UMGeometricObject, DCreatePort):
    """Cell with floating point units."""

    yaml_tag: ClassVar[str] = "!DKCell"

    @overload
    def __init__(self, *, base: TKCell) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        kcl: KCLayout | None = None,
        kdb_cell: kdb.Cell | None = None,
        ports: Iterable[ProtoPort[Any]] | None = None,
        info: dict[str, Any] | None = None,
        settings: dict[str, Any] | None = None,
        pins: Iterable[ProtoPin[Any]] | None = None,
    ) -> None: ...

    def __init__(
        self,
        name: str | None = None,
        kcl: KCLayout | None = None,
        kdb_cell: kdb.Cell | None = None,
        ports: Iterable[ProtoPort[Any]] | None = None,
        info: dict[str, Any] | None = None,
        settings: dict[str, Any] | None = None,
        pins: Iterable[ProtoPin[Any]] | None = None,
        *,
        base: TKCell | None = None,
    ) -> None:
        """Constructor of KCell.

        Args:
            base: If not `None`, a KCell will be created from and existing
                KLayout Cell
            name: Name of the cell, if None will autogenerate name to
                "Unnamed_<cell_index>".
            kcl: KCLayout the cell should be attached to.
            kdb_cell: If not `None`, a KCell will be created from and existing
                KLayout Cell
            ports: Attach an existing [Ports][kfactory.kcell.Ports] object to the KCell,
                if `None` create an empty one.
            info: Info object to attach to the KCell.
            settings: KCellSettings object to attach to the KCell.
        """
        super().__init__(
            base=base,
            name=name,
            kcl=kcl,
            kdb_cell=kdb_cell,
            ports=ports,
            info=info,
            settings=settings,
            pins=pins,
        )

    @property
    def ports(self) -> DPorts:
        """Ports associated with the cell."""
        return DPorts(kcl=self.kcl, bases=self._base.ports)

    @ports.setter
    def ports(self, new_ports: Iterable[ProtoPort[Any]]) -> None:
        if self.locked:
            raise LockedError(self)
        self._base.ports = [port.base for port in new_ports]

    @property
    def pins(self) -> DPins:
        """Pins associated with the cell."""
        return DPins(kcl=self.kcl, bases=self._base.pins)

    @pins.setter
    def pins(self, new_pins: Iterable[ProtoPin[Any]]) -> None:
        if self.locked:
            raise LockedError(self)
        self._base.pins = [pin.base for pin in new_pins]

    @property
    def insts(self) -> DInstances:
        """Instances associated with the cell."""
        return DInstances(cell=self._base)

    def __lshift__(self, cell: AnyTKCell) -> DInstance:
        """Convenience function for `DKCell.create_inst`.

        Args:
            cell: The cell to be added as an instance
        """
        return DInstance(kcl=self.kcl, instance=self.create_inst(cell).instance)

    def add_port(
        self,
        *,
        port: ProtoPort[Any],
        name: str | None = None,
        keep_mirror: bool = False,
    ) -> DPort:
        """Create a port in the cell."""
        if self.locked:
            raise LockedError(self)

        return self.ports.add_port(
            port=port,
            name=name,
            keep_mirror=keep_mirror,
        )

    def create_pin(
        self,
        *,
        ports: Iterable[ProtoPort[Any]],
        name: str | None = None,
        pin_type: str = "DC",
        info: dict[str, int | float | str] | None = None,
    ) -> DPin:
        """Create a pin in the cell."""
        return self.pins.create_pin(
            name=name, ports=ports, pin_type=pin_type, info=info
        )

    def __getitem__(self, key: int | str | None) -> DPort:
        """Returns port from instance."""
        return self.ports[key]

    def create_inst(
        self,
        cell: ProtoTKCell[Any] | int,
        trans: kdb.DTrans | kdb.DVector | kdb.DCplxTrans | None = None,
        *,
        a: kdb.DVector | None = None,
        b: kdb.DVector | None = None,
        na: int = 1,
        nb: int = 1,
        libcell_as_static: bool = False,
        static_name_separator: str = "__",
    ) -> DInstance:
        return DInstance(
            kcl=self.kcl,
            instance=self.dcreate_inst(
                cell,
                trans or kdb.DTrans(),
                a=a,
                b=b,
                na=na,
                nb=nb,
                libcell_as_static=libcell_as_static,
                static_name_separator=static_name_separator,
            ).instance,
        )

    def get_cross_section(
        self,
        cross_section: str
        | dict[str, Any]
        | Callable[..., CrossSection | DCrossSection]
        | SymmetricalCrossSection,
        **cross_section_kwargs: Any,
    ) -> DCrossSection:
        if isinstance(cross_section, str):
            return DCrossSection(
                kcl=self.kcl, base=self.kcl.cross_sections[cross_section]
            )
        if isinstance(cross_section, SymmetricalCrossSection):
            return DCrossSection(kcl=self.kcl, base=cross_section)
        if callable(cross_section):
            any_cross_section = cross_section(**cross_section_kwargs)
            return DCrossSection(kcl=self.kcl, base=any_cross_section._base)
        if isinstance(cross_section, dict):
            return DCrossSection(
                kcl=self.kcl,
                name=cross_section.get("name"),
                **cross_section["settings"],
            )
        raise ValueError(
            "Cannot create a cross section from "
            f"{type(cross_section)=} and {cross_section_kwargs=}"
        )

    @property
    def library_cell(self) -> DKCell:
        if self.kdb_cell.is_library_cell():
            lib_cell = self.base._library_cell
            assert lib_cell is not None
            return DKCell(base=lib_cell.base)
        raise ValueError(
            "This is not a proxy cell referencing a library cell. Please check"
            " with `.is_library_cell()` first if unsure."
        )

insts property

insts: DInstances

Instances associated with the cell.

pins property writable

pins: DPins

Pins associated with the cell.

ports property writable

ports: DPorts

Ports associated with the cell.

__getitem__

__getitem__(key: int | str | None) -> DPort

Returns port from instance.

Source code in kfactory/kcell.py
2913
2914
2915
def __getitem__(self, key: int | str | None) -> DPort:
    """Returns port from instance."""
    return self.ports[key]

__init__

__init__(*, base: TKCell) -> None
__init__(
    name: str | None = None,
    kcl: KCLayout | None = None,
    kdb_cell: Cell | None = None,
    ports: Iterable[ProtoPort[Any]] | None = None,
    info: dict[str, Any] | None = None,
    settings: dict[str, Any] | None = None,
    pins: Iterable[ProtoPin[Any]] | None = None,
) -> None
__init__(
    name: str | None = None,
    kcl: KCLayout | None = None,
    kdb_cell: Cell | None = None,
    ports: Iterable[ProtoPort[Any]] | None = None,
    info: dict[str, Any] | None = None,
    settings: dict[str, Any] | None = None,
    pins: Iterable[ProtoPin[Any]] | None = None,
    *,
    base: TKCell | None = None,
) -> None

Constructor of KCell.

Parameters:

Name Type Description Default
base TKCell | None

If not None, a KCell will be created from and existing KLayout Cell

None
name str | None

Name of the cell, if None will autogenerate name to "Unnamed_".

None
kcl KCLayout | None

KCLayout the cell should be attached to.

None
kdb_cell Cell | None

If not None, a KCell will be created from and existing KLayout Cell

None
ports Iterable[ProtoPort[Any]] | None

Attach an existing Ports object to the KCell, if None create an empty one.

None
info dict[str, Any] | None

Info object to attach to the KCell.

None
settings dict[str, Any] | None

KCellSettings object to attach to the KCell.

None
Source code in kfactory/kcell.py
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
def __init__(
    self,
    name: str | None = None,
    kcl: KCLayout | None = None,
    kdb_cell: kdb.Cell | None = None,
    ports: Iterable[ProtoPort[Any]] | None = None,
    info: dict[str, Any] | None = None,
    settings: dict[str, Any] | None = None,
    pins: Iterable[ProtoPin[Any]] | None = None,
    *,
    base: TKCell | None = None,
) -> None:
    """Constructor of KCell.

    Args:
        base: If not `None`, a KCell will be created from and existing
            KLayout Cell
        name: Name of the cell, if None will autogenerate name to
            "Unnamed_<cell_index>".
        kcl: KCLayout the cell should be attached to.
        kdb_cell: If not `None`, a KCell will be created from and existing
            KLayout Cell
        ports: Attach an existing [Ports][kfactory.kcell.Ports] object to the KCell,
            if `None` create an empty one.
        info: Info object to attach to the KCell.
        settings: KCellSettings object to attach to the KCell.
    """
    super().__init__(
        base=base,
        name=name,
        kcl=kcl,
        kdb_cell=kdb_cell,
        ports=ports,
        info=info,
        settings=settings,
        pins=pins,
    )

__lshift__

__lshift__(cell: AnyTKCell) -> DInstance

Convenience function for DKCell.create_inst.

Parameters:

Name Type Description Default
cell AnyTKCell

The cell to be added as an instance

required
Source code in kfactory/kcell.py
2875
2876
2877
2878
2879
2880
2881
def __lshift__(self, cell: AnyTKCell) -> DInstance:
    """Convenience function for `DKCell.create_inst`.

    Args:
        cell: The cell to be added as an instance
    """
    return DInstance(kcl=self.kcl, instance=self.create_inst(cell).instance)

add_port

add_port(
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> DPort

Create a port in the cell.

Source code in kfactory/kcell.py
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
def add_port(
    self,
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> DPort:
    """Create a port in the cell."""
    if self.locked:
        raise LockedError(self)

    return self.ports.add_port(
        port=port,
        name=name,
        keep_mirror=keep_mirror,
    )

create_pin

create_pin(
    *,
    ports: Iterable[ProtoPort[Any]],
    name: str | None = None,
    pin_type: str = "DC",
    info: dict[str, int | float | str] | None = None,
) -> DPin

Create a pin in the cell.

Source code in kfactory/kcell.py
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
def create_pin(
    self,
    *,
    ports: Iterable[ProtoPort[Any]],
    name: str | None = None,
    pin_type: str = "DC",
    info: dict[str, int | float | str] | None = None,
) -> DPin:
    """Create a pin in the cell."""
    return self.pins.create_pin(
        name=name, ports=ports, pin_type=pin_type, info=info
    )

DPin

Bases: ProtoPin[float]

Source code in kfactory/pin.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
class DPin(ProtoPin[float]):
    def __getitem__(self, key: int | str | None) -> DPort:
        if isinstance(key, int):
            return DPort(base=list(self._base.ports)[key])
        try:
            return DPort(
                base=next(
                    filter(lambda port_base: port_base.name == key, self._base.ports)
                )
            )
        except StopIteration as e:
            raise KeyError(
                f"{key=} is not a valid port name or index within the pin. "
                f"Available ports: {[v.name for v in self.ports]}"
            ) from e

    def copy(
        self,
        trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
        post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    ) -> DPin:
        """Get a copy of a pin.

        Transformation order which results in `copy.trans`:
            - Trans: `trans * pin.trans * post_trans`
            - DCplxTrans: `trans * pin.dcplx_trans * post_trans`

        Args:
            trans: an optional transformation applied to the pin to be copied.
            post_trans: transformation to apply to the pin after copying.

        Returns:
            pin: a copy of the pin
        """
        return DPin(base=self._base.transformed(trans=trans, post_trans=post_trans))

    @property
    def ports(self) -> list[DPort]:
        return [DPort(base=pb) for pb in self._base.ports]

    @ports.setter
    def ports(self, value: Iterable[ProtoPort[Any]]) -> None:
        self._base.ports = [p.base for p in value]

__getitem__

__getitem__(key: int | str | None) -> DPort

Get a port in the pin by index or name.

Source code in kfactory/pin.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def __getitem__(self, key: int | str | None) -> DPort:
    if isinstance(key, int):
        return DPort(base=list(self._base.ports)[key])
    try:
        return DPort(
            base=next(
                filter(lambda port_base: port_base.name == key, self._base.ports)
            )
        )
    except StopIteration as e:
        raise KeyError(
            f"{key=} is not a valid port name or index within the pin. "
            f"Available ports: {[v.name for v in self.ports]}"
        ) from e

copy

copy(
    trans: Trans | DCplxTrans = kdb.Trans.R0,
    post_trans: Trans | DCplxTrans = kdb.Trans.R0,
) -> DPin

Get a copy of a pin.

Transformation order which results in copy.trans: - Trans: trans * pin.trans * post_trans - DCplxTrans: trans * pin.dcplx_trans * post_trans

Parameters:

Name Type Description Default
trans Trans | DCplxTrans

an optional transformation applied to the pin to be copied.

R0
post_trans Trans | DCplxTrans

transformation to apply to the pin after copying.

R0

Returns:

Name Type Description
pin DPin

a copy of the pin

Source code in kfactory/pin.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def copy(
    self,
    trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
) -> DPin:
    """Get a copy of a pin.

    Transformation order which results in `copy.trans`:
        - Trans: `trans * pin.trans * post_trans`
        - DCplxTrans: `trans * pin.dcplx_trans * post_trans`

    Args:
        trans: an optional transformation applied to the pin to be copied.
        post_trans: transformation to apply to the pin after copying.

    Returns:
        pin: a copy of the pin
    """
    return DPin(base=self._base.transformed(trans=trans, post_trans=post_trans))

DPins

Bases: ProtoPins[float]

Source code in kfactory/pins.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
class DPins(ProtoPins[float]):
    yaml_tag: ClassVar[str] = "!DPins"

    def __iter__(self) -> Iterator[DPin]:
        """Iterator, that allows for loops etc to directly access the object."""
        yield from (DPin(base=b) for b in self._bases)

    def __getitem__(self, key: int | str | None) -> DPin:
        """Get a specific pin by name."""
        if isinstance(key, int):
            return DPin(base=self._bases[key])
        try:
            return DPin(base=next(filter(lambda base: base.name == key, self._bases)))
        except StopIteration as e:
            raise KeyError(
                f"{key=} is not a valid pin name or index. "
                f"Available pins: {[v.name for v in self._bases]}"
            ) from e

    def create_pin(
        self,
        *,
        ports: Iterable[ProtoPort[Any]],
        name: str | None = None,
        pin_type: str = "DC",
        info: dict[str, int | float | str] | None = None,
    ) -> DPin:
        """Add a pin to Pins."""
        if info is None:
            info = {}
        info_ = Info(**info)
        if len(list(ports)) < 1:
            raise ValueError(
                f"At least one port must provided to create pin named {name}."
            )
        port_bases = []
        for port in ports:
            port_base = port.base
            if port.kcl != self.kcl:
                port_base.kcl = self.kcl
            port_bases.append(port_base)

        base_ = BasePin(
            name=name, kcl=self.kcl, ports=port_bases, pin_type=pin_type, info=info_
        )
        self._bases.append(base_)

        return DPin(base=base_)

    def get_all_named(self) -> Mapping[str, DPin]:
        """Get all pins in a dictionary with names as keys."""
        return {v.name: DPin(base=v) for v in self._bases if v.name is not None}

__getitem__

__getitem__(key: int | str | None) -> DPin

Get a specific pin by name.

Source code in kfactory/pins.py
183
184
185
186
187
188
189
190
191
192
193
def __getitem__(self, key: int | str | None) -> DPin:
    """Get a specific pin by name."""
    if isinstance(key, int):
        return DPin(base=self._bases[key])
    try:
        return DPin(base=next(filter(lambda base: base.name == key, self._bases)))
    except StopIteration as e:
        raise KeyError(
            f"{key=} is not a valid pin name or index. "
            f"Available pins: {[v.name for v in self._bases]}"
        ) from e

__iter__

__iter__() -> Iterator[DPin]

Iterator, that allows for loops etc to directly access the object.

Source code in kfactory/pins.py
179
180
181
def __iter__(self) -> Iterator[DPin]:
    """Iterator, that allows for loops etc to directly access the object."""
    yield from (DPin(base=b) for b in self._bases)

create_pin

create_pin(
    *,
    ports: Iterable[ProtoPort[Any]],
    name: str | None = None,
    pin_type: str = "DC",
    info: dict[str, int | float | str] | None = None,
) -> DPin

Add a pin to Pins.

Source code in kfactory/pins.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def create_pin(
    self,
    *,
    ports: Iterable[ProtoPort[Any]],
    name: str | None = None,
    pin_type: str = "DC",
    info: dict[str, int | float | str] | None = None,
) -> DPin:
    """Add a pin to Pins."""
    if info is None:
        info = {}
    info_ = Info(**info)
    if len(list(ports)) < 1:
        raise ValueError(
            f"At least one port must provided to create pin named {name}."
        )
    port_bases = []
    for port in ports:
        port_base = port.base
        if port.kcl != self.kcl:
            port_base.kcl = self.kcl
        port_bases.append(port_base)

    base_ = BasePin(
        name=name, kcl=self.kcl, ports=port_bases, pin_type=pin_type, info=info_
    )
    self._bases.append(base_)

    return DPin(base=base_)

get_all_named

get_all_named() -> Mapping[str, DPin]

Get all pins in a dictionary with names as keys.

Source code in kfactory/pins.py
225
226
227
def get_all_named(self) -> Mapping[str, DPin]:
    """Get all pins in a dictionary with names as keys."""
    return {v.name: DPin(base=v) for v in self._bases if v.name is not None}

DPort

Bases: ProtoPort[float]

A port is the photonics equivalent to a pin in electronics.

In addition to the location and layer that defines a pin, a port also contains an orientation and a width. This can be fully represented with a transformation, integer and layer_index.

Attributes:

Name Type Description
name str | None

String to name the port.

width float

The width of the port in dbu.

trans Trans

Transformation in dbu. If the port can be represented in 90° intervals this is the safe way to do so.

dcplx_trans DCplxTrans

Transformation in micrometer. The port will autoconvert between trans and dcplx_trans on demand.

port_type str

A string defining the type of the port

layer LayerEnum | int

Index of the layer or a LayerEnum that acts like an integer, but can contain layer number and datatype

info Info

A dictionary with additional info. Not reflected in GDS. Copy will make a (shallow) copy of it.

d Info

Access port info in micrometer basis such as width and center / angle.

kcl KCLayout

Link to the layout this port resides in.

Source code in kfactory/port.py
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
class DPort(ProtoPort[float]):
    """A port is the photonics equivalent to a pin in electronics.

    In addition to the location and layer
    that defines a pin, a port also contains an orientation and a width.
    This can be fully represented with a transformation, integer and layer_index.


    Attributes:
        name: String to name the port.
        width: The width of the port in dbu.
        trans: Transformation in dbu. If the port can be represented in 90° intervals
            this is the safe way to do so.
        dcplx_trans: Transformation in micrometer. The port will autoconvert between
            trans and dcplx_trans on demand.
        port_type: A string defining the type of the port
        layer: Index of the layer or a LayerEnum that acts like an integer, but can
            contain layer number and datatype
        info: A dictionary with additional info. Not reflected in GDS. Copy will make a
            (shallow) copy of it.
        d: Access port info in micrometer basis such as width and center / angle.
        kcl: Link to the layout this port resides in.
    """

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: float,
        layer: LayerEnum | int,
        trans: kdb.Trans | str,
        kcl: KCLayout | None = None,
        port_type: str = "optical",
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: float,
        layer: LayerEnum | int,
        dcplx_trans: kdb.DCplxTrans | str,
        kcl: KCLayout | None = None,
        port_type: str = "optical",
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: float,
        layer: LayerEnum | int,
        port_type: str = "optical",
        orientation: float,
        center: tuple[float, float] = (0, 0),
        mirror_x: bool = False,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: float,
        layer_info: kdb.LayerInfo,
        trans: kdb.Trans | str,
        kcl: KCLayout | None = None,
        port_type: str = "optical",
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: float,
        layer_info: kdb.LayerInfo,
        dcplx_trans: kdb.DCplxTrans | str,
        kcl: KCLayout | None = None,
        port_type: str = "optical",
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: float,
        layer_info: kdb.LayerInfo,
        port_type: str = "optical",
        orientation: float,
        center: tuple[float, float] = (0, 0),
        mirror_x: bool = False,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        cross_section: DCrossSection | SymmetricalCrossSection,
        port_type: str = "optical",
        orientation: float,
        center: tuple[float, float],
        mirror_x: bool = False,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        cross_section: DCrossSection | SymmetricalCrossSection,
        trans: kdb.Trans | str,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
        port_type: str = "optical",
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        cross_section: DCrossSection | SymmetricalCrossSection,
        dcplx_trans: kdb.DCplxTrans | str,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
        port_type: str = "optical",
    ) -> None: ...

    @overload
    def __init__(self, *, base: BasePort) -> None: ...

    @overload
    def __init__(self, *, port: ProtoPort[Any]) -> None: ...

    def __init__(
        self,
        name: str | None = None,
        *,
        width: float | None = None,
        layer: int | None = None,
        layer_info: kdb.LayerInfo | None = None,
        port_type: str = "optical",
        trans: kdb.Trans | str | None = None,
        dcplx_trans: kdb.DCplxTrans | str | None = None,
        orientation: float = 0,
        center: tuple[float, float] = (0, 0),
        mirror_x: bool = False,
        port: ProtoPort[Any] | None = None,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] | None = None,
        cross_section: DCrossSection | SymmetricalCrossSection | None = None,
        base: BasePort | None = None,
    ) -> None:
        """Create a port from dbu or um based units."""
        if info is None:
            info = {}
        if base is not None:
            self._base = base
            return
        if port is not None:
            self._base = port.base.__copy__()
            return
        info_ = Info(**info)

        from .layout import get_default_kcl

        kcl_ = kcl or get_default_kcl()
        if cross_section is None:
            if layer_info is None:
                if layer is None:
                    raise ValueError("layer or layer_info for a port must be defined")
                layer_info = kcl_.layout.get_info(layer)
            if width is None:
                raise ValueError(
                    "If a cross_section is not given a width must be defined."
                )
            width_ = kcl_.to_dbu(width)
            if width_ % 2:
                raise ValueError(
                    f"width needs to be even to snap to grid. Got {width}."
                    "Ports must have a grid width of multiples of 2."
                )
            cross_section_ = kcl_.get_symmetrical_cross_section(
                CrossSectionSpec(layer=layer_info, width=kcl_.to_dbu(width))
            )
        elif isinstance(cross_section, SymmetricalCrossSection):
            cross_section_ = cross_section
        else:
            cross_section_ = cross_section.base
        if trans is not None:
            trans_ = kdb.Trans.from_s(trans) if isinstance(trans, str) else trans.dup()
            self._base = BasePort(
                name=name,
                kcl=kcl_,
                cross_section=cross_section_,
                trans=trans_,
                info=info_,
                port_type=port_type,
            )
        elif dcplx_trans is not None:
            if isinstance(dcplx_trans, str):
                dcplx_trans_ = kdb.DCplxTrans.from_s(dcplx_trans)
            else:
                dcplx_trans_ = dcplx_trans.dup()
            self._base = BasePort(
                name=name,
                kcl=kcl_,
                cross_section=cross_section_,
                dcplx_trans=dcplx_trans_,
                info=info_,
                port_type=port_type,
            )
        else:
            assert center is not None
            dcplx_trans_ = kdb.DCplxTrans.R0
            self._base = BasePort(
                name=name,
                kcl=kcl_,
                cross_section=cross_section_,
                dcplx_trans=dcplx_trans_,
                info=info_,
                port_type=port_type,
            )
            self.center = center
            self.orientation = orientation
            self.mirror_x = mirror_x

    def copy(
        self,
        trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
        post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    ) -> DPort:
        """Get a copy of a port.

        Transformation order which results in `copy.trans`:
            - Trans: `trans * port.trans * post_trans`
            - DCplxTrans: `trans * port.dcplx_trans * post_trans`

        Args:
            trans: an optional transformation applied to the port to be copied.
            post_trans: transformation to apply to the port after copying.

        Returns:
            port: a copy of the port
        """
        return DPort(base=self._base.transformed(trans=trans, post_trans=post_trans))

    def copy_polar(
        self,
        d: float = 0,
        d_orth: float = 0,
        orientation: float = 180,
        mirror: bool = False,
    ) -> DPort:
        """Get a polar copy of the port.

        This will return a port which is transformed relatively to the original port's
        transformation (orientation, angle and position).

        Args:
            d: The distance to the old port
            d_orth: Orthogonal distance (positive is positive y for a port which is
                facing angle=0°)
            orientation: Relative angle to the original port, in degrees.
            mirror: Whether to mirror the port relative to the original port.
        """
        return self.copy(
            post_trans=kdb.DCplxTrans(rot=orientation, mirrx=mirror, x=d, y=d_orth)
        )

    @property
    def x(self) -> float:
        """X coordinate of the port in um."""
        return self.dx

    @x.setter
    def x(self, value: float) -> None:
        self.dx = value

    @property
    def y(self) -> float:
        """Y coordinate of the port in um."""
        return self.dy

    @y.setter
    def y(self, value: float) -> None:
        self.dy = value

    @property
    def width(self) -> float:
        """Width of the port in um."""
        return self.dwidth

    @property
    def cross_section(self) -> DCrossSection:
        """Get the cross section of the port."""
        return DCrossSection(kcl=self._base.kcl, base=self._base.cross_section)

    @cross_section.setter
    def cross_section(
        self, value: SymmetricalCrossSection | TCrossSection[Any]
    ) -> None:
        if isinstance(value, SymmetricalCrossSection):
            self._base.cross_section = value
            return
        self._base.cross_section = value.base

center instance-attribute

center = center

Returns port center.

cross_section property writable

cross_section: DCrossSection

Get the cross section of the port.

orientation instance-attribute

orientation = orientation

Returns orientation in degrees for gdsfactory compatibility.

In the range of [0,360)

width property

width: float

Width of the port in um.

x property writable

x: float

X coordinate of the port in um.

y property writable

y: float

Y coordinate of the port in um.

__init__

__init__(
    name: str | None = None,
    *,
    width: float,
    layer: LayerEnum | int,
    trans: Trans | str,
    kcl: KCLayout | None = None,
    port_type: str = "optical",
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    width: float,
    layer: LayerEnum | int,
    dcplx_trans: DCplxTrans | str,
    kcl: KCLayout | None = None,
    port_type: str = "optical",
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    width: float,
    layer: LayerEnum | int,
    port_type: str = "optical",
    orientation: float,
    center: tuple[float, float] = (0, 0),
    mirror_x: bool = False,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    width: float,
    layer_info: LayerInfo,
    trans: Trans | str,
    kcl: KCLayout | None = None,
    port_type: str = "optical",
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    width: float,
    layer_info: LayerInfo,
    dcplx_trans: DCplxTrans | str,
    kcl: KCLayout | None = None,
    port_type: str = "optical",
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    width: float,
    layer_info: LayerInfo,
    port_type: str = "optical",
    orientation: float,
    center: tuple[float, float] = (0, 0),
    mirror_x: bool = False,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    cross_section: DCrossSection | SymmetricalCrossSection,
    port_type: str = "optical",
    orientation: float,
    center: tuple[float, float],
    mirror_x: bool = False,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    cross_section: DCrossSection | SymmetricalCrossSection,
    trans: Trans | str,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
    port_type: str = "optical",
) -> None
__init__(
    name: str | None = None,
    *,
    cross_section: DCrossSection | SymmetricalCrossSection,
    dcplx_trans: DCplxTrans | str,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
    port_type: str = "optical",
) -> None
__init__(*, base: BasePort) -> None
__init__(*, port: ProtoPort[Any]) -> None
__init__(
    name: str | None = None,
    *,
    width: float | None = None,
    layer: int | None = None,
    layer_info: LayerInfo | None = None,
    port_type: str = "optical",
    trans: Trans | str | None = None,
    dcplx_trans: DCplxTrans | str | None = None,
    orientation: float = 0,
    center: tuple[float, float] = (0, 0),
    mirror_x: bool = False,
    port: ProtoPort[Any] | None = None,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] | None = None,
    cross_section: DCrossSection
    | SymmetricalCrossSection
    | None = None,
    base: BasePort | None = None,
) -> None

Create a port from dbu or um based units.

Source code in kfactory/port.py
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
def __init__(
    self,
    name: str | None = None,
    *,
    width: float | None = None,
    layer: int | None = None,
    layer_info: kdb.LayerInfo | None = None,
    port_type: str = "optical",
    trans: kdb.Trans | str | None = None,
    dcplx_trans: kdb.DCplxTrans | str | None = None,
    orientation: float = 0,
    center: tuple[float, float] = (0, 0),
    mirror_x: bool = False,
    port: ProtoPort[Any] | None = None,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] | None = None,
    cross_section: DCrossSection | SymmetricalCrossSection | None = None,
    base: BasePort | None = None,
) -> None:
    """Create a port from dbu or um based units."""
    if info is None:
        info = {}
    if base is not None:
        self._base = base
        return
    if port is not None:
        self._base = port.base.__copy__()
        return
    info_ = Info(**info)

    from .layout import get_default_kcl

    kcl_ = kcl or get_default_kcl()
    if cross_section is None:
        if layer_info is None:
            if layer is None:
                raise ValueError("layer or layer_info for a port must be defined")
            layer_info = kcl_.layout.get_info(layer)
        if width is None:
            raise ValueError(
                "If a cross_section is not given a width must be defined."
            )
        width_ = kcl_.to_dbu(width)
        if width_ % 2:
            raise ValueError(
                f"width needs to be even to snap to grid. Got {width}."
                "Ports must have a grid width of multiples of 2."
            )
        cross_section_ = kcl_.get_symmetrical_cross_section(
            CrossSectionSpec(layer=layer_info, width=kcl_.to_dbu(width))
        )
    elif isinstance(cross_section, SymmetricalCrossSection):
        cross_section_ = cross_section
    else:
        cross_section_ = cross_section.base
    if trans is not None:
        trans_ = kdb.Trans.from_s(trans) if isinstance(trans, str) else trans.dup()
        self._base = BasePort(
            name=name,
            kcl=kcl_,
            cross_section=cross_section_,
            trans=trans_,
            info=info_,
            port_type=port_type,
        )
    elif dcplx_trans is not None:
        if isinstance(dcplx_trans, str):
            dcplx_trans_ = kdb.DCplxTrans.from_s(dcplx_trans)
        else:
            dcplx_trans_ = dcplx_trans.dup()
        self._base = BasePort(
            name=name,
            kcl=kcl_,
            cross_section=cross_section_,
            dcplx_trans=dcplx_trans_,
            info=info_,
            port_type=port_type,
        )
    else:
        assert center is not None
        dcplx_trans_ = kdb.DCplxTrans.R0
        self._base = BasePort(
            name=name,
            kcl=kcl_,
            cross_section=cross_section_,
            dcplx_trans=dcplx_trans_,
            info=info_,
            port_type=port_type,
        )
        self.center = center
        self.orientation = orientation
        self.mirror_x = mirror_x

copy

copy(
    trans: Trans | DCplxTrans = kdb.Trans.R0,
    post_trans: Trans | DCplxTrans = kdb.Trans.R0,
) -> DPort

Get a copy of a port.

Transformation order which results in copy.trans: - Trans: trans * port.trans * post_trans - DCplxTrans: trans * port.dcplx_trans * post_trans

Parameters:

Name Type Description Default
trans Trans | DCplxTrans

an optional transformation applied to the port to be copied.

R0
post_trans Trans | DCplxTrans

transformation to apply to the port after copying.

R0

Returns:

Name Type Description
port DPort

a copy of the port

Source code in kfactory/port.py
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
def copy(
    self,
    trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
) -> DPort:
    """Get a copy of a port.

    Transformation order which results in `copy.trans`:
        - Trans: `trans * port.trans * post_trans`
        - DCplxTrans: `trans * port.dcplx_trans * post_trans`

    Args:
        trans: an optional transformation applied to the port to be copied.
        post_trans: transformation to apply to the port after copying.

    Returns:
        port: a copy of the port
    """
    return DPort(base=self._base.transformed(trans=trans, post_trans=post_trans))

copy_polar

copy_polar(
    d: float = 0,
    d_orth: float = 0,
    orientation: float = 180,
    mirror: bool = False,
) -> DPort

Get a polar copy of the port.

This will return a port which is transformed relatively to the original port's transformation (orientation, angle and position).

Parameters:

Name Type Description Default
d float

The distance to the old port

0
d_orth float

Orthogonal distance (positive is positive y for a port which is facing angle=0°)

0
orientation float

Relative angle to the original port, in degrees.

180
mirror bool

Whether to mirror the port relative to the original port.

False
Source code in kfactory/port.py
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
def copy_polar(
    self,
    d: float = 0,
    d_orth: float = 0,
    orientation: float = 180,
    mirror: bool = False,
) -> DPort:
    """Get a polar copy of the port.

    This will return a port which is transformed relatively to the original port's
    transformation (orientation, angle and position).

    Args:
        d: The distance to the old port
        d_orth: Orthogonal distance (positive is positive y for a port which is
            facing angle=0°)
        orientation: Relative angle to the original port, in degrees.
        mirror: Whether to mirror the port relative to the original port.
    """
    return self.copy(
        post_trans=kdb.DCplxTrans(rot=orientation, mirrx=mirror, x=d, y=d_orth)
    )

DPorts

Bases: ProtoPorts[float], DCreatePort

A collection of um ports.

It is not a traditional dictionary. Elements can be retrieved as in a traditional dictionary. But to keep tabs on names etc, the ports are stored as a list

Source code in kfactory/ports.py
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
class DPorts(ProtoPorts[float], DCreatePort):
    """A collection of um ports.

    It is not a traditional dictionary. Elements can be retrieved as in a traditional
    dictionary. But to keep tabs on names etc, the ports are stored as a list
    """

    yaml_tag: ClassVar[str] = "!DPorts"

    def __iter__(self) -> Iterator[DPort]:
        """Iterator, that allows for loops etc to directly access the object."""
        yield from (DPort(base=b) for b in self._bases)

    def add_port(
        self,
        *,
        port: ProtoPort[Any],
        name: str | None = None,
        keep_mirror: bool = False,
    ) -> DPort:
        """Add a port object.

        Args:
            port: The port to add
            name: Overwrite the name of the port
            keep_mirror: Keep the mirror flag from the original port if `True`,
                else set [Port.trans.mirror][kfactory.kcell.Port.trans] (or the complex
                equivalent) to `False`.
        """
        if port.kcl == self.kcl:
            base = port.base.model_copy()
            if not keep_mirror:
                if base.trans is not None:
                    base.trans.mirror = False
                elif base.dcplx_trans is not None:
                    base.dcplx_trans.mirror = False
            if name is not None:
                base.name = name
            self._bases.append(base)
            port_ = DPort(base=base)
        else:
            dcplx_trans = port.dcplx_trans.dup()
            if not keep_mirror:
                dcplx_trans.mirror = False
            base = port.base.model_copy()
            base.trans = kdb.Trans.R0
            base.dcplx_trans = None
            base.kcl = self.kcl
            base.cross_section = self.kcl.get_symmetrical_cross_section(
                port.cross_section.base.to_dtype(port.kcl)
            )
            port_ = DPort(base=base)
            port_.dcplx_trans = dcplx_trans
            self._bases.append(port_.base)
        return port_

    def get_all_named(self) -> Mapping[str, DPort]:
        """Get all ports in a dictionary with names as keys."""
        return {v.name: DPort(base=v) for v in self._bases if v.name is not None}

    @overload
    def __getitem__(self, key: int | str | None) -> DPort:
        """Get a port by index or name."""

    @overload
    def __getitem__(self, key: slice) -> Self:
        """Get ports by slice."""

    def __getitem__(self, key: slice | int | str | None) -> Self | DPort:
        if isinstance(key, int):
            return DPort(base=self._bases[key])
        if isinstance(key, slice):
            return self.__class__(bases=self._bases[key], kcl=self.kcl)
        try:
            return DPort(base=next(filter(lambda base: base.name == key, self._bases)))
        except StopIteration as e:
            raise KeyError(
                f"{key=} is not a valid port name or index. "
                f"Available ports: {[v.name for v in self._bases]}"
            ) from e

    def copy(
        self, rename_function: Callable[[Sequence[DPort]], None] | None = None
    ) -> Self:
        """Get a copy of each port."""
        bases = [b.__copy__() for b in self._bases]
        if rename_function is not None:
            rename_function([DPort(base=b) for b in bases])
        return self.__class__(bases=bases, kcl=self.kcl)

    def filter(
        self,
        angle: Angle | None = None,
        orientation: float | None = None,
        layer: LayerEnum | int | None = None,
        port_type: str | None = None,
        regex: str | None = None,
    ) -> list[DPort]:
        """Filter ports by name.

        Args:
            angle: Filter by angle. 0, 1, 2, 3.
            orientation: Alias for angle.
            layer: Filter by layer.
            port_type: Filter by port type.
            regex: Filter by regex of the name.
        """
        return _filter_ports(
            (DPort(base=b) for b in self._bases),
            angle,
            orientation,
            layer,
            port_type,
            regex,
        )

    def __repr__(self) -> str:
        """Representation of the Ports as strings."""
        return repr([repr(Port(base=b)) for b in self._bases])

__iter__

__iter__() -> Iterator[DPort]

Iterator, that allows for loops etc to directly access the object.

Source code in kfactory/ports.py
794
795
796
def __iter__(self) -> Iterator[DPort]:
    """Iterator, that allows for loops etc to directly access the object."""
    yield from (DPort(base=b) for b in self._bases)

__repr__

__repr__() -> str

Representation of the Ports as strings.

Source code in kfactory/ports.py
901
902
903
def __repr__(self) -> str:
    """Representation of the Ports as strings."""
    return repr([repr(Port(base=b)) for b in self._bases])

add_port

add_port(
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> DPort

Add a port object.

Parameters:

Name Type Description Default
port ProtoPort[Any]

The port to add

required
name str | None

Overwrite the name of the port

None
keep_mirror bool

Keep the mirror flag from the original port if True, else set Port.trans.mirror (or the complex equivalent) to False.

False
Source code in kfactory/ports.py
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
def add_port(
    self,
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> DPort:
    """Add a port object.

    Args:
        port: The port to add
        name: Overwrite the name of the port
        keep_mirror: Keep the mirror flag from the original port if `True`,
            else set [Port.trans.mirror][kfactory.kcell.Port.trans] (or the complex
            equivalent) to `False`.
    """
    if port.kcl == self.kcl:
        base = port.base.model_copy()
        if not keep_mirror:
            if base.trans is not None:
                base.trans.mirror = False
            elif base.dcplx_trans is not None:
                base.dcplx_trans.mirror = False
        if name is not None:
            base.name = name
        self._bases.append(base)
        port_ = DPort(base=base)
    else:
        dcplx_trans = port.dcplx_trans.dup()
        if not keep_mirror:
            dcplx_trans.mirror = False
        base = port.base.model_copy()
        base.trans = kdb.Trans.R0
        base.dcplx_trans = None
        base.kcl = self.kcl
        base.cross_section = self.kcl.get_symmetrical_cross_section(
            port.cross_section.base.to_dtype(port.kcl)
        )
        port_ = DPort(base=base)
        port_.dcplx_trans = dcplx_trans
        self._bases.append(port_.base)
    return port_

copy

copy(
    rename_function: Callable[[Sequence[DPort]], None]
    | None = None,
) -> Self

Get a copy of each port.

Source code in kfactory/ports.py
866
867
868
869
870
871
872
873
def copy(
    self, rename_function: Callable[[Sequence[DPort]], None] | None = None
) -> Self:
    """Get a copy of each port."""
    bases = [b.__copy__() for b in self._bases]
    if rename_function is not None:
        rename_function([DPort(base=b) for b in bases])
    return self.__class__(bases=bases, kcl=self.kcl)

filter

filter(
    angle: Angle | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> list[DPort]

Filter ports by name.

Parameters:

Name Type Description Default
angle Angle | None

Filter by angle. 0, 1, 2, 3.

None
orientation float | None

Alias for angle.

None
layer LayerEnum | int | None

Filter by layer.

None
port_type str | None

Filter by port type.

None
regex str | None

Filter by regex of the name.

None
Source code in kfactory/ports.py
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
def filter(
    self,
    angle: Angle | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> list[DPort]:
    """Filter ports by name.

    Args:
        angle: Filter by angle. 0, 1, 2, 3.
        orientation: Alias for angle.
        layer: Filter by layer.
        port_type: Filter by port type.
        regex: Filter by regex of the name.
    """
    return _filter_ports(
        (DPort(base=b) for b in self._bases),
        angle,
        orientation,
        layer,
        port_type,
        regex,
    )

get_all_named

get_all_named() -> Mapping[str, DPort]

Get all ports in a dictionary with names as keys.

Source code in kfactory/ports.py
841
842
843
def get_all_named(self) -> Mapping[str, DPort]:
    """Get all ports in a dictionary with names as keys."""
    return {v.name: DPort(base=v) for v in self._bases if v.name is not None}

DSchema pydantic-model

Bases: DSchematic

Deprecated alias for DSchematic (will be removed in kfactory 2.0).

Fields:

Validators:

  • _validate_schematic
  • assign_backrefs
Source code in kfactory/schematic.py
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
class DSchema(DSchematic):
    """Deprecated alias for `DSchematic` (will be removed in kfactory 2.0)."""

    def __init__(self, **data: Any) -> None:
        logger.warning(
            "DSchema is deprecated, please use DSchematic. "
            "It will be removed in kfactory 2.0"
        )
        if "unit" in data:
            raise ValueError(
                "Cannot set the unit direct. It needs to be set by the class init."
            )
        super().__init__(**data)

DSchematic pydantic-model

Bases: TSchematic[um]

Schematic with a base unit of um for placements.

Fields:

Validators:

  • _validate_schematic
  • assign_backrefs
Source code in kfactory/schematic.py
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
class DSchematic(TSchematic[um]):
    """Schematic with a base unit of um for placements."""

    unit: Literal["um"] = "um"

    def __init__(self, **data: Any) -> None:
        if "unit" in data:
            raise ValueError(
                "Cannot set the unit direct. It needs to be set by the class init."
            )
        super().__init__(unit="um", **data)

Info pydantic-model

Bases: SettingMixin, BaseModel

Info for a KCell.

Validators:

Source code in kfactory/settings.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class Info(SettingMixin, BaseModel, extra="allow", validate_assignment=True):
    """Info for a KCell."""

    def __init__(self, **kwargs: Any) -> None:
        """Initialize the settings."""
        super().__init__(**kwargs)

    @model_validator(mode="before")
    @classmethod
    def restrict_types(cls, data: dict[str, MetaData]) -> dict[str, MetaData]:
        """Restrict the types of the settings."""
        for name, value in data.items():
            try:
                data[name] = check_metadata_type(value)
            except ValueError as e:
                raise ValueError(
                    "Values of the info dict only support int, float, string, "
                    "tuple, list, dict or None."
                    f"{name}: {value}, {type(value)}"
                ) from e

        return data

    def update(self, data: dict[str, MetaData]) -> None:
        """Update the settings."""
        for key, value in data.items():
            setattr(self, key, value)

    def __setitem__(self, key: str, value: MetaData) -> None:
        """Set the value of a setting."""
        setattr(self, key, value)

    def __iadd__(self, other: Info) -> Self:
        """Update the settings."""
        for key, value in other.model_dump().items():
            setattr(self, key, value)
        return self

    def __add__(self, other: Info) -> Self:
        """Update the settings."""
        return self.model_copy(update=other.model_dump())

__add__

__add__(other: Info) -> Self

Update the settings.

Source code in kfactory/settings.py
113
114
115
def __add__(self, other: Info) -> Self:
    """Update the settings."""
    return self.model_copy(update=other.model_dump())

__iadd__

__iadd__(other: Info) -> Self

Update the settings.

Source code in kfactory/settings.py
107
108
109
110
111
def __iadd__(self, other: Info) -> Self:
    """Update the settings."""
    for key, value in other.model_dump().items():
        setattr(self, key, value)
    return self

__init__

__init__(**kwargs: Any) -> None

Initialize the settings.

Source code in kfactory/settings.py
78
79
80
def __init__(self, **kwargs: Any) -> None:
    """Initialize the settings."""
    super().__init__(**kwargs)

__setitem__

__setitem__(key: str, value: MetaData) -> None

Set the value of a setting.

Source code in kfactory/settings.py
103
104
105
def __setitem__(self, key: str, value: MetaData) -> None:
    """Set the value of a setting."""
    setattr(self, key, value)

restrict_types pydantic-validator

restrict_types(
    data: dict[str, MetaData],
) -> dict[str, MetaData]

Restrict the types of the settings.

Source code in kfactory/settings.py
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@model_validator(mode="before")
@classmethod
def restrict_types(cls, data: dict[str, MetaData]) -> dict[str, MetaData]:
    """Restrict the types of the settings."""
    for name, value in data.items():
        try:
            data[name] = check_metadata_type(value)
        except ValueError as e:
            raise ValueError(
                "Values of the info dict only support int, float, string, "
                "tuple, list, dict or None."
                f"{name}: {value}, {type(value)}"
            ) from e

    return data

update

update(data: dict[str, MetaData]) -> None

Update the settings.

Source code in kfactory/settings.py
 98
 99
100
101
def update(self, data: dict[str, MetaData]) -> None:
    """Update the settings."""
    for key, value in data.items():
        setattr(self, key, value)

Instance

Bases: ProtoTInstance[int], DBUGeometricObject

An Instance of a KCell.

An Instance is a reference to a KCell with a transformation.

Attributes:

Name Type Description
_instance

The internal kdb.Instance reference

ports InstancePorts

Transformed ports of the KCell

kcl

Pointer to the layout object holding the instance

d

Helper that allows retrieval of instance information in um

Source code in kfactory/instance.py
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
class Instance(ProtoTInstance[int], DBUGeometricObject):
    """An Instance of a KCell.

    An Instance is a reference to a KCell with a transformation.

    Attributes:
        _instance: The internal `kdb.Instance` reference
        ports: Transformed ports of the KCell
        kcl: Pointer to the layout object holding the instance
        d: Helper that allows retrieval of instance information in um
    """

    yaml_tag: ClassVar[str] = "!Instance"

    def __init__(self, kcl: KCLayout, instance: kdb.Instance) -> None:
        """Create an instance from a KLayout Instance."""
        self.kcl = kcl
        self._instance = instance

    @functools.cached_property
    def ports(self) -> InstancePorts:
        """Gets the transformed ports of the KCell."""
        from .instance_ports import InstancePorts

        return InstancePorts(self)

    @functools.cached_property
    def pins(self) -> InstancePins:
        """Gets the transformed pins of the KCell."""
        from .instance_pins import InstancePins

        return InstancePins(self)

    def __getitem__(
        self, key: int | str | tuple[int | str | None, int, int] | None
    ) -> Port:
        """Returns port from instance.

        The key can either be an integer, in which case the nth port is
        returned, or a string in which case the first port with a matching
        name is returned.

        If the instance is an array, the key can also be a tuple in the
        form of `c.ports[key_name, i_a, i_b]`, where `i_a` is the index in
        the `instance.a` direction and `i_b` the `instance.b` direction.

        E.g. `c.ports["a", 3, 5]`, accesses the ports of the instance which is
        3 times in `a` direction (4th index in the array), and 5 times in `b` direction
        (5th index in the array).
        """
        return Port(base=self.ports[key].base)

    @property
    def parent_cell(self) -> KCell:
        """Gets the cell this instance is contained in."""
        return self.kcl[self._instance.parent_cell.cell_index()]

    @parent_cell.setter
    def parent_cell(self, cell: KCell | DKCell | kdb.Cell) -> None:
        if isinstance(cell, KCell | DKCell):
            self.parent_cell.insts.remove(self)
            self._instance.parent_cell = cell.kdb_cell
        else:
            self._instance.parent_cell = cell

    @property
    def cell(self) -> KCell:
        """Parent KCell of the Instance."""
        return self.kcl.kcells[self.cell_index]

    @cell.setter
    def cell(self, value: ProtoTKCell[Any]) -> None:
        self.cell_index = value.cell_index()

    @classmethod
    def to_yaml(cls, representer: BaseRepresenter, node: Self) -> MappingNode:
        """Convert the instance to a yaml representation."""
        d = {
            "cellname": node.cell.name,
            "trans": node._base.trans,
            "dcplx_trans": node._base.dcplx_trans,
        }
        return representer.represent_mapping(cls.yaml_tag, d)

cell property writable

cell: KCell

Parent KCell of the Instance.

kcl instance-attribute

kcl = kcl

KCLayout object.

parent_cell property writable

parent_cell: KCell

Gets the cell this instance is contained in.

pins cached property

pins: InstancePins

Gets the transformed pins of the KCell.

ports cached property

ports: InstancePorts

Gets the transformed ports of the KCell.

__getitem__

__getitem__(
    key: int
    | str
    | tuple[int | str | None, int, int]
    | None,
) -> Port

Returns port from instance.

The key can either be an integer, in which case the nth port is returned, or a string in which case the first port with a matching name is returned.

If the instance is an array, the key can also be a tuple in the form of c.ports[key_name, i_a, i_b], where i_a is the index in the instance.a direction and i_b the instance.b direction.

E.g. c.ports["a", 3, 5], accesses the ports of the instance which is 3 times in a direction (4th index in the array), and 5 times in b direction (5th index in the array).

Source code in kfactory/instance.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
def __getitem__(
    self, key: int | str | tuple[int | str | None, int, int] | None
) -> Port:
    """Returns port from instance.

    The key can either be an integer, in which case the nth port is
    returned, or a string in which case the first port with a matching
    name is returned.

    If the instance is an array, the key can also be a tuple in the
    form of `c.ports[key_name, i_a, i_b]`, where `i_a` is the index in
    the `instance.a` direction and `i_b` the `instance.b` direction.

    E.g. `c.ports["a", 3, 5]`, accesses the ports of the instance which is
    3 times in `a` direction (4th index in the array), and 5 times in `b` direction
    (5th index in the array).
    """
    return Port(base=self.ports[key].base)

__init__

__init__(kcl: KCLayout, instance: Instance) -> None

Create an instance from a KLayout Instance.

Source code in kfactory/instance.py
512
513
514
515
def __init__(self, kcl: KCLayout, instance: kdb.Instance) -> None:
    """Create an instance from a KLayout Instance."""
    self.kcl = kcl
    self._instance = instance

to_yaml classmethod

to_yaml(
    representer: BaseRepresenter, node: Self
) -> MappingNode

Convert the instance to a yaml representation.

Source code in kfactory/instance.py
572
573
574
575
576
577
578
579
580
@classmethod
def to_yaml(cls, representer: BaseRepresenter, node: Self) -> MappingNode:
    """Convert the instance to a yaml representation."""
    d = {
        "cellname": node.cell.name,
        "trans": node._base.trans,
        "dcplx_trans": node._base.dcplx_trans,
    }
    return representer.represent_mapping(cls.yaml_tag, d)

InstanceGroup

Bases: ProtoTInstanceGroup[int, Instance], DBUGeometricObject, ICreatePort

Group of Instances.

The instance group can be treated similar to a single instance with regards to transformation functions and bounding boxes.

Parameters:

Name Type Description Default
insts Sequence[TInstance_co] | None

List of the instances of the group.

None
Source code in kfactory/instance_group.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
class InstanceGroup(
    ProtoTInstanceGroup[int, Instance], DBUGeometricObject, ICreatePort
):
    """Group of Instances.

    The instance group can be treated similar to a single instance
    with regards to transformation functions and bounding boxes.

    Args:
        insts: List of the instances of the group.
    """

    @cached_property
    def ports(self) -> Ports:
        return Ports(kcl=self.kcl, bases=self._base_ports)

    def add_port(
        self,
        *,
        port: ProtoPort[Any],
        name: str | None = None,
        keep_mirror: bool = False,
    ) -> Port:
        return self.ports.add_port(port=port, name=name, keep_mirror=keep_mirror)

ports cached property

ports: Ports

Ports of the instance.

InstancePorts

Bases: ProtoTInstancePorts[int]

Source code in kfactory/instance_ports.py
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
class InstancePorts(ProtoTInstancePorts[int]):
    def __init__(self, instance: Instance) -> None:
        """Creates the virtual ports object.

        Args:
            instance: The related instance
        """
        self.instance = instance

    @property
    def cell_ports(self) -> Ports:
        return Ports(kcl=self.instance.cell.kcl, bases=self.instance.cell.ports.bases)

    def filter(
        self,
        angle: int | None = None,
        orientation: float | None = None,
        layer: LayerEnum | int | None = None,
        port_type: str | None = None,
        regex: str | None = None,
    ) -> Sequence[Port]:
        return [
            Port(base=p.base)
            for p in super().filter(angle, orientation, layer, port_type, regex)
        ]

    def __getitem__(
        self, key: int | str | tuple[int | str | None, int, int] | None
    ) -> Port:
        return Port(base=super().__getitem__(key).base)

    def __iter__(self) -> Iterator[Port]:
        yield from (p.to_itype() for p in self.each_port())

__getitem__

__getitem__(
    key: int
    | str
    | tuple[int | str | None, int, int]
    | None,
) -> Port

Returns port from instance.

The key can either be an integer, in which case the nth port is returned, or a string in which case the first port with a matching name is returned.

If the instance is an array, the key can also be a tuple in the form of c.ports[key_name, i_a, i_b], where i_a is the index in the instance.a direction and i_b the instance.b direction.

E.g. c.ports["a", 3, 5], accesses the ports of the instance which is 3 times in a direction (4th index in the array), and 5 times in b direction (5th index in the array).

Source code in kfactory/instance_ports.py
336
337
338
339
def __getitem__(
    self, key: int | str | tuple[int | str | None, int, int] | None
) -> Port:
    return Port(base=super().__getitem__(key).base)

__init__

__init__(instance: Instance) -> None

Creates the virtual ports object.

Parameters:

Name Type Description Default
instance Instance

The related instance

required
Source code in kfactory/instance_ports.py
311
312
313
314
315
316
317
def __init__(self, instance: Instance) -> None:
    """Creates the virtual ports object.

    Args:
        instance: The related instance
    """
    self.instance = instance

filter

filter(
    angle: int | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> Sequence[Port]

Filter ports by name.

Parameters:

Name Type Description Default
angle int | None

Filter by angle. 0, 1, 2, 3.

None
orientation float | None

Filter by orientation in degrees.

None
layer LayerEnum | int | None

Filter by layer.

None
port_type str | None

Filter by port type.

None
regex str | None

Filter by regex of the name.

None
Source code in kfactory/instance_ports.py
323
324
325
326
327
328
329
330
331
332
333
334
def filter(
    self,
    angle: int | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> Sequence[Port]:
    return [
        Port(base=p.base)
        for p in super().filter(angle, orientation, layer, port_type, regex)
    ]

Instances

Bases: ProtoTInstances[int]

Holder for instances.

Allows retrieval by name or index

Source code in kfactory/instances.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
class Instances(ProtoTInstances[int]):
    """Holder for instances.

    Allows retrieval by name or index
    """

    def __iter__(self) -> Iterator[Instance]:
        """Get instance iterator."""
        yield from (
            Instance(kcl=self._tkcell.kcl, instance=inst) for inst in self._insts
        )

    def __getitem__(self, key: str | int) -> Instance:
        """Retrieve instance by index or by name."""
        if isinstance(key, int):
            return Instance(kcl=self._tkcell.kcl, instance=list(self._insts)[key])
        return Instance(kcl=self._tkcell.kcl, instance=self._get_inst(key))

__getitem__

__getitem__(key: str | int) -> Instance

Retrieve instance by index or by name.

Source code in kfactory/instances.py
142
143
144
145
146
def __getitem__(self, key: str | int) -> Instance:
    """Retrieve instance by index or by name."""
    if isinstance(key, int):
        return Instance(kcl=self._tkcell.kcl, instance=list(self._insts)[key])
    return Instance(kcl=self._tkcell.kcl, instance=self._get_inst(key))

__iter__

__iter__() -> Iterator[Instance]

Get instance iterator.

Source code in kfactory/instances.py
136
137
138
139
140
def __iter__(self) -> Iterator[Instance]:
    """Get instance iterator."""
    yield from (
        Instance(kcl=self._tkcell.kcl, instance=inst) for inst in self._insts
    )

KCLayout pydantic-model

Bases: BaseModel

Small extension to the klayout.db.Layout.

It adds tracking for the KCell objects instead of only the klayout.db.Cell objects. Additionally it allows creation and registration through create_cell

All attributes of klayout.db.Layout are transparently accessible

Attributes:

Name Type Description
editable

Whether the layout should be opened in editable mode (default: True)

rename_function Callable[..., None]

function that takes an iterable object of ports and renames them

Fields:

  • name (str)
  • layout (Layout)
  • layer_enclosures (LayerEnclosureModel)
  • cross_sections (CrossSectionModel)
  • enclosure (KCellEnclosure)
  • library (Library)
  • factories (Factories[WrappedKCellFunc[Any, ProtoTKCell[Any]]])
  • virtual_factories (Factories[WrappedVKCellFunc[Any, VKCell]])
  • tkcells (dict[int, TKCell])
  • layers (type[LayerEnum])
  • infos (LayerInfos)
  • layer_stack (LayerStack)
  • netlist_layer_mapping (dict[LayerEnum | int, LayerEnum | int])
  • sparameters_path (Path | str | None)
  • interconnect_cml_path (Path | str | None)
  • constants (Constants)
  • rename_function (Callable[..., None])
  • _registered_functions (dict[int, Callable[..., TKCell]])
  • thread_lock (RLock)
  • info (Info)
  • settings (KCellSettings)
  • future_cell_name (str | None)
  • decorators (Decorators)
  • default_cell_output_type (type[KCell | DKCell])
  • default_vcell_output_type (type[VKCell])
  • connectivity (list[tuple[LayerInfo, LayerInfo] | tuple[LayerInfo, LayerInfo, LayerInfo]])
  • routing_strategies (dict[str, Callable[Concatenate[ProtoTKCell[Any], Sequence[ProtoPort[Any]], Sequence[ProtoPort[Any]], ...], list[ManhattanRoute]]])
  • technology_file (Path | None)

Validators:

  • _validate_layers
Source code in kfactory/layout.py
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
class KCLayout(
    BaseModel, arbitrary_types_allowed=True, extra="allow", validate_assignment=True
):
    """Small extension to the klayout.db.Layout.

    It adds tracking for the [KCell][kfactory.kcell.KCell] objects
    instead of only the `klayout.db.Cell` objects.
    Additionally it allows creation and registration through `create_cell`

    All attributes of `klayout.db.Layout` are transparently accessible

    Attributes:
        editable: Whether the layout should be opened in editable mode (default: True)
        rename_function: function that takes an iterable object of ports and renames
            them
    """

    """Store layers, enclosures, cell functions, simulation_settings ...

    only one Pdk can be active at a given time.

    Attributes:
        name: PDK name.
        enclosures: dict of enclosures factories.
        cells: dict of str mapping to KCells.
        cell_factories: dict of str mapping to cell factories.
        base_pdk: a pdk to copy from and extend.
        default_decorator: decorate all cells, if not otherwise defined on the cell.
        layers: maps name to gdslayer/datatype.
            Must be of type LayerEnum.
        layer_stack: maps name to layer numbers, thickness, zmin, sidewall_angle.
            if can also contain material properties
            (refractive index, nonlinear coefficient, sheet resistance ...).
        sparameters_path: to store Sparameters simulations.
        interconnect_cml_path: path to interconnect CML (optional).
        grid_size: in um. Defaults to 1nm.
        constants: dict of constants for the PDK.

    """
    name: str
    layout: kdb.Layout
    layer_enclosures: LayerEnclosureModel
    cross_sections: CrossSectionModel
    enclosure: KCellEnclosure
    library: kdb.Library

    factories: Factories[WrappedKCellFunc[Any, ProtoTKCell[Any]]]
    virtual_factories: Factories[WrappedVKCellFunc[Any, VKCell]]
    tkcells: dict[int, TKCell] = Field(default_factory=dict)
    layers: type[LayerEnum]
    infos: LayerInfos
    layer_stack: LayerStack
    netlist_layer_mapping: dict[LayerEnum | int, LayerEnum | int] = Field(
        default_factory=dict
    )
    sparameters_path: Path | str | None
    interconnect_cml_path: Path | str | None
    constants: Constants
    rename_function: Callable[..., None]
    _registered_functions: dict[int, Callable[..., TKCell]]
    thread_lock: RLock = Field(default_factory=RLock)

    info: Info = Field(default_factory=Info)
    settings: KCellSettings = Field(frozen=True)
    future_cell_name: str | None

    decorators: Decorators
    default_cell_output_type: type[KCell | DKCell] = KCell
    default_vcell_output_type: type[VKCell] = VKCell

    connectivity: list[
        tuple[kdb.LayerInfo, kdb.LayerInfo]
        | tuple[kdb.LayerInfo, kdb.LayerInfo, kdb.LayerInfo]
    ]

    routing_strategies: dict[
        str,
        Callable[
            Concatenate[
                ProtoTKCell[Any],
                Sequence[ProtoPort[Any]],
                Sequence[ProtoPort[Any]],
                ...,
            ],
            list[ManhattanRoute],
        ],
    ] = Field(default_factory=dict)
    technology_file: Path | None = None

    def __init__(
        self,
        name: str,
        layer_enclosures: dict[str, LayerEnclosure] | LayerEnclosureModel | None = None,
        enclosure: KCellEnclosure | None = None,
        infos: type[LayerInfos] | None = None,
        sparameters_path: Path | str | None = None,
        interconnect_cml_path: Path | str | None = None,
        layer_stack: LayerStack | None = None,
        constants: type[Constants] | None = None,
        base_kcl: KCLayout | None = None,
        port_rename_function: Callable[..., None] = rename_clockwise_multi,
        copy_base_kcl_layers: bool = True,
        info: dict[str, MetaData] | None = None,
        default_cell_output_type: type[KCell | DKCell] = KCell,
        connectivity: Sequence[
            tuple[kdb.LayerInfo, kdb.LayerInfo]
            | tuple[kdb.LayerInfo, kdb.LayerInfo, kdb.LayerInfo]
        ]
        | None = None,
        technology_file: Path | str | None = None,
    ) -> None:
        """Create a new KCLayout (PDK). Can be based on an old KCLayout.

        Args:
            name: Name of the PDK.
            layer_enclosures: Additional KCellEnclosures that should be available
                except the KCellEnclosure
            enclosure: The standard KCellEnclosure of the PDK.
            infos: A LayerInfos describing the layerstack of the PDK.
            sparameters_path: Path to the sparameters config file.
            interconnect_cml_path: Path to the interconnect file.
            layer_stack: maps name to layer numbers, thickness, zmin, sidewall_angle.
                if can also contain material properties
                (refractive index, nonlinear coefficient, sheet resistance ...).
            constants: A model containing all the constants related to the PDK.
            base_kcl: an optional basis of the PDK.
            port_rename_function: Which function to use for renaming kcell ports.
            copy_base_kcl_layers: Copy all known layers from the base if any are
                defined.
            info: Additional metadata to put into info attribute.
        """
        library = kdb.Library()
        layout = library.layout()
        layer_stack = layer_stack or LayerStack()
        constants_ = constants() if constants else Constants()
        infos_ = infos() if infos else LayerInfos()
        if layer_enclosures is not None:
            if isinstance(layer_enclosures, dict):
                layer_enclosures = LayerEnclosureModel(root=layer_enclosures)
        else:
            layer_enclosures = LayerEnclosureModel(root={})
        if technology_file:
            technology_file = Path(technology_file).resolve()
            if not technology_file.is_file():
                raise ValueError(
                    f"{technology_file=} is not an existing file."
                    " Make sure to link it to the .lyt file."
                )
        super().__init__(
            name=name,
            layer_enclosures=layer_enclosures,
            cross_sections=CrossSectionModel(kcl=self),
            enclosure=KCellEnclosure([]),
            infos=infos_,
            layers=LayerEnum,
            factories=Factories[WrappedKCellFunc[Any, ProtoTKCell[Any]]](),
            virtual_factories=Factories[WrappedVKCellFunc[Any, VKCell]](),
            sparameters_path=sparameters_path,
            interconnect_cml_path=interconnect_cml_path,
            constants=constants_,
            library=library,
            layer_stack=layer_stack,
            layout=layout,
            rename_function=port_rename_function,
            info=Info(**info) if info else Info(),
            future_cell_name=None,
            settings=KCellSettings(
                version=__version__,
                klayout_version=kdb.__version__,  # type: ignore[attr-defined]
                meta_format="v3",
            ),
            decorators=Decorators(self),
            default_cell_output_type=default_cell_output_type,
            connectivity=connectivity or [],
            technology_file=technology_file,
        )

        self.library.register(self.name)

        enclosure = KCellEnclosure(
            enclosures=[enc.model_copy() for enc in enclosure.enclosures.enclosures]
            if enclosure
            else []
        )
        self.sparameters_path = sparameters_path
        self.enclosure = enclosure
        self.interconnect_cml_path = interconnect_cml_path

        kcls[self.name] = self

    @model_validator(mode="before")
    @classmethod
    def _validate_layers(cls, data: dict[str, Any]) -> dict[str, Any]:
        data["layers"] = layerenum_from_dict(
            layers=data["infos"], layout=data["library"].layout()
        )
        data["library"].register(data["name"])
        return data

    @functools.cached_property
    def dkcells(self) -> DKCells:
        """DKCells is a mapping of int to DKCell."""
        return DKCells(self)

    @functools.cached_property
    def kcells(self) -> KCells:
        """KCells is a mapping of int to KCell."""
        return KCells(self)

    @property
    def dbu(self) -> float:
        """Get the database unit."""
        return self.layout.dbu

    def create_layer_enclosure(
        self,
        sections: Sequence[
            tuple[kdb.LayerInfo, int] | tuple[kdb.LayerInfo, int, int]
        ] = [],
        name: str | None = None,
        main_layer: kdb.LayerInfo | None = None,
        dsections: Sequence[
            tuple[kdb.LayerInfo, float] | tuple[kdb.LayerInfo, float, float]
        ]
        | None = None,
    ) -> LayerEnclosure:
        """Create a new LayerEnclosure in the KCLayout."""
        if name is None and main_layer is not None and main_layer.name != "":
            name = main_layer.name
        enc = LayerEnclosure(
            sections=sections,
            dsections=dsections,
            name=name,
            main_layer=main_layer,
            kcl=self,
        )

        self.layer_enclosures[enc.name] = enc
        return enc

    @cached_property
    def technology(self) -> kdb.Technology:
        if self.technology_file is not None:
            tech = kdb.Technology()
            tech.load(str(self.technology_file))
            kdb.Technology.register_technology(tech)
            return tech
        raise ValueError(f"{self.technology_file} is not a file or is None.")

    @overload
    def find_layer(self, name: str) -> LayerEnum: ...

    @overload
    def find_layer(self, info: kdb.LayerInfo) -> LayerEnum: ...

    @overload
    def find_layer(
        self,
        layer: int,
        datatype: int,
    ) -> LayerEnum: ...

    @overload
    def find_layer(
        self,
        layer: int,
        dataytpe: int,
        name: str,
    ) -> LayerEnum: ...

    @overload
    def find_layer(
        self, name: str, *, allow_undefined_layers: Literal[True] = True
    ) -> LayerEnum | int: ...

    @overload
    def find_layer(
        self, info: kdb.LayerInfo, *, allow_undefined_layers: Literal[True] = True
    ) -> LayerEnum | int: ...

    @overload
    def find_layer(
        self, layer: int, datatype: int, *, allow_undefined_layers: Literal[True] = True
    ) -> LayerEnum | int: ...

    @overload
    def find_layer(
        self,
        layer: int,
        dataytpe: int,
        name: str,
        allow_undefined_layers: Literal[True] = True,
    ) -> LayerEnum | int: ...

    def find_layer(
        self,
        *args: int | str | kdb.LayerInfo,
        **kwargs: int | str | kdb.LayerInfo | bool,
    ) -> LayerEnum | int:
        """Try to find a registered layer. Throws a KeyError if it cannot find it.

        Can find a layer either by name, layer and datatype (two args), LayerInfo, or
        all three of layer, datatype, and name.
        """
        allow_undefined_layers = kwargs.pop(
            "allow_undefined_layers", config.allow_undefined_layers
        )
        info = self.layout.get_info(self.layout.layer(*args, **kwargs))
        try:
            return self.layers[info.name]  # type:ignore[no-any-return, index]
        except KeyError as e:
            if allow_undefined_layers:
                return self.layout.layer(info)
            raise KeyError(
                f"Layer '{args=}, {kwargs=}' has not been defined in the KCLayout. "
                "Have you defined the layer and set it in KCLayout.info?"
            ) from e

    @overload
    def to_um(self, other: None) -> None: ...

    @overload
    def to_um(self, other: int) -> float: ...

    @overload
    def to_um(self, other: kdb.Point) -> kdb.DPoint: ...

    @overload
    def to_um(self, other: kdb.Vector) -> kdb.DVector: ...

    @overload
    def to_um(self, other: kdb.Box) -> kdb.DBox: ...

    @overload
    def to_um(self, other: kdb.Polygon) -> kdb.DPolygon: ...

    @overload
    def to_um(self, other: kdb.Path) -> kdb.DPath: ...

    @overload
    def to_um(self, other: kdb.Text) -> kdb.DText: ...

    def to_um(
        self,
        other: int
        | kdb.Point
        | kdb.Vector
        | kdb.Box
        | kdb.Polygon
        | kdb.Path
        | kdb.Text
        | None,
    ) -> (
        float
        | kdb.DPoint
        | kdb.DVector
        | kdb.DBox
        | kdb.DPolygon
        | kdb.DPath
        | kdb.DText
        | None
    ):
        """Convert Shapes or values in dbu to DShapes or floats in um."""
        if other is None:
            return None
        return kdb.CplxTrans(self.layout.dbu) * other

    @overload
    def to_dbu(self, other: None) -> None: ...
    @overload
    def to_dbu(self, other: float) -> int: ...

    @overload
    def to_dbu(self, other: kdb.DPoint) -> kdb.Point: ...

    @overload
    def to_dbu(self, other: kdb.DVector) -> kdb.Vector: ...

    @overload
    def to_dbu(self, other: kdb.DBox) -> kdb.Box: ...

    @overload
    def to_dbu(self, other: kdb.DPolygon) -> kdb.Polygon: ...

    @overload
    def to_dbu(self, other: kdb.DPath) -> kdb.Path: ...

    @overload
    def to_dbu(self, other: kdb.DText) -> kdb.Text: ...

    def to_dbu(
        self,
        other: float
        | kdb.DPoint
        | kdb.DVector
        | kdb.DBox
        | kdb.DPolygon
        | kdb.DPath
        | kdb.DText
        | None,
    ) -> (
        int
        | kdb.Point
        | kdb.Vector
        | kdb.Box
        | kdb.Polygon
        | kdb.Path
        | kdb.Text
        | None
    ):
        """Convert Shapes or values in dbu to DShapes or floats in um."""
        if other is None:
            return None
        return kdb.CplxTrans(self.layout.dbu).inverted() * other

    @overload
    def schematic_cell(
        self,
        _func: Callable[KCellParams, TSchematic[TUnit]],
        /,
    ) -> Callable[KCellParams, KCell]: ...

    @overload
    def schematic_cell(
        self,
        /,
        *,
        set_settings: bool = ...,
        set_name: bool = ...,
        check_ports: bool = ...,
        check_pins: bool = ...,
        check_instances: CheckInstances | None = ...,
        snap_ports: bool = ...,
        add_port_layers: bool = ...,
        cache: Cache[int, Any] | dict[int, Any] | None = ...,
        basename: str | None = ...,
        drop_params: list[str] = ...,
        register_factory: bool = ...,
        overwrite_existing: bool | None = ...,
        layout_cache: bool | None = ...,
        info: dict[str, MetaData] | None = ...,
        debug_names: bool | None = ...,
        tags: list[str] | None = ...,
        factories: Mapping[
            str, Callable[..., KCell] | Callable[..., DKCell] | Callable[..., VKCell]
        ]
        | None = None,
        cross_sections: Mapping[str, CrossSection | DCrossSection] | None = None,
        routing_strategies: dict[
            str,
            Callable[
                Concatenate[
                    ProtoTKCell[Any],
                    Sequence[ProtoPort[Any]],
                    Sequence[ProtoPort[Any]],
                    ...,
                ],
                Any,
            ],
        ]
        | None = None,
    ) -> Callable[
        [Callable[KCellParams, TSchematic[TUnit]]], Callable[KCellParams, KCell]
    ]: ...

    @overload
    def schematic_cell(
        self,
        /,
        *,
        set_settings: bool = ...,
        set_name: bool = ...,
        check_ports: bool = ...,
        check_pins: bool = ...,
        check_instances: CheckInstances | None = ...,
        snap_ports: bool = ...,
        add_port_layers: bool = ...,
        cache: Cache[int, Any] | dict[int, Any] | None = ...,
        basename: str | None = ...,
        drop_params: list[str] = ...,
        register_factory: bool = ...,
        overwrite_existing: bool | None = ...,
        layout_cache: bool | None = ...,
        info: dict[str, MetaData] | None = ...,
        post_process: Iterable[Callable[[KCell], None]],
        debug_names: bool | None = ...,
        tags: list[str] | None = ...,
        factories: Mapping[
            str, Callable[..., KCell] | Callable[..., DKCell] | Callable[..., VKCell]
        ]
        | None = None,
        cross_sections: Mapping[str, CrossSection | DCrossSection] | None = None,
        routing_strategies: dict[
            str,
            Callable[
                Concatenate[
                    ProtoTKCell[Any],
                    Sequence[ProtoPort[Any]],
                    Sequence[ProtoPort[Any]],
                    ...,
                ],
                Any,
            ],
        ]
        | None = None,
    ) -> Callable[
        [Callable[KCellParams, TSchematic[TUnit]]], Callable[KCellParams, KCell]
    ]: ...

    @overload
    def schematic_cell(
        self,
        /,
        *,
        output_type: type[KC],
        set_settings: bool = ...,
        set_name: bool = ...,
        check_ports: bool = ...,
        check_pins: bool = ...,
        check_instances: CheckInstances | None = ...,
        snap_ports: bool = ...,
        add_port_layers: bool = ...,
        cache: Cache[int, Any] | dict[int, Any] | None = ...,
        basename: str | None = ...,
        drop_params: list[str] = ...,
        register_factory: bool = ...,
        overwrite_existing: bool | None = ...,
        layout_cache: bool | None = ...,
        info: dict[str, MetaData] | None = ...,
        post_process: Iterable[Callable[[KCell], None]],
        debug_names: bool | None = ...,
        tags: list[str] | None = ...,
        factories: Mapping[
            str, Callable[..., KCell] | Callable[..., DKCell] | Callable[..., VKCell]
        ]
        | None = None,
        cross_sections: Mapping[str, CrossSection | DCrossSection] | None = None,
        routing_strategies: dict[
            str,
            Callable[
                Concatenate[
                    ProtoTKCell[Any],
                    Sequence[ProtoPort[Any]],
                    Sequence[ProtoPort[Any]],
                    ...,
                ],
                Any,
            ],
        ]
        | None = None,
    ) -> Callable[
        [Callable[KCellParams, TSchematic[TUnit]]], Callable[KCellParams, KC]
    ]: ...

    @overload
    def schematic_cell(
        self,
        /,
        *,
        output_type: type[KC],
        set_settings: bool = ...,
        set_name: bool = ...,
        check_ports: bool = ...,
        check_pins: bool = ...,
        check_instances: CheckInstances | None = ...,
        snap_ports: bool = ...,
        add_port_layers: bool = ...,
        cache: Cache[int, Any] | dict[int, Any] | None = ...,
        basename: str | None = ...,
        drop_params: list[str] = ...,
        register_factory: bool = ...,
        overwrite_existing: bool | None = ...,
        layout_cache: bool | None = ...,
        info: dict[str, MetaData] | None = ...,
        debug_names: bool | None = ...,
        tags: list[str] | None = ...,
        factories: Mapping[
            str, Callable[..., KCell] | Callable[..., DKCell] | Callable[..., VKCell]
        ]
        | None = None,
        cross_sections: Mapping[str, CrossSection | DCrossSection] | None = None,
        routing_strategies: dict[
            str,
            Callable[
                Concatenate[
                    ProtoTKCell[Any],
                    Sequence[ProtoPort[Any]],
                    Sequence[ProtoPort[Any]],
                    ...,
                ],
                Any,
            ],
        ]
        | None = None,
    ) -> Callable[
        [Callable[KCellParams, TSchematic[TUnit]]], Callable[KCellParams, KC]
    ]: ...

    def schematic_cell(
        self,
        _func: Callable[KCellParams, TSchematic[TUnit]] | None = None,
        /,
        *,
        output_type: type[KC] | None = None,
        set_settings: bool = True,
        set_name: bool = True,
        check_ports: bool = True,
        check_pins: bool = True,
        check_instances: CheckInstances | None = None,
        snap_ports: bool = True,
        add_port_layers: bool = True,
        cache: Cache[int, Any] | dict[int, Any] | None = None,
        basename: str | None = None,
        drop_params: Sequence[str] = ("self", "cls"),
        register_factory: bool = True,
        overwrite_existing: bool | None = None,
        layout_cache: bool | None = None,
        info: dict[str, MetaData] | None = None,
        post_process: Iterable[Callable[[KCell], None]] | None = None,
        debug_names: bool | None = None,
        tags: list[str] | None = None,
        factories: Mapping[
            str, Callable[..., KCell] | Callable[..., DKCell] | Callable[..., VKCell]
        ]
        | None = None,
        cross_sections: Mapping[str, CrossSection | DCrossSection] | None = None,
        routing_strategies: dict[
            str,
            Callable[
                Concatenate[
                    ProtoTKCell[Any],
                    Sequence[ProtoPort[Any]],
                    Sequence[ProtoPort[Any]],
                    ...,
                ],
                Any,
            ],
        ]
        | None = None,
    ) -> (
        Callable[KCellParams, KCell]
        | Callable[
            [Callable[KCellParams, TSchematic[Any]]],
            Callable[KCellParams, KC],
        ]
        | Callable[
            [Callable[KCellParams, TSchematic[Any]]],
            Callable[KCellParams, KCell],
        ]
    ):
        if _func is None:
            if output_type is None:

                def wrap_f(
                    f: Callable[KCellParams, TSchematic[TUnit]],
                ) -> Callable[KCellParams, KCell]:
                    @self.cell(
                        output_type=KCell,
                        set_settings=set_settings,
                        set_name=set_name,
                        check_ports=check_ports,
                        check_pins=check_pins,
                        check_instances=check_instances,
                        snap_ports=snap_ports,
                        add_port_layers=add_port_layers,
                        cache=cache,
                        basename=basename,
                        drop_params=list(drop_params),
                        register_factory=register_factory,
                        overwrite_existing=overwrite_existing,
                        layout_cache=layout_cache,
                        info=info,
                        post_process=post_process or [],
                        debug_names=debug_names,
                        tags=tags,
                    )
                    @functools.wraps(f)
                    def kcell_func(
                        *args: KCellParams.args, **kwargs: KCellParams.kwargs
                    ) -> KCell:
                        schematic = f(*args, **kwargs)
                        if set_name:
                            schematic.name = self.future_cell_name
                        c_ = schematic.create_cell(
                            KCell,
                            factories=factories,
                            cross_sections=cross_sections,
                            routing_strategies=routing_strategies,
                        )
                        c_.schematic = schematic
                        return c_

                    return kcell_func

                return wrap_f

            def custom_wrap_f(
                f: Callable[KCellParams, TSchematic[TUnit]],
            ) -> Callable[KCellParams, KC]:
                @self.cell(
                    output_type=output_type,
                    set_settings=set_settings,
                    set_name=set_name,
                    check_ports=check_ports,
                    check_pins=check_pins,
                    check_instances=check_instances,
                    snap_ports=snap_ports,
                    add_port_layers=add_port_layers,
                    cache=cache,
                    basename=basename,
                    drop_params=list(drop_params),
                    register_factory=register_factory,
                    overwrite_existing=overwrite_existing,
                    layout_cache=layout_cache,
                    info=info,
                    post_process=post_process or [],
                    debug_names=debug_names,
                    tags=tags,
                )
                @functools.wraps(f)
                def custom_kcell_func(
                    *args: KCellParams.args, **kwargs: KCellParams.kwargs
                ) -> KCell:
                    schematic = f(*args, **kwargs)
                    if set_name:
                        schematic.name = self.future_cell_name
                    c_ = schematic.create_cell(
                        KCell,
                        factories=factories,
                        cross_sections=cross_sections,
                        routing_strategies=routing_strategies,
                    )
                    c_.schematic = schematic
                    return c_

                return custom_kcell_func

            return custom_wrap_f

        def simple_wrap_f(
            f: Callable[KCellParams, TSchematic[TUnit]],
        ) -> Callable[KCellParams, KCell]:
            @functools.wraps(f)
            @self.cell(output_type=KCell)
            def kcell_func(
                *args: KCellParams.args, **kwargs: KCellParams.kwargs
            ) -> KCell:
                schematic = f(*args, **kwargs)
                if set_name:
                    schematic.name = self.future_cell_name
                c_ = schematic.create_cell(
                    KCell,
                    factories=factories,
                    cross_sections=cross_sections,
                    routing_strategies=routing_strategies,
                )
                c_.schematic = schematic
                return c_

            return kcell_func

        return simple_wrap_f(_func)

    @overload
    def cell(
        self,
        _func: Callable[KCellParams, KC],
        /,
    ) -> Callable[KCellParams, KC]: ...

    @overload
    def cell(
        self,
        /,
        *,
        set_settings: bool = ...,
        set_name: bool = ...,
        check_ports: bool = ...,
        check_pins: bool = ...,
        check_instances: CheckInstances | None = ...,
        snap_ports: bool = ...,
        add_port_layers: bool = ...,
        cache: Cache[int, Any] | dict[int, Any] | None = ...,
        basename: str | None = ...,
        drop_params: list[str] = ...,
        register_factory: bool = ...,
        overwrite_existing: bool | None = ...,
        layout_cache: bool | None = ...,
        info: dict[str, MetaData] | None = ...,
        debug_names: bool | None = ...,
        tags: list[str] | None = ...,
        lvs_equivalent_ports: list[list[str]] | None = None,
        ports: PortsDefinition | None = None,
    ) -> Callable[[Callable[KCellParams, KC]], Callable[KCellParams, KC]]: ...

    @overload
    def cell(
        self,
        /,
        *,
        set_settings: bool = ...,
        set_name: bool = ...,
        check_ports: bool = ...,
        check_pins: bool = ...,
        check_instances: CheckInstances | None = ...,
        snap_ports: bool = ...,
        add_port_layers: bool = ...,
        cache: Cache[int, Any] | dict[int, Any] | None = ...,
        basename: str | None = ...,
        drop_params: list[str] = ...,
        register_factory: bool = ...,
        overwrite_existing: bool | None = ...,
        layout_cache: bool | None = ...,
        info: dict[str, MetaData] | None = ...,
        post_process: Iterable[Callable[[KC_contra], None]],
        debug_names: bool | None = ...,
        tags: list[str] | None = ...,
        lvs_equivalent_ports: list[list[str]] | None = None,
        ports: PortsDefinition | None = None,
    ) -> Callable[[Callable[KCellParams, KC]], Callable[KCellParams, KC]]: ...

    @overload
    def cell(
        self,
        /,
        *,
        output_type: type[KC],
        set_settings: bool = ...,
        set_name: bool = ...,
        check_ports: bool = ...,
        check_pins: bool = ...,
        check_instances: CheckInstances | None = ...,
        snap_ports: bool = ...,
        add_port_layers: bool = ...,
        cache: Cache[int, Any] | dict[int, Any] | None = ...,
        basename: str | None = ...,
        drop_params: list[str] = ...,
        register_factory: bool = ...,
        overwrite_existing: bool | None = ...,
        layout_cache: bool | None = ...,
        info: dict[str, MetaData] | None = ...,
        post_process: Iterable[Callable[[KC_contra], None]],
        debug_names: bool | None = ...,
        tags: list[str] | None = ...,
        lvs_equivalent_ports: list[list[str]] | None = None,
        ports: PortsDefinition | None = None,
    ) -> Callable[
        [Callable[KCellParams, ProtoTKCell[Any]]], Callable[KCellParams, KC]
    ]: ...

    @overload
    def cell(
        self,
        /,
        *,
        output_type: type[KC],
        set_settings: bool = ...,
        set_name: bool = ...,
        check_ports: bool = ...,
        check_pins: bool = ...,
        check_instances: CheckInstances | None = ...,
        snap_ports: bool = ...,
        add_port_layers: bool = ...,
        cache: Cache[int, Any] | dict[int, Any] | None = ...,
        basename: str | None = ...,
        drop_params: list[str] = ...,
        register_factory: bool = ...,
        overwrite_existing: bool | None = ...,
        layout_cache: bool | None = ...,
        info: dict[str, MetaData] | None = ...,
        debug_names: bool | None = ...,
        tags: list[str] | None = ...,
        lvs_equivalent_ports: list[list[str]] | None = None,
        ports: PortsDefinition | None = None,
    ) -> Callable[
        [Callable[KCellParams, ProtoTKCell[Any]]], Callable[KCellParams, KC]
    ]: ...

    def cell(
        self,
        _func: Callable[KCellParams, ProtoTKCell[Any]] | None = None,
        /,
        *,
        output_type: type[KC] | None = None,
        set_settings: bool = True,
        set_name: bool = True,
        check_ports: bool = True,
        check_pins: bool = True,
        check_instances: CheckInstances | None = None,
        snap_ports: bool = True,
        add_port_layers: bool = True,
        cache: Cache[int, Any] | dict[int, Any] | None = None,
        basename: str | None = None,
        drop_params: Sequence[str] = ("self", "cls"),
        register_factory: bool = True,
        overwrite_existing: bool | None = None,
        layout_cache: bool | None = None,
        info: dict[str, MetaData] | None = None,
        post_process: Iterable[Callable[[KC_contra], None]] | None = None,
        debug_names: bool | None = None,
        tags: list[str] | None = None,
        lvs_equivalent_ports: list[list[str]] | None = None,
        ports: PortsDefinition | None = None,
    ) -> (
        Callable[KCellParams, KC]
        | Callable[
            [Callable[KCellParams, ProtoTKCell[Any]]],
            Callable[KCellParams, KC],
        ]
    ):
        """Decorator to cache and auto name the cell.

        This will use `functools.cache` to cache the function call.
        Additionally, if enabled this will set the name and from the args/kwargs of the
        function and also paste them into a settings dictionary of the
        [KCell][kfactory.kcell.KCell].

        Args:
            output_type: The type of the cell to return.
            set_settings: Copy the args & kwargs into the settings dictionary
            set_name: Auto create the name of the cell to the functionname plus a
                string created from the args/kwargs
            check_ports: Check uniqueness of port names.
            check_pins: Check uniqueness of pin names.
            check_instances: Check for any complex instances. A complex instance is a an
                instance that has a magnification != 1 or non-90° rotation.
                Depending on the setting, an error is raised, the cell is flattened,
                a VInstance is created instead of a regular instance, or they are
                ignored.
            snap_ports: Snap the centers of the ports onto the grid
                (only x/y, not angle).
            add_port_layers: Add special layers of `KCLayout.netlist_layer_mapping`
                to the ports if the port layer is in the mapping.
            cache: Provide a user defined cache instead of an internal one. This
                can be used for example to clear the cache.
                expensive if the cell is called often).
            basename: Overwrite the name normally inferred from the function or class
                name.
            drop_params: Drop these parameters before writing the
                [settings][kfactory.kcell.KCell.settings]
            register_factory: Register the resulting KCell-function to the
                `KCLayout.factories`
            layout_cache: If true, treat the layout like a cache, if a cell with the
                same name exists already, pick that one instead of using running the
                function. This only works if `set_name` is true. Can be globally
                configured through `config.cell_layout_cache`.
            overwrite_existing: If cells were created with the same name, delete other
                cells with the same name. Can be globally configured through
                `config.cell_overwrite_existing`.
            info: Additional metadata to put into info attribute.
            post_process: List of functions to call after the cell has been created.
            debug_names: Check on setting the name whether a cell with this name already
                exists.
            tags: Tag cell functions with user defined tags.
        Returns:
            A wrapped cell function which caches responses and modifies the cell
            according to settings.
        """
        if check_instances is None:
            check_instances = config.check_instances
        if overwrite_existing is None:
            overwrite_existing = config.cell_overwrite_existing
        if layout_cache is None:
            layout_cache = config.cell_layout_cache
        if debug_names is None:
            debug_names = config.debug_names
        if post_process is None:
            post_process = ()

        def decorator_autocell(
            f: Callable[KCellParams, KCIN],
        ) -> Callable[KCellParams, KC]:
            sig = inspect.signature(f)
            output_cell_type_: type[KC | ProtoTKCell[Any]]
            if output_type is not None:
                output_cell_type_ = output_type
            elif sig.return_annotation is not inspect.Signature.empty:
                output_cell_type_ = sig.return_annotation
            else:
                output_cell_type_ = self.default_cell_output_type

            output_cell_type__ = cast("type[KC]", output_cell_type_)

            cache_: Cache[int, KC] | dict[int, KC] = cache or Cache(
                maxsize=float("inf")
            )
            wrapper_autocell: WrappedKCellFunc[KCellParams, KC] = WrappedKCellFunc(
                kcl=self,
                f=f,
                sig=sig,
                output_type=output_cell_type__,
                cache=cache_,
                set_settings=set_settings,
                set_name=set_name,
                check_ports=check_ports,
                check_pins=check_pins,
                check_instances=check_instances,
                snap_ports=snap_ports,
                add_port_layers=add_port_layers,
                basename=basename,
                drop_params=drop_params,
                overwrite_existing=overwrite_existing,
                layout_cache=layout_cache,
                info=info,
                post_process=post_process,  # type: ignore[arg-type]
                debug_names=debug_names,
                lvs_equivalent_ports=lvs_equivalent_ports,
                ports=ports,
            )

            if register_factory:
                with self.thread_lock:
                    if wrapper_autocell.name is None:
                        raise ValueError(f"Function {f} has no name.")
                    self.factories.add(wrapper_autocell)  # type: ignore[arg-type]

            @functools.wraps(f)
            def func(*args: KCellParams.args, **kwargs: KCellParams.kwargs) -> KC:
                return wrapper_autocell(*args, **kwargs)

            return func

        return decorator_autocell if _func is None else decorator_autocell(_func)

    @overload
    def vcell(
        self,
        _func: Callable[KCellParams, VK],
        /,
    ) -> Callable[KCellParams, VK]: ...

    @overload
    def vcell(
        self,
        /,
        *,
        set_settings: bool = True,
        set_name: bool = True,
        add_port_layers: bool = True,
        cache: Cache[int, Any] | dict[int, Any] | None = None,
        basename: str | None = None,
        drop_params: Sequence[str] = ("self", "cls"),
        register_factory: bool = True,
        post_process: Iterable[Callable[[VKCell], None]],
        info: dict[str, MetaData] | None = None,
        check_ports: bool = True,
        check_pins: bool = True,
        tags: list[str] | None = None,
        lvs_equivalent_ports: list[list[str]] | None = None,
        ports: PortsDefinition | None = None,
    ) -> Callable[[Callable[KCellParams, VK]], Callable[KCellParams, VK]]: ...

    @overload
    def vcell(
        self,
        /,
        *,
        output_type: type[VK],
        set_settings: bool = True,
        set_name: bool = True,
        add_port_layers: bool = True,
        cache: Cache[int, Any] | dict[int, Any] | None = None,
        basename: str | None = None,
        drop_params: Sequence[str] = ("self", "cls"),
        register_factory: bool = True,
        post_process: Iterable[Callable[[VKCell], None]],
        info: dict[str, MetaData] | None = None,
        check_ports: bool = True,
        check_pins: bool = True,
        tags: list[str] | None = None,
        lvs_equivalent_ports: list[list[str]] | None = None,
        ports: PortsDefinition | None = None,
    ) -> Callable[[Callable[KCellParams, VKCell]], Callable[KCellParams, VK]]: ...

    def vcell(
        self,
        _func: Callable[KCellParams, VKCell] | None = None,
        /,
        *,
        output_type: type[VK] | None = None,
        set_settings: bool = True,
        set_name: bool = True,
        add_port_layers: bool = True,
        cache: Cache[int, Any] | dict[int, Any] | None = None,
        basename: str | None = None,
        drop_params: Sequence[str] = ("self", "cls"),
        register_factory: bool = True,
        post_process: Iterable[Callable[[VKCell], None]] | None = None,
        info: dict[str, MetaData] | None = None,
        check_ports: bool = True,
        check_pins: bool = True,
        tags: list[str] | None = None,
        lvs_equivalent_ports: list[list[str]] | None = None,
        ports: PortsDefinition | None = None,
    ) -> (
        Callable[KCellParams, VK]
        | Callable[[Callable[KCellParams, VK]], Callable[KCellParams, VK]]
    ):
        """Decorator to cache and auto name the cell.

        This will use `functools.cache` to cache the function call.
        Additionally, if enabled this will set the name and from the args/kwargs of the
        function and also paste them into a settings dictionary of the
        [KCell][kfactory.kcell.KCell].

        Args:
            set_settings: Copy the args & kwargs into the settings dictionary
            set_name: Auto create the name of the cell to the functionname plus a
                string created from the args/kwargs
            check_ports: Check uniqueness of port names.
            check_pins: Check uniqueness of pin names.
            add_port_layers: Add special layers of `KCLayout.netlist_layer_mapping`
                to the ports if the port layer is in the mapping.
            cache: Provide a user defined cache instead of an internal one. This
                can be used for example to clear the cache.
            basename: Overwrite the name normally inferred from the function or class
                name.
            drop_params: Drop these parameters before writing the
                [settings][kfactory.kcell.KCell.settings]
            register_factory: Register the resulting KCell-function to the
                `KCLayout.factories`
            info: Additional metadata to put into info attribute.
            post_process: List of functions to call after the cell has been created.
        Returns:
            A wrapped vcell function which caches responses and modifies the VKCell
            according to settings.
        """
        if post_process is None:
            post_process = ()

        def decorator_autocell(
            f: Callable[KCellParams, VKCell],
        ) -> Callable[KCellParams, VK]:
            sig = inspect.signature(f)
            output_cell_type_: type[VK | VKCell]
            if output_type is not None:
                output_cell_type_ = output_type
            elif sig.return_annotation is not inspect.Signature.empty:
                output_cell_type_ = sig.return_annotation
            else:
                output_cell_type_ = self.default_vcell_output_type

            output_cell_type__ = cast("type[VK]", output_cell_type_)
            # previously was a KCellCache, but dict should do for most case
            cache_: Cache[int, VK] | dict[int, VK] = cache or Cache(
                maxsize=float("inf")
            )

            wrapper_autocell = WrappedVKCellFunc(
                kcl=self,
                f=f,
                sig=sig,
                cache=cache_,
                set_settings=set_settings,
                set_name=set_name,
                add_port_layers=add_port_layers,
                basename=basename,
                drop_params=drop_params,
                post_process=post_process,
                output_type=output_cell_type__,
                info=info,
                check_ports=check_ports,
                check_pins=check_pins,
                lvs_equivalent_ports=lvs_equivalent_ports,
                ports=ports,
            )

            if register_factory:
                if wrapper_autocell.name is None:
                    raise ValueError(f"Function {f} has no name.")
                self.virtual_factories.add(wrapper_autocell)  # type: ignore[arg-type]

            @functools.wraps(f)
            def func(*args: KCellParams.args, **kwargs: KCellParams.kwargs) -> VK:
                return wrapper_autocell(*args, **kwargs)

            return func

        return decorator_autocell if _func is None else decorator_autocell(_func)

    def kcell(self, name: str | None = None, ports: Ports | None = None) -> KCell:
        """Create a new cell based ont he pdk's layout object."""
        return KCell(name=name, kcl=self, ports=ports)

    def dkcell(self, name: str | None = None, ports: DPorts | None = None) -> DKCell:
        """Create a new cell based ont he pdk's layout object."""
        return DKCell(name=name, kcl=self, ports=ports)

    def vkcell(self, name: str | None = None) -> VKCell:
        """Create a new cell based ont he pdk's layout object."""
        return VKCell(name=name, kcl=self)

    def set_layers_from_infos(self, name: str, layers: LayerInfos) -> type[LayerEnum]:
        """Create a new LAYER enum based on the pdk's kcl."""
        return layerenum_from_dict(name=name, layers=layers, layout=self.layout)

    def __getattr__(self, name: str) -> Any:
        """If KCLayout doesn't have an attribute, look in the KLayout Cell."""
        if name != "_name" and name not in self.__class__.model_fields:
            return self.layout.__getattribute__(name)
        return None

    def __setattr__(self, name: str, value: Any) -> None:
        """Use a custom setter to automatically set attributes.

        If the attribute is not in this object, set it on the
        Layout object.
        """
        if name in self.__class__.model_fields:
            super().__setattr__(name, value)
        elif hasattr(self.layout, name):
            self.layout.__setattr__(name, value)

    def layerenum_from_dict(
        self, name: str = "LAYER", *, layers: LayerInfos
    ) -> type[LayerEnum]:
        """Create a new [LayerEnum][kfactory.kcell.LayerEnum] from this KCLayout."""
        return layerenum_from_dict(layers=layers, name=name, layout=self.layout)

    def clear(self, keep_layers: bool = True) -> None:
        """Clear the Layout.

        If the layout is cleared, all the LayerEnums and
        """
        for c in self.layout.cells("*"):
            c.locked = False
        self.layout.clear()
        self.tkcells = {}

        if keep_layers:
            self.layers = self.layerenum_from_dict(layers=self.infos)
        else:
            self.layers = self.layerenum_from_dict(layers=LayerInfos())

    def dup(self, init_cells: bool = True) -> KCLayout:
        """Create a duplication of the `~KCLayout` object.

        Args:
            init_cells: initialize the all cells in the new KCLayout object

        Returns:
            Copy of itself
        """
        kcl = KCLayout(self.name + "_DUPLICATE")
        kcl.layout.assign(self.layout.dup())
        if init_cells:
            for i, kc in self.tkcells.items():
                kcl.tkcells[i] = kc.model_copy(
                    update={"kdb_cell": kc.kdb_cell, "kcl": kcl}
                )
        kcl.rename_function = self.rename_function
        return kcl

    def layout_cell(self, name: str | int) -> kdb.Cell | None:
        """Get a cell by name or index from the Layout object."""
        return self.layout.cell(name)

    @overload
    def cells(self, name: str) -> list[kdb.Cell]: ...

    @overload
    def cells(self) -> int: ...

    def cells(self, name: str | None = None) -> int | list[kdb.Cell]:
        if name is None:
            return self.layout.cells()
        return self.layout.cells(name)

    def create_cell(
        self,
        name: str,
        *args: str,
        allow_duplicate: bool = False,
    ) -> kdb.Cell:
        """Create a new cell in the library.

        This shouldn't be called manually.
        The constructor of KCell will call this method.

        Args:
            name: The (initial) name of the cell.
            allow_duplicate: Allow the creation of a cell with the same name which
                already is registered in the Layout.
                This will create a cell with the name `name` + `$1` or `2..n`
                increasing by the number of existing duplicates
            args: additional arguments passed to
                `klayout.db.Layout.create_cell`

        Returns:
            klayout.db.Cell: klayout.db.Cell object created in the Layout

        """
        with self.thread_lock:
            if allow_duplicate or (self.layout_cell(name) is None):
                return self.layout.create_cell(name, *args)
            raise ValueError(
                f"Cellname {name} already exists in the layout/KCLayout. "
                "Please make sure the cellname is"
                " unique or pass `allow_duplicate` when creating the library"
            )

    def delete_cell(self, cell: AnyTKCell | int) -> None:
        """Delete a cell in the kcl object."""
        with self.thread_lock:
            if isinstance(cell, int):
                self.layout.cell(cell).locked = False
                self.layout.delete_cell(cell)
                self.tkcells.pop(cell, None)
            else:
                ci = cell.cell_index()
                self.layout.cell(ci).locked = False
                self.layout.delete_cell(ci)
                self.tkcells.pop(ci, None)

    def delete_cell_rec(self, cell_index: int) -> None:
        """Deletes a KCell plus all subcells."""
        with self.thread_lock:
            self.layout.delete_cell_rec(cell_index)
            self.rebuild()

    def delete_cells(self, cell_index_list: Sequence[int]) -> None:
        """Delete a sequence of cell by indexes."""
        with self.thread_lock:
            for ci in cell_index_list:
                self.layout.cell(ci).locked = False
                self.tkcells.pop(ci, None)
            self.layout.delete_cells(cell_index_list)
            self.rebuild()

    def assign(self, layout: kdb.Layout) -> None:
        """Assign a new Layout object to the KCLayout object."""
        with self.thread_lock:
            self.layout.assign(layout)
            self.rebuild()

    def rebuild(self) -> None:
        """Rebuild the KCLayout based on the Layout object."""
        kcells2delete: list[int] = []
        with self.thread_lock:
            for ci, c in self.tkcells.items():
                if c.kdb_cell._destroyed():
                    kcells2delete.append(ci)

            for ci in kcells2delete:
                del self.tkcells[ci]

            for cell in self.cells("*"):
                if cell.cell_index() not in self.tkcells:
                    self.tkcells[cell.cell_index()] = self.get_cell(
                        cell.cell_index(), KCell
                    ).base

    def register_cell(self, kcell: AnyTKCell, allow_reregister: bool = False) -> None:
        """Register an existing cell in the KCLayout object.

        Args:
            kcell: KCell 56 be registered in the KCLayout
            allow_reregister: Overwrite the existing KCell registration with this one.
                Doesn't allow name duplication.
        """
        with self.thread_lock:
            if (kcell.cell_index() not in self.tkcells) or allow_reregister:
                self.tkcells[kcell.cell_index()] = kcell.base
            else:
                raise ValueError(
                    f"Cannot register {kcell} if it has been registered already"
                    " exists in the library"
                )

    def __getitem__(self, obj: str | int) -> KCell:
        """Retrieve a cell by name(str) or index(int).

        Attrs:
            obj: name of cell or cell_index
        """
        return self.get_cell(obj)

    def get_cell(
        self,
        obj: str | int,
        cell_type: type[KC] = KCell,  # type: ignore[assignment]
        error_search_limit: int | None = 10,
    ) -> KC:
        """Retrieve a cell by name(str) or index(int).

        Attrs:
            obj: name of cell or cell_index
            cell_type: type of cell to return
        """
        if isinstance(obj, int):
            # search by index
            try:
                return cell_type(base=self.tkcells[obj])
            except KeyError:
                kdb_c = self.layout_cell(obj)
                if kdb_c is None:
                    raise
                return cell_type(name=kdb_c.name, kcl=self, kdb_cell=kdb_c)
        # search by name/key
        kdb_c = self.layout_cell(obj)
        if kdb_c is not None:
            try:
                return cell_type(base=self.tkcells[kdb_c.cell_index()])
            except KeyError:
                c = cell_type(name=kdb_c.name, kcl=self, kdb_cell=kdb_c)
                c.get_meta_data()
                return c
        if error_search_limit:
            # limit the print of available cells
            # and throw closest names with fuzzy search
            from rapidfuzz import process

            closest_names = [
                result[0]
                for result in process.extract(
                    obj,
                    (cell.name for cell in self.kcells.values()),
                    limit=error_search_limit,
                )
            ]
            raise ValueError(
                f"Library doesn't have a KCell named {obj},"
                f" closest {error_search_limit} are: \n"
                f"{pformat(closest_names)}"
            )

        raise ValueError(
            f"Library doesn't have a KCell named {obj},"
            " available KCells are"
            f"{pformat(sorted([cell.name for cell in self.kcells.values()]))}"
        )

    def read(
        self,
        filename: str | Path,
        options: kdb.LoadLayoutOptions | None = None,
        register_cells: bool | None = None,
        test_merge: bool = True,
        update_kcl_meta_data: Literal["overwrite", "skip", "drop"] = "skip",
        meta_format: Literal["v1", "v2", "v3"] | None = None,
    ) -> kdb.LayerMap:
        """Read a GDS file into the existing Layout.

        Any existing meta info (KCell.info and KCell.settings) will be overwritten if
        a KCell already exists. Instead of overwriting the cells, they can also be
        loaded into new cells by using the corresponding cell_conflict_resolution.

        This will fail if any of the read cells try to load into a locked KCell.

        Layout meta infos are ignored from the loaded layout.

        Args:
            filename: Path of the GDS file.
            options: KLayout options to load from the GDS. Can determine how merge
                conflicts are handled for example. See
                https://www.klayout.de/doc-qt5/code/class_LoadLayoutOptions.html
            register_cells: If `True` create KCells for all cells in the GDS.
            test_merge: Check the layouts first whether they are compatible
                (no differences).
            update_kcl_meta_data: How to treat loaded KCLayout info.
                overwrite: overwrite existing info entries
                skip: keep existing info values
                drop: don't add any new info
            meta_format: How to read KCell metainfo from the gds. `v1` had stored port
                transformations as strings, never versions have them stored and loaded
                in their native KLayout formats.
        """
        if options is None:
            options = load_layout_options()
        with self.thread_lock:
            if meta_format is None:
                meta_format = config.meta_format
            if register_cells is None:
                register_cells = meta_format == config.meta_format
            layout_b = kdb.Layout()
            layout_b.read(str(filename), options)
            if (
                self.cells() > 0
                and test_merge
                and (
                    options.cell_conflict_resolution
                    != kdb.LoadLayoutOptions.CellConflictResolution.RenameCell
                )
            ):
                self.set_meta_data()
                for kcell in self.kcells.values():
                    kcell.set_meta_data()
                diff = MergeDiff(
                    layout_a=self.layout,
                    layout_b=layout_b,
                    name_a=self.name,
                    name_b=Path(filename).stem,
                )
                diff.compare()
                if diff.dbu_differs:
                    raise MergeError(
                        "Layouts' DBU differ. Check the log for more info."
                    )
                if diff.diff_xor.cells() > 0:
                    diff_kcl = KCLayout(self.name + "_XOR")
                    diff_kcl.layout.assign(diff.diff_xor)
                    show(diff_kcl)

                    err_msg = (
                        f"Layout {self.name} cannot merge with layout "
                        f"{Path(filename).stem} safely. See the error messages "
                        f"or check with KLayout."
                    )

                    if diff.layout_meta_diff:
                        yaml = ruamel.yaml.YAML(typ=["rt", "string"])
                        err_msg += (
                            "\nLayout Meta Diff:\n```\n"
                            + yaml.dumps(dict(diff.layout_meta_diff))
                            + "\n```"
                        )
                    if diff.cells_meta_diff:
                        yaml = ruamel.yaml.YAML(typ=["rt", "string"])
                        err_msg += (
                            "\nLayout Meta Diff:\n```\n"
                            + yaml.dumps(dict(diff.cells_meta_diff))
                            + "\n```"
                        )

                    raise MergeError(err_msg)

            cells = set(self.cells("*"))
            saveopts = save_layout_options()
            saveopts.gds2_max_cellname_length = (
                kdb.SaveLayoutOptions().gds2_max_cellname_length
            )
            binary_layout = layout_b.write_bytes(saveopts)
            locked_cells = [
                kdb_cell for kdb_cell in self.layout.each_cell() if kdb_cell.locked
            ]
            for kdb_cell in locked_cells:
                kdb_cell.locked = False
            lm = self.layout.read_bytes(binary_layout, options)
            for kdb_cell in locked_cells:
                kdb_cell.locked = True
            info, settings = self.get_meta_data()

            match update_kcl_meta_data:
                case "overwrite":
                    for k, v in info.items():
                        self.info[k] = v
                case "skip":
                    info_ = self.info.model_dump()

                    info.update(info_)
                    self.info = Info(**info)

                case "drop":
                    pass
                case _:
                    raise ValueError(
                        f"Unknown meta update strategy {update_kcl_meta_data=}"
                        ", available strategies are 'overwrite', 'skip', or 'drop'"
                    )
            meta_format = settings.get("meta_format") or config.meta_format
            load_cells = {
                cell
                for c in layout_b.cells("*")
                if (cell := self.layout_cell(c.name)) is not None
            }
            new_cells = load_cells - cells

            if register_cells:
                for c in sorted(new_cells, key=lambda _c: _c.hierarchy_levels()):
                    kc = KCell(kdb_cell=c, kcl=self)
                    kc.get_meta_data(
                        meta_format=meta_format,
                    )

            for c in load_cells & cells:
                kc = self.kcells[c.cell_index()]
                kc.get_meta_data(meta_format=meta_format)

            return lm

    def get_meta_data(self) -> tuple[dict[str, Any], dict[str, Any]]:
        """Read KCLayout meta info from the KLayout object."""
        settings: dict[str, Any] = {}
        info: dict[str, Any] = {}
        cross_sections: list[dict[str, Any]] = []
        for meta in self.layout.each_meta_info():
            if meta.name.startswith("kfactory:info"):
                info[meta.name.removeprefix("kfactory:info:")] = meta.value
            elif meta.name.startswith("kfactory:settings"):
                settings[meta.name.removeprefix("kfactory:settings:")] = meta.value
            elif meta.name.startswith("kfactory:layer_enclosure:"):
                self.get_enclosure(
                    LayerEnclosure(
                        **meta.value,
                    )
                )
            elif meta.name.startswith("kfactory:cross_section:"):
                cross_sections.append(
                    {
                        "name": meta.name.removeprefix("kfactory:cross_section:"),
                        **meta.value,
                    }
                )

        for cs in cross_sections:
            self.get_symmetrical_cross_section(
                SymmetricalCrossSection(
                    width=cs["width"],
                    enclosure=self.get_enclosure(cs["layer_enclosure"]),
                    name=cs["name"],
                )
            )

        return info, settings

    def set_meta_data(self) -> None:
        """Set the info/settings of the KCLayout."""
        for name, setting in self.settings.model_dump().items():
            self.add_meta_info(
                kdb.LayoutMetaInfo(f"kfactory:settings:{name}", setting, None, True)
            )
        for name, info in self.info.model_dump().items():
            self.add_meta_info(
                kdb.LayoutMetaInfo(f"kfactory:info:{name}", info, None, True)
            )
        for enclosure in self.layer_enclosures.root.values():
            self.add_meta_info(
                kdb.LayoutMetaInfo(
                    f"kfactory:layer_enclosure:{enclosure.name}",
                    enclosure.model_dump(),
                    None,
                    True,
                )
            )
        for cross_section in self.cross_sections.cross_sections.values():
            self.add_meta_info(
                kdb.LayoutMetaInfo(
                    f"kfactory:cross_section:{cross_section.name}",
                    {
                        "width": cross_section.width,
                        "layer_enclosure": cross_section.enclosure.name,
                    },
                    None,
                    True,
                )
            )

    def write(
        self,
        filename: str | Path,
        options: kdb.SaveLayoutOptions | None = None,
        set_meta_data: bool = True,
        convert_external_cells: bool = False,
        autoformat_from_file_extension: bool = True,
    ) -> None:
        """Write a GDS file into the existing Layout.

        Args:
            filename: Path of the GDS file.
            options: KLayout options to load from the GDS. Can determine how merge
                conflicts are handled for example. See
                https://www.klayout.de/doc-qt5/code/class_LoadLayoutOptions.html
            set_meta_data: Make sure all the cells have their metadata set
            convert_external_cells: Whether to make KCells not in this KCLayout to
            autoformat_from_file_extension: Set the format of the output file
                automatically from the file extension of `filename`. This is necessary
                for the options. If not set, this will default to `GDSII`.
        """
        if options is None:
            options = save_layout_options()
        if isinstance(filename, Path):
            filename = str(filename.resolve())
        for kc in list(self.kcells.values()):
            kc.insert_vinsts()
        match (set_meta_data, convert_external_cells):
            case (True, True):
                self.set_meta_data()
                for kcell in self.kcells.values():
                    if not kcell.destroyed():
                        kcell.set_meta_data()
                        if kcell.is_library_cell():
                            kcell.convert_to_static(recursive=True)
            case (True, False):
                self.set_meta_data()
                for kcell in self.kcells.values():
                    if not kcell.destroyed():
                        kcell.set_meta_data()
            case (False, True):
                for kcell in self.kcells.values():
                    if kcell.is_library_cell() and not kcell.destroyed():
                        kcell.convert_to_static(recursive=True)

        if autoformat_from_file_extension:
            options.set_format_from_filename(filename)

        return self.layout.write(filename, options)

    def top_kcells(self) -> list[KCell]:
        """Return the top KCells."""
        return [self[tc.cell_index()] for tc in self.top_cells()]

    def top_kcell(self) -> KCell:
        """Return the top KCell if there is a single one."""
        return self[self.top_cell().cell_index()]

    def clear_kcells(self) -> None:
        """Clears all cells in the Layout object."""
        for kc in self.kcells.values():
            kc.locked = False
        for tc in self.top_kcells():
            tc.kdb_cell.prune_cell()
        self.tkcells = {}

    def get_enclosure(
        self, enclosure: str | LayerEnclosure | LayerEnclosureSpec
    ) -> LayerEnclosure:
        """Gets a layer enclosure by name specification or the layerenclosure itself."""
        return self.layer_enclosures.get_enclosure(enclosure, self)

    def get_symmetrical_cross_section(
        self,
        cross_section: str
        | SymmetricalCrossSection
        | CrossSectionSpec
        | DCrossSectionSpec
        | DSymmetricalCrossSection,
    ) -> SymmetricalCrossSection:
        """Get a cross section by name or specification."""
        return self.cross_sections.get_cross_section(cross_section)

    def get_icross_section(
        self,
        cross_section: str
        | SymmetricalCrossSection
        | CrossSectionSpec
        | DCrossSectionSpec
        | DCrossSection
        | DSymmetricalCrossSection
        | CrossSection,
    ) -> CrossSection:
        """Get a cross section by name or specification."""
        return CrossSection(
            kcl=self, base=self.cross_sections.get_cross_section(cross_section)
        )

    def get_dcross_section(
        self,
        cross_section: str
        | SymmetricalCrossSection
        | CrossSectionSpec
        | DCrossSectionSpec
        | DSymmetricalCrossSection
        | CrossSection
        | DCrossSection,
    ) -> DCrossSection:
        """Get a cross section by name or specification."""
        return DCrossSection(
            kcl=self, base=self.cross_sections.get_cross_section(cross_section)
        )

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.name}, n={len(self.kcells)})"

    def delete(self) -> None:
        del kcls[self.name]
        self.library.delete()

    def routing_strategy(
        self,
        f: Callable[
            Concatenate[
                ProtoTKCell[Any],
                Sequence[ProtoPort[Any]],
                Sequence[ProtoPort[Any]],
                P,
            ],
            list[ManhattanRoute],
        ],
    ) -> Callable[
        Concatenate[
            ProtoTKCell[Any],
            Sequence[ProtoPort[Any]],
            Sequence[ProtoPort[Any]],
            P,
        ],
        list[ManhattanRoute],
    ]:
        self.routing_strategies[f.__name__] = f
        return f

dbu property

dbu: float

Get the database unit.

dkcells cached property

dkcells: DKCells

DKCells is a mapping of int to DKCell.

kcells cached property

kcells: KCells

KCells is a mapping of int to KCell.

__getattr__

__getattr__(name: str) -> Any

If KCLayout doesn't have an attribute, look in the KLayout Cell.

Source code in kfactory/layout.py
1387
1388
1389
1390
1391
def __getattr__(self, name: str) -> Any:
    """If KCLayout doesn't have an attribute, look in the KLayout Cell."""
    if name != "_name" and name not in self.__class__.model_fields:
        return self.layout.__getattribute__(name)
    return None

__getitem__

__getitem__(obj: str | int) -> KCell

Retrieve a cell by name(str) or index(int).

Attrs

obj: name of cell or cell_index

Source code in kfactory/layout.py
1560
1561
1562
1563
1564
1565
1566
def __getitem__(self, obj: str | int) -> KCell:
    """Retrieve a cell by name(str) or index(int).

    Attrs:
        obj: name of cell or cell_index
    """
    return self.get_cell(obj)

__init__

__init__(
    name: str,
    layer_enclosures: dict[str, LayerEnclosure]
    | LayerEnclosureModel
    | None = None,
    enclosure: KCellEnclosure | None = None,
    infos: type[LayerInfos] | None = None,
    sparameters_path: Path | str | None = None,
    interconnect_cml_path: Path | str | None = None,
    layer_stack: LayerStack | None = None,
    constants: type[Constants] | None = None,
    base_kcl: KCLayout | None = None,
    port_rename_function: Callable[
        ..., None
    ] = rename_clockwise_multi,
    copy_base_kcl_layers: bool = True,
    info: dict[str, MetaData] | None = None,
    default_cell_output_type: type[KCell | DKCell] = KCell,
    connectivity: Sequence[
        tuple[LayerInfo, LayerInfo]
        | tuple[LayerInfo, LayerInfo, LayerInfo]
    ]
    | None = None,
    technology_file: Path | str | None = None,
) -> None

Create a new KCLayout (PDK). Can be based on an old KCLayout.

Parameters:

Name Type Description Default
name str

Name of the PDK.

required
layer_enclosures dict[str, LayerEnclosure] | LayerEnclosureModel | None

Additional KCellEnclosures that should be available except the KCellEnclosure

None
enclosure KCellEnclosure | None

The standard KCellEnclosure of the PDK.

None
infos type[LayerInfos] | None

A LayerInfos describing the layerstack of the PDK.

None
sparameters_path Path | str | None

Path to the sparameters config file.

None
interconnect_cml_path Path | str | None

Path to the interconnect file.

None
layer_stack LayerStack | None

maps name to layer numbers, thickness, zmin, sidewall_angle. if can also contain material properties (refractive index, nonlinear coefficient, sheet resistance ...).

None
constants type[Constants] | None

A model containing all the constants related to the PDK.

None
base_kcl KCLayout | None

an optional basis of the PDK.

None
port_rename_function Callable[..., None]

Which function to use for renaming kcell ports.

rename_clockwise_multi
copy_base_kcl_layers bool

Copy all known layers from the base if any are defined.

True
info dict[str, MetaData] | None

Additional metadata to put into info attribute.

None
Source code in kfactory/layout.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
def __init__(
    self,
    name: str,
    layer_enclosures: dict[str, LayerEnclosure] | LayerEnclosureModel | None = None,
    enclosure: KCellEnclosure | None = None,
    infos: type[LayerInfos] | None = None,
    sparameters_path: Path | str | None = None,
    interconnect_cml_path: Path | str | None = None,
    layer_stack: LayerStack | None = None,
    constants: type[Constants] | None = None,
    base_kcl: KCLayout | None = None,
    port_rename_function: Callable[..., None] = rename_clockwise_multi,
    copy_base_kcl_layers: bool = True,
    info: dict[str, MetaData] | None = None,
    default_cell_output_type: type[KCell | DKCell] = KCell,
    connectivity: Sequence[
        tuple[kdb.LayerInfo, kdb.LayerInfo]
        | tuple[kdb.LayerInfo, kdb.LayerInfo, kdb.LayerInfo]
    ]
    | None = None,
    technology_file: Path | str | None = None,
) -> None:
    """Create a new KCLayout (PDK). Can be based on an old KCLayout.

    Args:
        name: Name of the PDK.
        layer_enclosures: Additional KCellEnclosures that should be available
            except the KCellEnclosure
        enclosure: The standard KCellEnclosure of the PDK.
        infos: A LayerInfos describing the layerstack of the PDK.
        sparameters_path: Path to the sparameters config file.
        interconnect_cml_path: Path to the interconnect file.
        layer_stack: maps name to layer numbers, thickness, zmin, sidewall_angle.
            if can also contain material properties
            (refractive index, nonlinear coefficient, sheet resistance ...).
        constants: A model containing all the constants related to the PDK.
        base_kcl: an optional basis of the PDK.
        port_rename_function: Which function to use for renaming kcell ports.
        copy_base_kcl_layers: Copy all known layers from the base if any are
            defined.
        info: Additional metadata to put into info attribute.
    """
    library = kdb.Library()
    layout = library.layout()
    layer_stack = layer_stack or LayerStack()
    constants_ = constants() if constants else Constants()
    infos_ = infos() if infos else LayerInfos()
    if layer_enclosures is not None:
        if isinstance(layer_enclosures, dict):
            layer_enclosures = LayerEnclosureModel(root=layer_enclosures)
    else:
        layer_enclosures = LayerEnclosureModel(root={})
    if technology_file:
        technology_file = Path(technology_file).resolve()
        if not technology_file.is_file():
            raise ValueError(
                f"{technology_file=} is not an existing file."
                " Make sure to link it to the .lyt file."
            )
    super().__init__(
        name=name,
        layer_enclosures=layer_enclosures,
        cross_sections=CrossSectionModel(kcl=self),
        enclosure=KCellEnclosure([]),
        infos=infos_,
        layers=LayerEnum,
        factories=Factories[WrappedKCellFunc[Any, ProtoTKCell[Any]]](),
        virtual_factories=Factories[WrappedVKCellFunc[Any, VKCell]](),
        sparameters_path=sparameters_path,
        interconnect_cml_path=interconnect_cml_path,
        constants=constants_,
        library=library,
        layer_stack=layer_stack,
        layout=layout,
        rename_function=port_rename_function,
        info=Info(**info) if info else Info(),
        future_cell_name=None,
        settings=KCellSettings(
            version=__version__,
            klayout_version=kdb.__version__,  # type: ignore[attr-defined]
            meta_format="v3",
        ),
        decorators=Decorators(self),
        default_cell_output_type=default_cell_output_type,
        connectivity=connectivity or [],
        technology_file=technology_file,
    )

    self.library.register(self.name)

    enclosure = KCellEnclosure(
        enclosures=[enc.model_copy() for enc in enclosure.enclosures.enclosures]
        if enclosure
        else []
    )
    self.sparameters_path = sparameters_path
    self.enclosure = enclosure
    self.interconnect_cml_path = interconnect_cml_path

    kcls[self.name] = self

__setattr__

__setattr__(name: str, value: Any) -> None

Use a custom setter to automatically set attributes.

If the attribute is not in this object, set it on the Layout object.

Source code in kfactory/layout.py
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
def __setattr__(self, name: str, value: Any) -> None:
    """Use a custom setter to automatically set attributes.

    If the attribute is not in this object, set it on the
    Layout object.
    """
    if name in self.__class__.model_fields:
        super().__setattr__(name, value)
    elif hasattr(self.layout, name):
        self.layout.__setattr__(name, value)

assign

assign(layout: Layout) -> None

Assign a new Layout object to the KCLayout object.

Source code in kfactory/layout.py
1520
1521
1522
1523
1524
def assign(self, layout: kdb.Layout) -> None:
    """Assign a new Layout object to the KCLayout object."""
    with self.thread_lock:
        self.layout.assign(layout)
        self.rebuild()

cell

cell(
    _func: Callable[KCellParams, KC],
) -> Callable[KCellParams, KC]
cell(
    *,
    set_settings: bool = ...,
    set_name: bool = ...,
    check_ports: bool = ...,
    check_pins: bool = ...,
    check_instances: CheckInstances | None = ...,
    snap_ports: bool = ...,
    add_port_layers: bool = ...,
    cache: Cache[int, Any] | dict[int, Any] | None = ...,
    basename: str | None = ...,
    drop_params: list[str] = ...,
    register_factory: bool = ...,
    overwrite_existing: bool | None = ...,
    layout_cache: bool | None = ...,
    info: dict[str, MetaData] | None = ...,
    debug_names: bool | None = ...,
    tags: list[str] | None = ...,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> Callable[
    [Callable[KCellParams, KC]], Callable[KCellParams, KC]
]
cell(
    *,
    set_settings: bool = ...,
    set_name: bool = ...,
    check_ports: bool = ...,
    check_pins: bool = ...,
    check_instances: CheckInstances | None = ...,
    snap_ports: bool = ...,
    add_port_layers: bool = ...,
    cache: Cache[int, Any] | dict[int, Any] | None = ...,
    basename: str | None = ...,
    drop_params: list[str] = ...,
    register_factory: bool = ...,
    overwrite_existing: bool | None = ...,
    layout_cache: bool | None = ...,
    info: dict[str, MetaData] | None = ...,
    post_process: Iterable[Callable[[KC_contra], None]],
    debug_names: bool | None = ...,
    tags: list[str] | None = ...,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> Callable[
    [Callable[KCellParams, KC]], Callable[KCellParams, KC]
]
cell(
    *,
    output_type: type[KC],
    set_settings: bool = ...,
    set_name: bool = ...,
    check_ports: bool = ...,
    check_pins: bool = ...,
    check_instances: CheckInstances | None = ...,
    snap_ports: bool = ...,
    add_port_layers: bool = ...,
    cache: Cache[int, Any] | dict[int, Any] | None = ...,
    basename: str | None = ...,
    drop_params: list[str] = ...,
    register_factory: bool = ...,
    overwrite_existing: bool | None = ...,
    layout_cache: bool | None = ...,
    info: dict[str, MetaData] | None = ...,
    post_process: Iterable[Callable[[KC_contra], None]],
    debug_names: bool | None = ...,
    tags: list[str] | None = ...,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> Callable[
    [Callable[KCellParams, ProtoTKCell[Any]]],
    Callable[KCellParams, KC],
]
cell(
    *,
    output_type: type[KC],
    set_settings: bool = ...,
    set_name: bool = ...,
    check_ports: bool = ...,
    check_pins: bool = ...,
    check_instances: CheckInstances | None = ...,
    snap_ports: bool = ...,
    add_port_layers: bool = ...,
    cache: Cache[int, Any] | dict[int, Any] | None = ...,
    basename: str | None = ...,
    drop_params: list[str] = ...,
    register_factory: bool = ...,
    overwrite_existing: bool | None = ...,
    layout_cache: bool | None = ...,
    info: dict[str, MetaData] | None = ...,
    debug_names: bool | None = ...,
    tags: list[str] | None = ...,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> Callable[
    [Callable[KCellParams, ProtoTKCell[Any]]],
    Callable[KCellParams, KC],
]
cell(
    _func: Callable[KCellParams, ProtoTKCell[Any]]
    | None = None,
    /,
    *,
    output_type: type[KC] | None = None,
    set_settings: bool = True,
    set_name: bool = True,
    check_ports: bool = True,
    check_pins: bool = True,
    check_instances: CheckInstances | None = None,
    snap_ports: bool = True,
    add_port_layers: bool = True,
    cache: Cache[int, Any] | dict[int, Any] | None = None,
    basename: str | None = None,
    drop_params: Sequence[str] = ("self", "cls"),
    register_factory: bool = True,
    overwrite_existing: bool | None = None,
    layout_cache: bool | None = None,
    info: dict[str, MetaData] | None = None,
    post_process: Iterable[Callable[[KC_contra], None]]
    | None = None,
    debug_names: bool | None = None,
    tags: list[str] | None = None,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> (
    Callable[KCellParams, KC]
    | Callable[
        [Callable[KCellParams, ProtoTKCell[Any]]],
        Callable[KCellParams, KC],
    ]
)

Decorator to cache and auto name the cell.

This will use functools.cache to cache the function call. Additionally, if enabled this will set the name and from the args/kwargs of the function and also paste them into a settings dictionary of the KCell.

Parameters:

Name Type Description Default
output_type type[KC] | None

The type of the cell to return.

None
set_settings bool

Copy the args & kwargs into the settings dictionary

True
set_name bool

Auto create the name of the cell to the functionname plus a string created from the args/kwargs

True
check_ports bool

Check uniqueness of port names.

True
check_pins bool

Check uniqueness of pin names.

True
check_instances CheckInstances | None

Check for any complex instances. A complex instance is a an instance that has a magnification != 1 or non-90° rotation. Depending on the setting, an error is raised, the cell is flattened, a VInstance is created instead of a regular instance, or they are ignored.

None
snap_ports bool

Snap the centers of the ports onto the grid (only x/y, not angle).

True
add_port_layers bool

Add special layers of KCLayout.netlist_layer_mapping to the ports if the port layer is in the mapping.

True
cache Cache[int, Any] | dict[int, Any] | None

Provide a user defined cache instead of an internal one. This can be used for example to clear the cache. expensive if the cell is called often).

None
basename str | None

Overwrite the name normally inferred from the function or class name.

None
drop_params Sequence[str]

Drop these parameters before writing the settings

('self', 'cls')
register_factory bool

Register the resulting KCell-function to the KCLayout.factories

True
layout_cache bool | None

If true, treat the layout like a cache, if a cell with the same name exists already, pick that one instead of using running the function. This only works if set_name is true. Can be globally configured through config.cell_layout_cache.

None
overwrite_existing bool | None

If cells were created with the same name, delete other cells with the same name. Can be globally configured through config.cell_overwrite_existing.

None
info dict[str, MetaData] | None

Additional metadata to put into info attribute.

None
post_process Iterable[Callable[[KC_contra], None]] | None

List of functions to call after the cell has been created.

None
debug_names bool | None

Check on setting the name whether a cell with this name already exists.

None
tags list[str] | None

Tag cell functions with user defined tags.

None

Returns: A wrapped cell function which caches responses and modifies the cell according to settings.

Source code in kfactory/layout.py
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
def cell(
    self,
    _func: Callable[KCellParams, ProtoTKCell[Any]] | None = None,
    /,
    *,
    output_type: type[KC] | None = None,
    set_settings: bool = True,
    set_name: bool = True,
    check_ports: bool = True,
    check_pins: bool = True,
    check_instances: CheckInstances | None = None,
    snap_ports: bool = True,
    add_port_layers: bool = True,
    cache: Cache[int, Any] | dict[int, Any] | None = None,
    basename: str | None = None,
    drop_params: Sequence[str] = ("self", "cls"),
    register_factory: bool = True,
    overwrite_existing: bool | None = None,
    layout_cache: bool | None = None,
    info: dict[str, MetaData] | None = None,
    post_process: Iterable[Callable[[KC_contra], None]] | None = None,
    debug_names: bool | None = None,
    tags: list[str] | None = None,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> (
    Callable[KCellParams, KC]
    | Callable[
        [Callable[KCellParams, ProtoTKCell[Any]]],
        Callable[KCellParams, KC],
    ]
):
    """Decorator to cache and auto name the cell.

    This will use `functools.cache` to cache the function call.
    Additionally, if enabled this will set the name and from the args/kwargs of the
    function and also paste them into a settings dictionary of the
    [KCell][kfactory.kcell.KCell].

    Args:
        output_type: The type of the cell to return.
        set_settings: Copy the args & kwargs into the settings dictionary
        set_name: Auto create the name of the cell to the functionname plus a
            string created from the args/kwargs
        check_ports: Check uniqueness of port names.
        check_pins: Check uniqueness of pin names.
        check_instances: Check for any complex instances. A complex instance is a an
            instance that has a magnification != 1 or non-90° rotation.
            Depending on the setting, an error is raised, the cell is flattened,
            a VInstance is created instead of a regular instance, or they are
            ignored.
        snap_ports: Snap the centers of the ports onto the grid
            (only x/y, not angle).
        add_port_layers: Add special layers of `KCLayout.netlist_layer_mapping`
            to the ports if the port layer is in the mapping.
        cache: Provide a user defined cache instead of an internal one. This
            can be used for example to clear the cache.
            expensive if the cell is called often).
        basename: Overwrite the name normally inferred from the function or class
            name.
        drop_params: Drop these parameters before writing the
            [settings][kfactory.kcell.KCell.settings]
        register_factory: Register the resulting KCell-function to the
            `KCLayout.factories`
        layout_cache: If true, treat the layout like a cache, if a cell with the
            same name exists already, pick that one instead of using running the
            function. This only works if `set_name` is true. Can be globally
            configured through `config.cell_layout_cache`.
        overwrite_existing: If cells were created with the same name, delete other
            cells with the same name. Can be globally configured through
            `config.cell_overwrite_existing`.
        info: Additional metadata to put into info attribute.
        post_process: List of functions to call after the cell has been created.
        debug_names: Check on setting the name whether a cell with this name already
            exists.
        tags: Tag cell functions with user defined tags.
    Returns:
        A wrapped cell function which caches responses and modifies the cell
        according to settings.
    """
    if check_instances is None:
        check_instances = config.check_instances
    if overwrite_existing is None:
        overwrite_existing = config.cell_overwrite_existing
    if layout_cache is None:
        layout_cache = config.cell_layout_cache
    if debug_names is None:
        debug_names = config.debug_names
    if post_process is None:
        post_process = ()

    def decorator_autocell(
        f: Callable[KCellParams, KCIN],
    ) -> Callable[KCellParams, KC]:
        sig = inspect.signature(f)
        output_cell_type_: type[KC | ProtoTKCell[Any]]
        if output_type is not None:
            output_cell_type_ = output_type
        elif sig.return_annotation is not inspect.Signature.empty:
            output_cell_type_ = sig.return_annotation
        else:
            output_cell_type_ = self.default_cell_output_type

        output_cell_type__ = cast("type[KC]", output_cell_type_)

        cache_: Cache[int, KC] | dict[int, KC] = cache or Cache(
            maxsize=float("inf")
        )
        wrapper_autocell: WrappedKCellFunc[KCellParams, KC] = WrappedKCellFunc(
            kcl=self,
            f=f,
            sig=sig,
            output_type=output_cell_type__,
            cache=cache_,
            set_settings=set_settings,
            set_name=set_name,
            check_ports=check_ports,
            check_pins=check_pins,
            check_instances=check_instances,
            snap_ports=snap_ports,
            add_port_layers=add_port_layers,
            basename=basename,
            drop_params=drop_params,
            overwrite_existing=overwrite_existing,
            layout_cache=layout_cache,
            info=info,
            post_process=post_process,  # type: ignore[arg-type]
            debug_names=debug_names,
            lvs_equivalent_ports=lvs_equivalent_ports,
            ports=ports,
        )

        if register_factory:
            with self.thread_lock:
                if wrapper_autocell.name is None:
                    raise ValueError(f"Function {f} has no name.")
                self.factories.add(wrapper_autocell)  # type: ignore[arg-type]

        @functools.wraps(f)
        def func(*args: KCellParams.args, **kwargs: KCellParams.kwargs) -> KC:
            return wrapper_autocell(*args, **kwargs)

        return func

    return decorator_autocell if _func is None else decorator_autocell(_func)

clear

clear(keep_layers: bool = True) -> None

Clear the Layout.

If the layout is cleared, all the LayerEnums and

Source code in kfactory/layout.py
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
def clear(self, keep_layers: bool = True) -> None:
    """Clear the Layout.

    If the layout is cleared, all the LayerEnums and
    """
    for c in self.layout.cells("*"):
        c.locked = False
    self.layout.clear()
    self.tkcells = {}

    if keep_layers:
        self.layers = self.layerenum_from_dict(layers=self.infos)
    else:
        self.layers = self.layerenum_from_dict(layers=LayerInfos())

clear_kcells

clear_kcells() -> None

Clears all cells in the Layout object.

Source code in kfactory/layout.py
1896
1897
1898
1899
1900
1901
1902
def clear_kcells(self) -> None:
    """Clears all cells in the Layout object."""
    for kc in self.kcells.values():
        kc.locked = False
    for tc in self.top_kcells():
        tc.kdb_cell.prune_cell()
    self.tkcells = {}

create_cell

create_cell(
    name: str, *args: str, allow_duplicate: bool = False
) -> kdb.Cell

Create a new cell in the library.

This shouldn't be called manually. The constructor of KCell will call this method.

Parameters:

Name Type Description Default
name str

The (initial) name of the cell.

required
allow_duplicate bool

Allow the creation of a cell with the same name which already is registered in the Layout. This will create a cell with the name name + $1 or 2..n increasing by the number of existing duplicates

False
args str

additional arguments passed to klayout.db.Layout.create_cell

()

Returns:

Type Description
Cell

klayout.db.Cell: klayout.db.Cell object created in the Layout

Source code in kfactory/layout.py
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
def create_cell(
    self,
    name: str,
    *args: str,
    allow_duplicate: bool = False,
) -> kdb.Cell:
    """Create a new cell in the library.

    This shouldn't be called manually.
    The constructor of KCell will call this method.

    Args:
        name: The (initial) name of the cell.
        allow_duplicate: Allow the creation of a cell with the same name which
            already is registered in the Layout.
            This will create a cell with the name `name` + `$1` or `2..n`
            increasing by the number of existing duplicates
        args: additional arguments passed to
            `klayout.db.Layout.create_cell`

    Returns:
        klayout.db.Cell: klayout.db.Cell object created in the Layout

    """
    with self.thread_lock:
        if allow_duplicate or (self.layout_cell(name) is None):
            return self.layout.create_cell(name, *args)
        raise ValueError(
            f"Cellname {name} already exists in the layout/KCLayout. "
            "Please make sure the cellname is"
            " unique or pass `allow_duplicate` when creating the library"
        )

create_layer_enclosure

create_layer_enclosure(
    sections: Sequence[
        tuple[LayerInfo, int] | tuple[LayerInfo, int, int]
    ] = [],
    name: str | None = None,
    main_layer: LayerInfo | None = None,
    dsections: Sequence[
        tuple[LayerInfo, float]
        | tuple[LayerInfo, float, float]
    ]
    | None = None,
) -> LayerEnclosure

Create a new LayerEnclosure in the KCLayout.

Source code in kfactory/layout.py
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
def create_layer_enclosure(
    self,
    sections: Sequence[
        tuple[kdb.LayerInfo, int] | tuple[kdb.LayerInfo, int, int]
    ] = [],
    name: str | None = None,
    main_layer: kdb.LayerInfo | None = None,
    dsections: Sequence[
        tuple[kdb.LayerInfo, float] | tuple[kdb.LayerInfo, float, float]
    ]
    | None = None,
) -> LayerEnclosure:
    """Create a new LayerEnclosure in the KCLayout."""
    if name is None and main_layer is not None and main_layer.name != "":
        name = main_layer.name
    enc = LayerEnclosure(
        sections=sections,
        dsections=dsections,
        name=name,
        main_layer=main_layer,
        kcl=self,
    )

    self.layer_enclosures[enc.name] = enc
    return enc

delete_cell

delete_cell(cell: AnyTKCell | int) -> None

Delete a cell in the kcl object.

Source code in kfactory/layout.py
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
def delete_cell(self, cell: AnyTKCell | int) -> None:
    """Delete a cell in the kcl object."""
    with self.thread_lock:
        if isinstance(cell, int):
            self.layout.cell(cell).locked = False
            self.layout.delete_cell(cell)
            self.tkcells.pop(cell, None)
        else:
            ci = cell.cell_index()
            self.layout.cell(ci).locked = False
            self.layout.delete_cell(ci)
            self.tkcells.pop(ci, None)

delete_cell_rec

delete_cell_rec(cell_index: int) -> None

Deletes a KCell plus all subcells.

Source code in kfactory/layout.py
1505
1506
1507
1508
1509
def delete_cell_rec(self, cell_index: int) -> None:
    """Deletes a KCell plus all subcells."""
    with self.thread_lock:
        self.layout.delete_cell_rec(cell_index)
        self.rebuild()

delete_cells

delete_cells(cell_index_list: Sequence[int]) -> None

Delete a sequence of cell by indexes.

Source code in kfactory/layout.py
1511
1512
1513
1514
1515
1516
1517
1518
def delete_cells(self, cell_index_list: Sequence[int]) -> None:
    """Delete a sequence of cell by indexes."""
    with self.thread_lock:
        for ci in cell_index_list:
            self.layout.cell(ci).locked = False
            self.tkcells.pop(ci, None)
        self.layout.delete_cells(cell_index_list)
        self.rebuild()

dkcell

dkcell(
    name: str | None = None, ports: DPorts | None = None
) -> DKCell

Create a new cell based ont he pdk's layout object.

Source code in kfactory/layout.py
1375
1376
1377
def dkcell(self, name: str | None = None, ports: DPorts | None = None) -> DKCell:
    """Create a new cell based ont he pdk's layout object."""
    return DKCell(name=name, kcl=self, ports=ports)

dup

dup(init_cells: bool = True) -> KCLayout

Create a duplication of the ~KCLayout object.

Parameters:

Name Type Description Default
init_cells bool

initialize the all cells in the new KCLayout object

True

Returns:

Type Description
KCLayout

Copy of itself

Source code in kfactory/layout.py
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
def dup(self, init_cells: bool = True) -> KCLayout:
    """Create a duplication of the `~KCLayout` object.

    Args:
        init_cells: initialize the all cells in the new KCLayout object

    Returns:
        Copy of itself
    """
    kcl = KCLayout(self.name + "_DUPLICATE")
    kcl.layout.assign(self.layout.dup())
    if init_cells:
        for i, kc in self.tkcells.items():
            kcl.tkcells[i] = kc.model_copy(
                update={"kdb_cell": kc.kdb_cell, "kcl": kcl}
            )
    kcl.rename_function = self.rename_function
    return kcl

find_layer

find_layer(name: str) -> LayerEnum
find_layer(info: LayerInfo) -> LayerEnum
find_layer(layer: int, datatype: int) -> LayerEnum
find_layer(
    layer: int, dataytpe: int, name: str
) -> LayerEnum
find_layer(
    name: str,
    *,
    allow_undefined_layers: Literal[True] = True,
) -> LayerEnum | int
find_layer(
    info: LayerInfo,
    *,
    allow_undefined_layers: Literal[True] = True,
) -> LayerEnum | int
find_layer(
    layer: int,
    datatype: int,
    *,
    allow_undefined_layers: Literal[True] = True,
) -> LayerEnum | int
find_layer(
    layer: int,
    dataytpe: int,
    name: str,
    allow_undefined_layers: Literal[True] = True,
) -> LayerEnum | int
find_layer(
    *args: int | str | LayerInfo,
    **kwargs: int | str | LayerInfo | bool,
) -> LayerEnum | int

Try to find a registered layer. Throws a KeyError if it cannot find it.

Can find a layer either by name, layer and datatype (two args), LayerInfo, or all three of layer, datatype, and name.

Source code in kfactory/layout.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
def find_layer(
    self,
    *args: int | str | kdb.LayerInfo,
    **kwargs: int | str | kdb.LayerInfo | bool,
) -> LayerEnum | int:
    """Try to find a registered layer. Throws a KeyError if it cannot find it.

    Can find a layer either by name, layer and datatype (two args), LayerInfo, or
    all three of layer, datatype, and name.
    """
    allow_undefined_layers = kwargs.pop(
        "allow_undefined_layers", config.allow_undefined_layers
    )
    info = self.layout.get_info(self.layout.layer(*args, **kwargs))
    try:
        return self.layers[info.name]  # type:ignore[no-any-return, index]
    except KeyError as e:
        if allow_undefined_layers:
            return self.layout.layer(info)
        raise KeyError(
            f"Layer '{args=}, {kwargs=}' has not been defined in the KCLayout. "
            "Have you defined the layer and set it in KCLayout.info?"
        ) from e

get_cell

get_cell(
    obj: str | int,
    cell_type: type[KC] = KCell,
    error_search_limit: int | None = 10,
) -> KC

Retrieve a cell by name(str) or index(int).

Attrs

obj: name of cell or cell_index cell_type: type of cell to return

Source code in kfactory/layout.py
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
def get_cell(
    self,
    obj: str | int,
    cell_type: type[KC] = KCell,  # type: ignore[assignment]
    error_search_limit: int | None = 10,
) -> KC:
    """Retrieve a cell by name(str) or index(int).

    Attrs:
        obj: name of cell or cell_index
        cell_type: type of cell to return
    """
    if isinstance(obj, int):
        # search by index
        try:
            return cell_type(base=self.tkcells[obj])
        except KeyError:
            kdb_c = self.layout_cell(obj)
            if kdb_c is None:
                raise
            return cell_type(name=kdb_c.name, kcl=self, kdb_cell=kdb_c)
    # search by name/key
    kdb_c = self.layout_cell(obj)
    if kdb_c is not None:
        try:
            return cell_type(base=self.tkcells[kdb_c.cell_index()])
        except KeyError:
            c = cell_type(name=kdb_c.name, kcl=self, kdb_cell=kdb_c)
            c.get_meta_data()
            return c
    if error_search_limit:
        # limit the print of available cells
        # and throw closest names with fuzzy search
        from rapidfuzz import process

        closest_names = [
            result[0]
            for result in process.extract(
                obj,
                (cell.name for cell in self.kcells.values()),
                limit=error_search_limit,
            )
        ]
        raise ValueError(
            f"Library doesn't have a KCell named {obj},"
            f" closest {error_search_limit} are: \n"
            f"{pformat(closest_names)}"
        )

    raise ValueError(
        f"Library doesn't have a KCell named {obj},"
        " available KCells are"
        f"{pformat(sorted([cell.name for cell in self.kcells.values()]))}"
    )

get_dcross_section

get_dcross_section(
    cross_section: str
    | SymmetricalCrossSection
    | CrossSectionSpec
    | DCrossSectionSpec
    | DSymmetricalCrossSection
    | CrossSection
    | DCrossSection,
) -> DCrossSection

Get a cross section by name or specification.

Source code in kfactory/layout.py
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
def get_dcross_section(
    self,
    cross_section: str
    | SymmetricalCrossSection
    | CrossSectionSpec
    | DCrossSectionSpec
    | DSymmetricalCrossSection
    | CrossSection
    | DCrossSection,
) -> DCrossSection:
    """Get a cross section by name or specification."""
    return DCrossSection(
        kcl=self, base=self.cross_sections.get_cross_section(cross_section)
    )

get_enclosure

get_enclosure(
    enclosure: str | LayerEnclosure | LayerEnclosureSpec,
) -> LayerEnclosure

Gets a layer enclosure by name specification or the layerenclosure itself.

Source code in kfactory/layout.py
1904
1905
1906
1907
1908
def get_enclosure(
    self, enclosure: str | LayerEnclosure | LayerEnclosureSpec
) -> LayerEnclosure:
    """Gets a layer enclosure by name specification or the layerenclosure itself."""
    return self.layer_enclosures.get_enclosure(enclosure, self)

get_icross_section

get_icross_section(
    cross_section: str
    | SymmetricalCrossSection
    | CrossSectionSpec
    | DCrossSectionSpec
    | DCrossSection
    | DSymmetricalCrossSection
    | CrossSection,
) -> CrossSection

Get a cross section by name or specification.

Source code in kfactory/layout.py
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
def get_icross_section(
    self,
    cross_section: str
    | SymmetricalCrossSection
    | CrossSectionSpec
    | DCrossSectionSpec
    | DCrossSection
    | DSymmetricalCrossSection
    | CrossSection,
) -> CrossSection:
    """Get a cross section by name or specification."""
    return CrossSection(
        kcl=self, base=self.cross_sections.get_cross_section(cross_section)
    )

get_meta_data

get_meta_data() -> tuple[dict[str, Any], dict[str, Any]]

Read KCLayout meta info from the KLayout object.

Source code in kfactory/layout.py
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
def get_meta_data(self) -> tuple[dict[str, Any], dict[str, Any]]:
    """Read KCLayout meta info from the KLayout object."""
    settings: dict[str, Any] = {}
    info: dict[str, Any] = {}
    cross_sections: list[dict[str, Any]] = []
    for meta in self.layout.each_meta_info():
        if meta.name.startswith("kfactory:info"):
            info[meta.name.removeprefix("kfactory:info:")] = meta.value
        elif meta.name.startswith("kfactory:settings"):
            settings[meta.name.removeprefix("kfactory:settings:")] = meta.value
        elif meta.name.startswith("kfactory:layer_enclosure:"):
            self.get_enclosure(
                LayerEnclosure(
                    **meta.value,
                )
            )
        elif meta.name.startswith("kfactory:cross_section:"):
            cross_sections.append(
                {
                    "name": meta.name.removeprefix("kfactory:cross_section:"),
                    **meta.value,
                }
            )

    for cs in cross_sections:
        self.get_symmetrical_cross_section(
            SymmetricalCrossSection(
                width=cs["width"],
                enclosure=self.get_enclosure(cs["layer_enclosure"]),
                name=cs["name"],
            )
        )

    return info, settings

get_symmetrical_cross_section

get_symmetrical_cross_section(
    cross_section: str
    | SymmetricalCrossSection
    | CrossSectionSpec
    | DCrossSectionSpec
    | DSymmetricalCrossSection,
) -> SymmetricalCrossSection

Get a cross section by name or specification.

Source code in kfactory/layout.py
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
def get_symmetrical_cross_section(
    self,
    cross_section: str
    | SymmetricalCrossSection
    | CrossSectionSpec
    | DCrossSectionSpec
    | DSymmetricalCrossSection,
) -> SymmetricalCrossSection:
    """Get a cross section by name or specification."""
    return self.cross_sections.get_cross_section(cross_section)

kcell

kcell(
    name: str | None = None, ports: Ports | None = None
) -> KCell

Create a new cell based ont he pdk's layout object.

Source code in kfactory/layout.py
1371
1372
1373
def kcell(self, name: str | None = None, ports: Ports | None = None) -> KCell:
    """Create a new cell based ont he pdk's layout object."""
    return KCell(name=name, kcl=self, ports=ports)

layerenum_from_dict

layerenum_from_dict(
    name: str = "LAYER", *, layers: LayerInfos
) -> type[LayerEnum]

Create a new LayerEnum from this KCLayout.

Source code in kfactory/layout.py
1404
1405
1406
1407
1408
def layerenum_from_dict(
    self, name: str = "LAYER", *, layers: LayerInfos
) -> type[LayerEnum]:
    """Create a new [LayerEnum][kfactory.kcell.LayerEnum] from this KCLayout."""
    return layerenum_from_dict(layers=layers, name=name, layout=self.layout)

layout_cell

layout_cell(name: str | int) -> kdb.Cell | None

Get a cell by name or index from the Layout object.

Source code in kfactory/layout.py
1444
1445
1446
def layout_cell(self, name: str | int) -> kdb.Cell | None:
    """Get a cell by name or index from the Layout object."""
    return self.layout.cell(name)

read

read(
    filename: str | Path,
    options: LoadLayoutOptions | None = None,
    register_cells: bool | None = None,
    test_merge: bool = True,
    update_kcl_meta_data: Literal[
        "overwrite", "skip", "drop"
    ] = "skip",
    meta_format: Literal["v1", "v2", "v3"] | None = None,
) -> kdb.LayerMap

Read a GDS file into the existing Layout.

Any existing meta info (KCell.info and KCell.settings) will be overwritten if a KCell already exists. Instead of overwriting the cells, they can also be loaded into new cells by using the corresponding cell_conflict_resolution.

This will fail if any of the read cells try to load into a locked KCell.

Layout meta infos are ignored from the loaded layout.

Parameters:

Name Type Description Default
filename str | Path

Path of the GDS file.

required
options LoadLayoutOptions | None

KLayout options to load from the GDS. Can determine how merge conflicts are handled for example. See https://www.klayout.de/doc-qt5/code/class_LoadLayoutOptions.html

None
register_cells bool | None

If True create KCells for all cells in the GDS.

None
test_merge bool

Check the layouts first whether they are compatible (no differences).

True
update_kcl_meta_data Literal['overwrite', 'skip', 'drop']

How to treat loaded KCLayout info. overwrite: overwrite existing info entries skip: keep existing info values drop: don't add any new info

'skip'
meta_format Literal['v1', 'v2', 'v3'] | None

How to read KCell metainfo from the gds. v1 had stored port transformations as strings, never versions have them stored and loaded in their native KLayout formats.

None
Source code in kfactory/layout.py
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
def read(
    self,
    filename: str | Path,
    options: kdb.LoadLayoutOptions | None = None,
    register_cells: bool | None = None,
    test_merge: bool = True,
    update_kcl_meta_data: Literal["overwrite", "skip", "drop"] = "skip",
    meta_format: Literal["v1", "v2", "v3"] | None = None,
) -> kdb.LayerMap:
    """Read a GDS file into the existing Layout.

    Any existing meta info (KCell.info and KCell.settings) will be overwritten if
    a KCell already exists. Instead of overwriting the cells, they can also be
    loaded into new cells by using the corresponding cell_conflict_resolution.

    This will fail if any of the read cells try to load into a locked KCell.

    Layout meta infos are ignored from the loaded layout.

    Args:
        filename: Path of the GDS file.
        options: KLayout options to load from the GDS. Can determine how merge
            conflicts are handled for example. See
            https://www.klayout.de/doc-qt5/code/class_LoadLayoutOptions.html
        register_cells: If `True` create KCells for all cells in the GDS.
        test_merge: Check the layouts first whether they are compatible
            (no differences).
        update_kcl_meta_data: How to treat loaded KCLayout info.
            overwrite: overwrite existing info entries
            skip: keep existing info values
            drop: don't add any new info
        meta_format: How to read KCell metainfo from the gds. `v1` had stored port
            transformations as strings, never versions have them stored and loaded
            in their native KLayout formats.
    """
    if options is None:
        options = load_layout_options()
    with self.thread_lock:
        if meta_format is None:
            meta_format = config.meta_format
        if register_cells is None:
            register_cells = meta_format == config.meta_format
        layout_b = kdb.Layout()
        layout_b.read(str(filename), options)
        if (
            self.cells() > 0
            and test_merge
            and (
                options.cell_conflict_resolution
                != kdb.LoadLayoutOptions.CellConflictResolution.RenameCell
            )
        ):
            self.set_meta_data()
            for kcell in self.kcells.values():
                kcell.set_meta_data()
            diff = MergeDiff(
                layout_a=self.layout,
                layout_b=layout_b,
                name_a=self.name,
                name_b=Path(filename).stem,
            )
            diff.compare()
            if diff.dbu_differs:
                raise MergeError(
                    "Layouts' DBU differ. Check the log for more info."
                )
            if diff.diff_xor.cells() > 0:
                diff_kcl = KCLayout(self.name + "_XOR")
                diff_kcl.layout.assign(diff.diff_xor)
                show(diff_kcl)

                err_msg = (
                    f"Layout {self.name} cannot merge with layout "
                    f"{Path(filename).stem} safely. See the error messages "
                    f"or check with KLayout."
                )

                if diff.layout_meta_diff:
                    yaml = ruamel.yaml.YAML(typ=["rt", "string"])
                    err_msg += (
                        "\nLayout Meta Diff:\n```\n"
                        + yaml.dumps(dict(diff.layout_meta_diff))
                        + "\n```"
                    )
                if diff.cells_meta_diff:
                    yaml = ruamel.yaml.YAML(typ=["rt", "string"])
                    err_msg += (
                        "\nLayout Meta Diff:\n```\n"
                        + yaml.dumps(dict(diff.cells_meta_diff))
                        + "\n```"
                    )

                raise MergeError(err_msg)

        cells = set(self.cells("*"))
        saveopts = save_layout_options()
        saveopts.gds2_max_cellname_length = (
            kdb.SaveLayoutOptions().gds2_max_cellname_length
        )
        binary_layout = layout_b.write_bytes(saveopts)
        locked_cells = [
            kdb_cell for kdb_cell in self.layout.each_cell() if kdb_cell.locked
        ]
        for kdb_cell in locked_cells:
            kdb_cell.locked = False
        lm = self.layout.read_bytes(binary_layout, options)
        for kdb_cell in locked_cells:
            kdb_cell.locked = True
        info, settings = self.get_meta_data()

        match update_kcl_meta_data:
            case "overwrite":
                for k, v in info.items():
                    self.info[k] = v
            case "skip":
                info_ = self.info.model_dump()

                info.update(info_)
                self.info = Info(**info)

            case "drop":
                pass
            case _:
                raise ValueError(
                    f"Unknown meta update strategy {update_kcl_meta_data=}"
                    ", available strategies are 'overwrite', 'skip', or 'drop'"
                )
        meta_format = settings.get("meta_format") or config.meta_format
        load_cells = {
            cell
            for c in layout_b.cells("*")
            if (cell := self.layout_cell(c.name)) is not None
        }
        new_cells = load_cells - cells

        if register_cells:
            for c in sorted(new_cells, key=lambda _c: _c.hierarchy_levels()):
                kc = KCell(kdb_cell=c, kcl=self)
                kc.get_meta_data(
                    meta_format=meta_format,
                )

        for c in load_cells & cells:
            kc = self.kcells[c.cell_index()]
            kc.get_meta_data(meta_format=meta_format)

        return lm

rebuild

rebuild() -> None

Rebuild the KCLayout based on the Layout object.

Source code in kfactory/layout.py
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
def rebuild(self) -> None:
    """Rebuild the KCLayout based on the Layout object."""
    kcells2delete: list[int] = []
    with self.thread_lock:
        for ci, c in self.tkcells.items():
            if c.kdb_cell._destroyed():
                kcells2delete.append(ci)

        for ci in kcells2delete:
            del self.tkcells[ci]

        for cell in self.cells("*"):
            if cell.cell_index() not in self.tkcells:
                self.tkcells[cell.cell_index()] = self.get_cell(
                    cell.cell_index(), KCell
                ).base

register_cell

register_cell(
    kcell: AnyTKCell, allow_reregister: bool = False
) -> None

Register an existing cell in the KCLayout object.

Parameters:

Name Type Description Default
kcell AnyTKCell

KCell 56 be registered in the KCLayout

required
allow_reregister bool

Overwrite the existing KCell registration with this one. Doesn't allow name duplication.

False
Source code in kfactory/layout.py
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
def register_cell(self, kcell: AnyTKCell, allow_reregister: bool = False) -> None:
    """Register an existing cell in the KCLayout object.

    Args:
        kcell: KCell 56 be registered in the KCLayout
        allow_reregister: Overwrite the existing KCell registration with this one.
            Doesn't allow name duplication.
    """
    with self.thread_lock:
        if (kcell.cell_index() not in self.tkcells) or allow_reregister:
            self.tkcells[kcell.cell_index()] = kcell.base
        else:
            raise ValueError(
                f"Cannot register {kcell} if it has been registered already"
                " exists in the library"
            )

set_layers_from_infos

set_layers_from_infos(
    name: str, layers: LayerInfos
) -> type[LayerEnum]

Create a new LAYER enum based on the pdk's kcl.

Source code in kfactory/layout.py
1383
1384
1385
def set_layers_from_infos(self, name: str, layers: LayerInfos) -> type[LayerEnum]:
    """Create a new LAYER enum based on the pdk's kcl."""
    return layerenum_from_dict(name=name, layers=layers, layout=self.layout)

set_meta_data

set_meta_data() -> None

Set the info/settings of the KCLayout.

Source code in kfactory/layout.py
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
def set_meta_data(self) -> None:
    """Set the info/settings of the KCLayout."""
    for name, setting in self.settings.model_dump().items():
        self.add_meta_info(
            kdb.LayoutMetaInfo(f"kfactory:settings:{name}", setting, None, True)
        )
    for name, info in self.info.model_dump().items():
        self.add_meta_info(
            kdb.LayoutMetaInfo(f"kfactory:info:{name}", info, None, True)
        )
    for enclosure in self.layer_enclosures.root.values():
        self.add_meta_info(
            kdb.LayoutMetaInfo(
                f"kfactory:layer_enclosure:{enclosure.name}",
                enclosure.model_dump(),
                None,
                True,
            )
        )
    for cross_section in self.cross_sections.cross_sections.values():
        self.add_meta_info(
            kdb.LayoutMetaInfo(
                f"kfactory:cross_section:{cross_section.name}",
                {
                    "width": cross_section.width,
                    "layer_enclosure": cross_section.enclosure.name,
                },
                None,
                True,
            )
        )

to_dbu

to_dbu(other: None) -> None
to_dbu(other: float) -> int
to_dbu(other: DPoint) -> kdb.Point
to_dbu(other: DVector) -> kdb.Vector
to_dbu(other: DBox) -> kdb.Box
to_dbu(other: DPolygon) -> kdb.Polygon
to_dbu(other: DPath) -> kdb.Path
to_dbu(other: DText) -> kdb.Text
to_dbu(
    other: float
    | DPoint
    | DVector
    | DBox
    | DPolygon
    | DPath
    | DText
    | None,
) -> (
    int
    | kdb.Point
    | kdb.Vector
    | kdb.Box
    | kdb.Polygon
    | kdb.Path
    | kdb.Text
    | None
)

Convert Shapes or values in dbu to DShapes or floats in um.

Source code in kfactory/layout.py
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def to_dbu(
    self,
    other: float
    | kdb.DPoint
    | kdb.DVector
    | kdb.DBox
    | kdb.DPolygon
    | kdb.DPath
    | kdb.DText
    | None,
) -> (
    int
    | kdb.Point
    | kdb.Vector
    | kdb.Box
    | kdb.Polygon
    | kdb.Path
    | kdb.Text
    | None
):
    """Convert Shapes or values in dbu to DShapes or floats in um."""
    if other is None:
        return None
    return kdb.CplxTrans(self.layout.dbu).inverted() * other

to_um

to_um(other: None) -> None
to_um(other: int) -> float
to_um(other: Point) -> kdb.DPoint
to_um(other: Vector) -> kdb.DVector
to_um(other: Box) -> kdb.DBox
to_um(other: Polygon) -> kdb.DPolygon
to_um(other: Path) -> kdb.DPath
to_um(other: Text) -> kdb.DText
to_um(
    other: int
    | Point
    | Vector
    | Box
    | Polygon
    | Path
    | Text
    | None,
) -> (
    float
    | kdb.DPoint
    | kdb.DVector
    | kdb.DBox
    | kdb.DPolygon
    | kdb.DPath
    | kdb.DText
    | None
)

Convert Shapes or values in dbu to DShapes or floats in um.

Source code in kfactory/layout.py
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
def to_um(
    self,
    other: int
    | kdb.Point
    | kdb.Vector
    | kdb.Box
    | kdb.Polygon
    | kdb.Path
    | kdb.Text
    | None,
) -> (
    float
    | kdb.DPoint
    | kdb.DVector
    | kdb.DBox
    | kdb.DPolygon
    | kdb.DPath
    | kdb.DText
    | None
):
    """Convert Shapes or values in dbu to DShapes or floats in um."""
    if other is None:
        return None
    return kdb.CplxTrans(self.layout.dbu) * other

top_kcell

top_kcell() -> KCell

Return the top KCell if there is a single one.

Source code in kfactory/layout.py
1892
1893
1894
def top_kcell(self) -> KCell:
    """Return the top KCell if there is a single one."""
    return self[self.top_cell().cell_index()]

top_kcells

top_kcells() -> list[KCell]

Return the top KCells.

Source code in kfactory/layout.py
1888
1889
1890
def top_kcells(self) -> list[KCell]:
    """Return the top KCells."""
    return [self[tc.cell_index()] for tc in self.top_cells()]

vcell

vcell(
    _func: Callable[KCellParams, VK],
) -> Callable[KCellParams, VK]
vcell(
    *,
    set_settings: bool = True,
    set_name: bool = True,
    add_port_layers: bool = True,
    cache: Cache[int, Any] | dict[int, Any] | None = None,
    basename: str | None = None,
    drop_params: Sequence[str] = ("self", "cls"),
    register_factory: bool = True,
    post_process: Iterable[Callable[[VKCell], None]],
    info: dict[str, MetaData] | None = None,
    check_ports: bool = True,
    check_pins: bool = True,
    tags: list[str] | None = None,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> Callable[
    [Callable[KCellParams, VK]], Callable[KCellParams, VK]
]
vcell(
    *,
    output_type: type[VK],
    set_settings: bool = True,
    set_name: bool = True,
    add_port_layers: bool = True,
    cache: Cache[int, Any] | dict[int, Any] | None = None,
    basename: str | None = None,
    drop_params: Sequence[str] = ("self", "cls"),
    register_factory: bool = True,
    post_process: Iterable[Callable[[VKCell], None]],
    info: dict[str, MetaData] | None = None,
    check_ports: bool = True,
    check_pins: bool = True,
    tags: list[str] | None = None,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> Callable[
    [Callable[KCellParams, VKCell]],
    Callable[KCellParams, VK],
]
vcell(
    _func: Callable[KCellParams, VKCell] | None = None,
    /,
    *,
    output_type: type[VK] | None = None,
    set_settings: bool = True,
    set_name: bool = True,
    add_port_layers: bool = True,
    cache: Cache[int, Any] | dict[int, Any] | None = None,
    basename: str | None = None,
    drop_params: Sequence[str] = ("self", "cls"),
    register_factory: bool = True,
    post_process: Iterable[Callable[[VKCell], None]]
    | None = None,
    info: dict[str, MetaData] | None = None,
    check_ports: bool = True,
    check_pins: bool = True,
    tags: list[str] | None = None,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> (
    Callable[KCellParams, VK]
    | Callable[
        [Callable[KCellParams, VK]],
        Callable[KCellParams, VK],
    ]
)

Decorator to cache and auto name the cell.

This will use functools.cache to cache the function call. Additionally, if enabled this will set the name and from the args/kwargs of the function and also paste them into a settings dictionary of the KCell.

Parameters:

Name Type Description Default
set_settings bool

Copy the args & kwargs into the settings dictionary

True
set_name bool

Auto create the name of the cell to the functionname plus a string created from the args/kwargs

True
check_ports bool

Check uniqueness of port names.

True
check_pins bool

Check uniqueness of pin names.

True
add_port_layers bool

Add special layers of KCLayout.netlist_layer_mapping to the ports if the port layer is in the mapping.

True
cache Cache[int, Any] | dict[int, Any] | None

Provide a user defined cache instead of an internal one. This can be used for example to clear the cache.

None
basename str | None

Overwrite the name normally inferred from the function or class name.

None
drop_params Sequence[str]

Drop these parameters before writing the settings

('self', 'cls')
register_factory bool

Register the resulting KCell-function to the KCLayout.factories

True
info dict[str, MetaData] | None

Additional metadata to put into info attribute.

None
post_process Iterable[Callable[[VKCell], None]] | None

List of functions to call after the cell has been created.

None

Returns: A wrapped vcell function which caches responses and modifies the VKCell according to settings.

Source code in kfactory/layout.py
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
def vcell(
    self,
    _func: Callable[KCellParams, VKCell] | None = None,
    /,
    *,
    output_type: type[VK] | None = None,
    set_settings: bool = True,
    set_name: bool = True,
    add_port_layers: bool = True,
    cache: Cache[int, Any] | dict[int, Any] | None = None,
    basename: str | None = None,
    drop_params: Sequence[str] = ("self", "cls"),
    register_factory: bool = True,
    post_process: Iterable[Callable[[VKCell], None]] | None = None,
    info: dict[str, MetaData] | None = None,
    check_ports: bool = True,
    check_pins: bool = True,
    tags: list[str] | None = None,
    lvs_equivalent_ports: list[list[str]] | None = None,
    ports: PortsDefinition | None = None,
) -> (
    Callable[KCellParams, VK]
    | Callable[[Callable[KCellParams, VK]], Callable[KCellParams, VK]]
):
    """Decorator to cache and auto name the cell.

    This will use `functools.cache` to cache the function call.
    Additionally, if enabled this will set the name and from the args/kwargs of the
    function and also paste them into a settings dictionary of the
    [KCell][kfactory.kcell.KCell].

    Args:
        set_settings: Copy the args & kwargs into the settings dictionary
        set_name: Auto create the name of the cell to the functionname plus a
            string created from the args/kwargs
        check_ports: Check uniqueness of port names.
        check_pins: Check uniqueness of pin names.
        add_port_layers: Add special layers of `KCLayout.netlist_layer_mapping`
            to the ports if the port layer is in the mapping.
        cache: Provide a user defined cache instead of an internal one. This
            can be used for example to clear the cache.
        basename: Overwrite the name normally inferred from the function or class
            name.
        drop_params: Drop these parameters before writing the
            [settings][kfactory.kcell.KCell.settings]
        register_factory: Register the resulting KCell-function to the
            `KCLayout.factories`
        info: Additional metadata to put into info attribute.
        post_process: List of functions to call after the cell has been created.
    Returns:
        A wrapped vcell function which caches responses and modifies the VKCell
        according to settings.
    """
    if post_process is None:
        post_process = ()

    def decorator_autocell(
        f: Callable[KCellParams, VKCell],
    ) -> Callable[KCellParams, VK]:
        sig = inspect.signature(f)
        output_cell_type_: type[VK | VKCell]
        if output_type is not None:
            output_cell_type_ = output_type
        elif sig.return_annotation is not inspect.Signature.empty:
            output_cell_type_ = sig.return_annotation
        else:
            output_cell_type_ = self.default_vcell_output_type

        output_cell_type__ = cast("type[VK]", output_cell_type_)
        # previously was a KCellCache, but dict should do for most case
        cache_: Cache[int, VK] | dict[int, VK] = cache or Cache(
            maxsize=float("inf")
        )

        wrapper_autocell = WrappedVKCellFunc(
            kcl=self,
            f=f,
            sig=sig,
            cache=cache_,
            set_settings=set_settings,
            set_name=set_name,
            add_port_layers=add_port_layers,
            basename=basename,
            drop_params=drop_params,
            post_process=post_process,
            output_type=output_cell_type__,
            info=info,
            check_ports=check_ports,
            check_pins=check_pins,
            lvs_equivalent_ports=lvs_equivalent_ports,
            ports=ports,
        )

        if register_factory:
            if wrapper_autocell.name is None:
                raise ValueError(f"Function {f} has no name.")
            self.virtual_factories.add(wrapper_autocell)  # type: ignore[arg-type]

        @functools.wraps(f)
        def func(*args: KCellParams.args, **kwargs: KCellParams.kwargs) -> VK:
            return wrapper_autocell(*args, **kwargs)

        return func

    return decorator_autocell if _func is None else decorator_autocell(_func)

vkcell

vkcell(name: str | None = None) -> VKCell

Create a new cell based ont he pdk's layout object.

Source code in kfactory/layout.py
1379
1380
1381
def vkcell(self, name: str | None = None) -> VKCell:
    """Create a new cell based ont he pdk's layout object."""
    return VKCell(name=name, kcl=self)

write

write(
    filename: str | Path,
    options: SaveLayoutOptions | None = None,
    set_meta_data: bool = True,
    convert_external_cells: bool = False,
    autoformat_from_file_extension: bool = True,
) -> None

Write a GDS file into the existing Layout.

Parameters:

Name Type Description Default
filename str | Path

Path of the GDS file.

required
options SaveLayoutOptions | None

KLayout options to load from the GDS. Can determine how merge conflicts are handled for example. See https://www.klayout.de/doc-qt5/code/class_LoadLayoutOptions.html

None
set_meta_data bool

Make sure all the cells have their metadata set

True
convert_external_cells bool

Whether to make KCells not in this KCLayout to

False
autoformat_from_file_extension bool

Set the format of the output file automatically from the file extension of filename. This is necessary for the options. If not set, this will default to GDSII.

True
Source code in kfactory/layout.py
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
def write(
    self,
    filename: str | Path,
    options: kdb.SaveLayoutOptions | None = None,
    set_meta_data: bool = True,
    convert_external_cells: bool = False,
    autoformat_from_file_extension: bool = True,
) -> None:
    """Write a GDS file into the existing Layout.

    Args:
        filename: Path of the GDS file.
        options: KLayout options to load from the GDS. Can determine how merge
            conflicts are handled for example. See
            https://www.klayout.de/doc-qt5/code/class_LoadLayoutOptions.html
        set_meta_data: Make sure all the cells have their metadata set
        convert_external_cells: Whether to make KCells not in this KCLayout to
        autoformat_from_file_extension: Set the format of the output file
            automatically from the file extension of `filename`. This is necessary
            for the options. If not set, this will default to `GDSII`.
    """
    if options is None:
        options = save_layout_options()
    if isinstance(filename, Path):
        filename = str(filename.resolve())
    for kc in list(self.kcells.values()):
        kc.insert_vinsts()
    match (set_meta_data, convert_external_cells):
        case (True, True):
            self.set_meta_data()
            for kcell in self.kcells.values():
                if not kcell.destroyed():
                    kcell.set_meta_data()
                    if kcell.is_library_cell():
                        kcell.convert_to_static(recursive=True)
        case (True, False):
            self.set_meta_data()
            for kcell in self.kcells.values():
                if not kcell.destroyed():
                    kcell.set_meta_data()
        case (False, True):
            for kcell in self.kcells.values():
                if kcell.is_library_cell() and not kcell.destroyed():
                    kcell.convert_to_static(recursive=True)

    if autoformat_from_file_extension:
        options.set_format_from_filename(filename)

    return self.layout.write(filename, options)

KCell

Bases: ProtoTKCell[int], DBUGeometricObject, ICreatePort

Cell with integer units.

Source code in kfactory/kcell.py
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
class KCell(ProtoTKCell[int], DBUGeometricObject, ICreatePort):
    """Cell with integer units."""

    yaml_tag: ClassVar[str] = "!KCell"

    @overload
    def __init__(self, *, base: TKCell) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        kcl: KCLayout | None = None,
        kdb_cell: kdb.Cell | None = None,
        ports: Iterable[ProtoPort[Any]] | None = None,
        info: dict[str, Any] | None = None,
        settings: dict[str, Any] | None = None,
        pins: Iterable[ProtoPin[Any]] | None = None,
    ) -> None: ...

    def __init__(
        self,
        name: str | None = None,
        kcl: KCLayout | None = None,
        kdb_cell: kdb.Cell | None = None,
        ports: Iterable[ProtoPort[Any]] | None = None,
        info: dict[str, Any] | None = None,
        settings: dict[str, Any] | None = None,
        pins: Iterable[ProtoPin[Any]] | None = None,
        *,
        base: TKCell | None = None,
    ) -> None:
        """Constructor of KCell.

        Args:
            base: If not `None`, a KCell will be created from and existing
                KLayout Cell
            name: Name of the cell, if None will autogenerate name to
                "Unnamed_<cell_index>".
            kcl: KCLayout the cell should be attached to.
            kdb_cell: If not `None`, a KCell will be created from and existing
                KLayout Cell
            ports: Attach an existing [Ports][kfactory.kcell.Ports] object to the KCell,
                if `None` create an empty one.
            info: Info object to attach to the KCell.
            settings: KCellSettings object to attach to the KCell.
        """
        super().__init__(
            base=base,
            name=name,
            kcl=kcl,
            kdb_cell=kdb_cell,
            ports=ports,
            info=info,
            settings=settings,
            pins=pins,
        )

    @property
    def ports(self) -> Ports:
        """Ports associated with the cell."""
        return Ports(kcl=self.kcl, bases=self._base.ports)

    @ports.setter
    def ports(self, new_ports: Iterable[ProtoPort[Any]]) -> None:
        if self.locked:
            raise LockedError(self)
        self._base.ports = [port.base for port in new_ports]

    @property
    def pins(self) -> Pins:
        """Pins associated with the cell."""
        return Pins(kcl=self.kcl, bases=self._base.pins)

    @pins.setter
    def pins(self, new_pins: Iterable[ProtoPin[Any]]) -> None:
        if self.locked:
            raise LockedError(self)
        self._base.pins = [pin.base for pin in new_pins]

    @property
    def insts(self) -> Instances:
        """Instances associated with the cell."""
        return Instances(cell=self._base)

    @property
    def library_cell(self) -> KCell:
        if self.kdb_cell.is_library_cell():
            lib_cell = self.base._library_cell
            assert lib_cell is not None
            return lib_cell
        raise ValueError(
            "This is not a proxy cell referencing a library cell. Please check"
            " with `.is_library_cell()` first if unsure."
        )

    def __lshift__(self, cell: AnyTKCell) -> Instance:
        """Convenience function for `KCell.create_inst`.

        Args:
            cell: The cell to be added as an instance
        """
        return self.create_inst(cell)

    def add_port(
        self,
        *,
        port: ProtoPort[Any],
        name: str | None = None,
        keep_mirror: bool = False,
    ) -> Port:
        """Create a port in the cell."""
        if self.locked:
            raise LockedError(self)

        return self.ports.add_port(
            port=port,
            name=name,
            keep_mirror=keep_mirror,
        )

    def create_pin(
        self,
        *,
        ports: Iterable[ProtoPort[Any]],
        name: str | None = None,
        pin_type: str = "DC",
        info: dict[str, int | float | str] | None = None,
    ) -> Pin:
        """Create a pin in the cell."""
        return self.pins.create_pin(
            name=name, ports=ports, pin_type=pin_type, info=info
        )

    def __getitem__(self, key: int | str | None) -> Port:
        """Returns port from instance."""
        return self.ports[key]

    def create_inst(
        self,
        cell: AnyTKCell | int,
        trans: kdb.Trans | kdb.Vector | kdb.ICplxTrans | None = None,
        *,
        a: kdb.Vector | None = None,
        b: kdb.Vector | None = None,
        na: int = 1,
        nb: int = 1,
        libcell_as_static: bool = False,
        static_name_separator: str = "__",
    ) -> Instance:
        return Instance(
            kcl=self.kcl,
            instance=self.icreate_inst(
                cell,
                trans or kdb.Trans(),
                a=a,
                b=b,
                na=na,
                nb=nb,
                libcell_as_static=libcell_as_static,
                static_name_separator=static_name_separator,
            ).instance,
        )

    @classmethod
    def from_yaml(
        cls,
        constructor: SafeConstructor,
        node: Any,
        verbose: bool = False,
    ) -> Self:
        """Internal function used by the placer to convert yaml to a KCell."""
        d = SafeConstructor.construct_mapping(
            constructor,
            node,
            deep=True,
        )
        cell = cls(name=d["name"])
        if verbose:
            logger.info(f"Building {d['name']}")
        for _d in d.get("ports", Ports(ports=[], kcl=cell.kcl)):
            layer_as_string = (
                str(_d["layer"]).replace("[", "").replace("]", "").replace(", ", "/")
            )
            if "dcplx_trans" in _d:
                cell.create_port(
                    name=str(_d["name"]),
                    dcplx_trans=kdb.DCplxTrans.from_s(_d["dcplx_trans"]),
                    width=_d["dwidth"],
                    layer=cell.kcl.layer(kdb.LayerInfo.from_string(layer_as_string)),
                    port_type=_d["port_type"],
                )
            else:
                cell.create_port(
                    name=str(_d["name"]),
                    trans=kdb.Trans.from_s(_d["trans"]),
                    width=int(_d["width"]),
                    layer=cell.kcl.layer(kdb.LayerInfo.from_string(layer_as_string)),
                    port_type=_d["port_type"],
                )
        cell.settings = KCellSettings(
            **{
                name: deserialize_setting(setting)
                for name, setting in d.get("settings", {}).items()
            }
        )
        cell.info = Info(
            **{
                name: deserialize_setting(setting)
                for name, setting in d.get("info", {}).items()
            }
        )
        for inst in d.get("insts", []):
            if "cellname" in inst:
                cell_ = cell.kcl[inst["cellname"]]
            elif "cellfunction" in inst:
                module_name, fname = inst["cellfunction"].rsplit(".", 1)
                module = importlib.import_module(module_name)
                cellf = getattr(module, fname)
                cell_ = cellf(**inst["settings"])
                del module
            else:
                raise NotImplementedError(
                    'To define an instance, either a "cellfunction" or'
                    ' a "cellname" needs to be defined'
                )
            t = inst.get("trans", {})
            if isinstance(t, str):
                cell.create_inst(
                    cell_,
                    kdb.Trans.from_s(inst["trans"]),
                )
            else:
                angle = t.get("angle", 0)
                mirror = t.get("mirror", False)

                kinst = cell.create_inst(
                    cell_,
                    kdb.Trans(angle, mirror, 0, 0),
                )

                x0_yml = t.get("x0", DEFAULT_TRANS["x0"])
                y0_yml = t.get("y0", DEFAULT_TRANS["y0"])
                x_yml = t.get("x", DEFAULT_TRANS["x"])
                y_yml = t.get("y", DEFAULT_TRANS["y"])
                margin = t.get("margin", DEFAULT_TRANS["margin"])
                margin_x = margin.get(
                    "x",
                    DEFAULT_TRANS["margin"]["x"],  # type: ignore[index]
                )
                margin_y = margin.get(
                    "y",
                    DEFAULT_TRANS["margin"]["y"],  # type: ignore[index]
                )
                margin_x0 = margin.get(
                    "x0",
                    DEFAULT_TRANS["margin"]["x0"],  # type: ignore[index]
                )
                margin_y0 = margin.get(
                    "y0",
                    DEFAULT_TRANS["margin"]["y0"],  # type: ignore[index]
                )
                ref_yml = t.get("ref", DEFAULT_TRANS["ref"])
                if isinstance(ref_yml, str):
                    i: Instance
                    for i in reversed(cell.insts):
                        if i.cell.name == ref_yml:
                            ref = i
                            break
                    else:
                        raise IndexError(
                            f"No instance with cell name: <{ref_yml}> found"
                        )
                elif isinstance(ref_yml, int) and len(cell.insts) > 1:
                    ref = cell.insts[ref_yml]

                # margins for x0/y0 need to be in with opposite sign of
                # x/y due to them being subtracted later

                # x0
                match x0_yml:
                    case "W":
                        x0 = kinst.bbox().left - margin_x0
                    case "E":
                        x0 = kinst.bbox().right + margin_x0
                    case _:
                        if isinstance(x0_yml, int):
                            x0 = x0_yml
                        else:
                            raise NotImplementedError("unknown format for x0")
                # y0
                match y0_yml:
                    case "S":
                        y0 = kinst.bbox().bottom - margin_y0
                    case "N":
                        y0 = kinst.bbox().top + margin_y0
                    case _:
                        if isinstance(y0_yml, int):
                            y0 = y0_yml
                        else:
                            raise NotImplementedError("unknown format for y0")
                # x
                match x_yml:
                    case "W":
                        if len(cell.insts) > 1:
                            x = ref.bbox().left
                            if x_yml != x0_yml:
                                x -= margin_x
                        else:
                            x = margin_x
                    case "E":
                        if len(cell.insts) > 1:
                            x = ref.bbox().right
                            if x_yml != x0_yml:
                                x += margin_x
                        else:
                            x = margin_x
                    case _:
                        if isinstance(x_yml, int):
                            x = x_yml
                        else:
                            raise NotImplementedError("unknown format for x")
                # y
                match y_yml:
                    case "S":
                        if len(cell.insts) > 1:
                            y = ref.bbox().bottom
                            if y_yml != y0_yml:
                                y -= margin_y
                        else:
                            y = margin_y
                    case "N":
                        if len(cell.insts) > 1:
                            y = ref.bbox().top
                            if y_yml != y0_yml:
                                y += margin_y
                        else:
                            y = margin_y
                    case _:
                        if isinstance(y_yml, int):
                            y = y_yml
                        else:
                            raise NotImplementedError("unknown format for y")
                kinst.transform(kdb.Trans(0, False, x - x0, y - y0))
        type_to_class: dict[
            str,
            Callable[
                [str],
                kdb.Box
                | kdb.DBox
                | kdb.Polygon
                | kdb.DPolygon
                | kdb.Edge
                | kdb.DEdge
                | kdb.Text
                | kdb.DText,
            ],
        ] = {
            "box": kdb.Box.from_s,
            "polygon": kdb.Polygon.from_s,
            "edge": kdb.Edge.from_s,
            "text": kdb.Text.from_s,
            "dbox": kdb.DBox.from_s,
            "dpolygon": kdb.DPolygon.from_s,
            "dedge": kdb.DEdge.from_s,
            "dtext": kdb.DText.from_s,
        }

        for layer, shapes in dict(d.get("shapes", {})).items():
            linfo = kdb.LayerInfo.from_string(layer)
            for shape in shapes:
                shapetype, shapestring = shape.split(" ", 1)
                cell.shapes(cell.layout().layer(linfo)).insert(
                    type_to_class[shapetype](shapestring)
                )

        return cell

    @classmethod
    def to_yaml(cls, representer: BaseRepresenter, node: Self) -> MappingNode:
        """Internal function to convert the cell to yaml."""
        d: dict[str, Any] = {"name": node.name}

        insts = [
            {"cellname": inst.cell.name, "trans": inst.instance.trans.to_s()}
            for inst in node.insts
        ]
        shapes = {
            node.layout().get_info(layer).to_s(): [
                shape.to_s() for shape in node.shapes(layer).each()
            ]
            for layer in node.layout().layer_indexes()
            if not node.shapes(layer).is_empty()
        }
        ports: list[dict[str, Any]] = []
        for port in node.ports:
            l_ = node.kcl.get_info(port.layer)
            p: dict[str, Any] = {
                "name": port.name,
                "layer": [l_.layer, l_.datatype],
                "port_type": port.port_type,
            }
            if port.base.trans:
                p["trans"] = port.base.trans.to_s()
                p["width"] = port.width
            else:
                assert port.base.dcplx_trans is not None
                p["dcplx_trans"] = port.base.dcplx_trans.to_s()
                p["dwidth"] = port.dwidth
            p["info"] = {
                name: serialize_setting(setting)
                for name, setting in node.info.model_dump().items()
            }
            ports.append(p)

        d["ports"] = ports

        if insts:
            d["insts"] = insts
        if shapes:
            d["shapes"] = shapes
        d["settings"] = {
            name: serialize_setting(setting)
            for name, setting in node.settings.model_dump().items()
        }
        d["info"] = {
            name: serialize_setting(info)
            for name, info in node.info.model_dump().items()
        }
        return representer.represent_mapping(cls.yaml_tag, d)

    def get_cross_section(
        self,
        cross_section: str
        | dict[str, Any]
        | Callable[..., CrossSection | DCrossSection]
        | SymmetricalCrossSection,
        **cross_section_kwargs: Any,
    ) -> CrossSection:
        if isinstance(cross_section, str):
            return CrossSection(
                kcl=self.kcl, base=self.kcl.cross_sections[cross_section]
            )
        if isinstance(cross_section, SymmetricalCrossSection):
            return CrossSection(kcl=self.kcl, base=cross_section)
        if callable(cross_section):
            any_cross_section = cross_section(**cross_section_kwargs)
            return CrossSection(kcl=self.kcl, base=any_cross_section._base)
        if isinstance(cross_section, dict):
            return CrossSection(
                kcl=self.kcl,
                name=cross_section.get("name"),
                **cross_section["settings"],
            )
        raise ValueError(
            "Cannot create a cross section from "
            f"{type(cross_section)=} and {cross_section_kwargs=}"
        )

insts property

insts: Instances

Instances associated with the cell.

pins property writable

pins: Pins

Pins associated with the cell.

ports property writable

ports: Ports

Ports associated with the cell.

__getitem__

__getitem__(key: int | str | None) -> Port

Returns port from instance.

Source code in kfactory/kcell.py
3117
3118
3119
def __getitem__(self, key: int | str | None) -> Port:
    """Returns port from instance."""
    return self.ports[key]

__init__

__init__(*, base: TKCell) -> None
__init__(
    name: str | None = None,
    kcl: KCLayout | None = None,
    kdb_cell: Cell | None = None,
    ports: Iterable[ProtoPort[Any]] | None = None,
    info: dict[str, Any] | None = None,
    settings: dict[str, Any] | None = None,
    pins: Iterable[ProtoPin[Any]] | None = None,
) -> None
__init__(
    name: str | None = None,
    kcl: KCLayout | None = None,
    kdb_cell: Cell | None = None,
    ports: Iterable[ProtoPort[Any]] | None = None,
    info: dict[str, Any] | None = None,
    settings: dict[str, Any] | None = None,
    pins: Iterable[ProtoPin[Any]] | None = None,
    *,
    base: TKCell | None = None,
) -> None

Constructor of KCell.

Parameters:

Name Type Description Default
base TKCell | None

If not None, a KCell will be created from and existing KLayout Cell

None
name str | None

Name of the cell, if None will autogenerate name to "Unnamed_".

None
kcl KCLayout | None

KCLayout the cell should be attached to.

None
kdb_cell Cell | None

If not None, a KCell will be created from and existing KLayout Cell

None
ports Iterable[ProtoPort[Any]] | None

Attach an existing Ports object to the KCell, if None create an empty one.

None
info dict[str, Any] | None

Info object to attach to the KCell.

None
settings dict[str, Any] | None

KCellSettings object to attach to the KCell.

None
Source code in kfactory/kcell.py
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
def __init__(
    self,
    name: str | None = None,
    kcl: KCLayout | None = None,
    kdb_cell: kdb.Cell | None = None,
    ports: Iterable[ProtoPort[Any]] | None = None,
    info: dict[str, Any] | None = None,
    settings: dict[str, Any] | None = None,
    pins: Iterable[ProtoPin[Any]] | None = None,
    *,
    base: TKCell | None = None,
) -> None:
    """Constructor of KCell.

    Args:
        base: If not `None`, a KCell will be created from and existing
            KLayout Cell
        name: Name of the cell, if None will autogenerate name to
            "Unnamed_<cell_index>".
        kcl: KCLayout the cell should be attached to.
        kdb_cell: If not `None`, a KCell will be created from and existing
            KLayout Cell
        ports: Attach an existing [Ports][kfactory.kcell.Ports] object to the KCell,
            if `None` create an empty one.
        info: Info object to attach to the KCell.
        settings: KCellSettings object to attach to the KCell.
    """
    super().__init__(
        base=base,
        name=name,
        kcl=kcl,
        kdb_cell=kdb_cell,
        ports=ports,
        info=info,
        settings=settings,
        pins=pins,
    )

__lshift__

__lshift__(cell: AnyTKCell) -> Instance

Convenience function for KCell.create_inst.

Parameters:

Name Type Description Default
cell AnyTKCell

The cell to be added as an instance

required
Source code in kfactory/kcell.py
3079
3080
3081
3082
3083
3084
3085
def __lshift__(self, cell: AnyTKCell) -> Instance:
    """Convenience function for `KCell.create_inst`.

    Args:
        cell: The cell to be added as an instance
    """
    return self.create_inst(cell)

add_port

add_port(
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> Port

Create a port in the cell.

Source code in kfactory/kcell.py
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
def add_port(
    self,
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> Port:
    """Create a port in the cell."""
    if self.locked:
        raise LockedError(self)

    return self.ports.add_port(
        port=port,
        name=name,
        keep_mirror=keep_mirror,
    )

create_pin

create_pin(
    *,
    ports: Iterable[ProtoPort[Any]],
    name: str | None = None,
    pin_type: str = "DC",
    info: dict[str, int | float | str] | None = None,
) -> Pin

Create a pin in the cell.

Source code in kfactory/kcell.py
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
def create_pin(
    self,
    *,
    ports: Iterable[ProtoPort[Any]],
    name: str | None = None,
    pin_type: str = "DC",
    info: dict[str, int | float | str] | None = None,
) -> Pin:
    """Create a pin in the cell."""
    return self.pins.create_pin(
        name=name, ports=ports, pin_type=pin_type, info=info
    )

from_yaml classmethod

from_yaml(
    constructor: SafeConstructor,
    node: Any,
    verbose: bool = False,
) -> Self

Internal function used by the placer to convert yaml to a KCell.

Source code in kfactory/kcell.py
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
@classmethod
def from_yaml(
    cls,
    constructor: SafeConstructor,
    node: Any,
    verbose: bool = False,
) -> Self:
    """Internal function used by the placer to convert yaml to a KCell."""
    d = SafeConstructor.construct_mapping(
        constructor,
        node,
        deep=True,
    )
    cell = cls(name=d["name"])
    if verbose:
        logger.info(f"Building {d['name']}")
    for _d in d.get("ports", Ports(ports=[], kcl=cell.kcl)):
        layer_as_string = (
            str(_d["layer"]).replace("[", "").replace("]", "").replace(", ", "/")
        )
        if "dcplx_trans" in _d:
            cell.create_port(
                name=str(_d["name"]),
                dcplx_trans=kdb.DCplxTrans.from_s(_d["dcplx_trans"]),
                width=_d["dwidth"],
                layer=cell.kcl.layer(kdb.LayerInfo.from_string(layer_as_string)),
                port_type=_d["port_type"],
            )
        else:
            cell.create_port(
                name=str(_d["name"]),
                trans=kdb.Trans.from_s(_d["trans"]),
                width=int(_d["width"]),
                layer=cell.kcl.layer(kdb.LayerInfo.from_string(layer_as_string)),
                port_type=_d["port_type"],
            )
    cell.settings = KCellSettings(
        **{
            name: deserialize_setting(setting)
            for name, setting in d.get("settings", {}).items()
        }
    )
    cell.info = Info(
        **{
            name: deserialize_setting(setting)
            for name, setting in d.get("info", {}).items()
        }
    )
    for inst in d.get("insts", []):
        if "cellname" in inst:
            cell_ = cell.kcl[inst["cellname"]]
        elif "cellfunction" in inst:
            module_name, fname = inst["cellfunction"].rsplit(".", 1)
            module = importlib.import_module(module_name)
            cellf = getattr(module, fname)
            cell_ = cellf(**inst["settings"])
            del module
        else:
            raise NotImplementedError(
                'To define an instance, either a "cellfunction" or'
                ' a "cellname" needs to be defined'
            )
        t = inst.get("trans", {})
        if isinstance(t, str):
            cell.create_inst(
                cell_,
                kdb.Trans.from_s(inst["trans"]),
            )
        else:
            angle = t.get("angle", 0)
            mirror = t.get("mirror", False)

            kinst = cell.create_inst(
                cell_,
                kdb.Trans(angle, mirror, 0, 0),
            )

            x0_yml = t.get("x0", DEFAULT_TRANS["x0"])
            y0_yml = t.get("y0", DEFAULT_TRANS["y0"])
            x_yml = t.get("x", DEFAULT_TRANS["x"])
            y_yml = t.get("y", DEFAULT_TRANS["y"])
            margin = t.get("margin", DEFAULT_TRANS["margin"])
            margin_x = margin.get(
                "x",
                DEFAULT_TRANS["margin"]["x"],  # type: ignore[index]
            )
            margin_y = margin.get(
                "y",
                DEFAULT_TRANS["margin"]["y"],  # type: ignore[index]
            )
            margin_x0 = margin.get(
                "x0",
                DEFAULT_TRANS["margin"]["x0"],  # type: ignore[index]
            )
            margin_y0 = margin.get(
                "y0",
                DEFAULT_TRANS["margin"]["y0"],  # type: ignore[index]
            )
            ref_yml = t.get("ref", DEFAULT_TRANS["ref"])
            if isinstance(ref_yml, str):
                i: Instance
                for i in reversed(cell.insts):
                    if i.cell.name == ref_yml:
                        ref = i
                        break
                else:
                    raise IndexError(
                        f"No instance with cell name: <{ref_yml}> found"
                    )
            elif isinstance(ref_yml, int) and len(cell.insts) > 1:
                ref = cell.insts[ref_yml]

            # margins for x0/y0 need to be in with opposite sign of
            # x/y due to them being subtracted later

            # x0
            match x0_yml:
                case "W":
                    x0 = kinst.bbox().left - margin_x0
                case "E":
                    x0 = kinst.bbox().right + margin_x0
                case _:
                    if isinstance(x0_yml, int):
                        x0 = x0_yml
                    else:
                        raise NotImplementedError("unknown format for x0")
            # y0
            match y0_yml:
                case "S":
                    y0 = kinst.bbox().bottom - margin_y0
                case "N":
                    y0 = kinst.bbox().top + margin_y0
                case _:
                    if isinstance(y0_yml, int):
                        y0 = y0_yml
                    else:
                        raise NotImplementedError("unknown format for y0")
            # x
            match x_yml:
                case "W":
                    if len(cell.insts) > 1:
                        x = ref.bbox().left
                        if x_yml != x0_yml:
                            x -= margin_x
                    else:
                        x = margin_x
                case "E":
                    if len(cell.insts) > 1:
                        x = ref.bbox().right
                        if x_yml != x0_yml:
                            x += margin_x
                    else:
                        x = margin_x
                case _:
                    if isinstance(x_yml, int):
                        x = x_yml
                    else:
                        raise NotImplementedError("unknown format for x")
            # y
            match y_yml:
                case "S":
                    if len(cell.insts) > 1:
                        y = ref.bbox().bottom
                        if y_yml != y0_yml:
                            y -= margin_y
                    else:
                        y = margin_y
                case "N":
                    if len(cell.insts) > 1:
                        y = ref.bbox().top
                        if y_yml != y0_yml:
                            y += margin_y
                    else:
                        y = margin_y
                case _:
                    if isinstance(y_yml, int):
                        y = y_yml
                    else:
                        raise NotImplementedError("unknown format for y")
            kinst.transform(kdb.Trans(0, False, x - x0, y - y0))
    type_to_class: dict[
        str,
        Callable[
            [str],
            kdb.Box
            | kdb.DBox
            | kdb.Polygon
            | kdb.DPolygon
            | kdb.Edge
            | kdb.DEdge
            | kdb.Text
            | kdb.DText,
        ],
    ] = {
        "box": kdb.Box.from_s,
        "polygon": kdb.Polygon.from_s,
        "edge": kdb.Edge.from_s,
        "text": kdb.Text.from_s,
        "dbox": kdb.DBox.from_s,
        "dpolygon": kdb.DPolygon.from_s,
        "dedge": kdb.DEdge.from_s,
        "dtext": kdb.DText.from_s,
    }

    for layer, shapes in dict(d.get("shapes", {})).items():
        linfo = kdb.LayerInfo.from_string(layer)
        for shape in shapes:
            shapetype, shapestring = shape.split(" ", 1)
            cell.shapes(cell.layout().layer(linfo)).insert(
                type_to_class[shapetype](shapestring)
            )

    return cell

to_yaml classmethod

to_yaml(
    representer: BaseRepresenter, node: Self
) -> MappingNode

Internal function to convert the cell to yaml.

Source code in kfactory/kcell.py
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
@classmethod
def to_yaml(cls, representer: BaseRepresenter, node: Self) -> MappingNode:
    """Internal function to convert the cell to yaml."""
    d: dict[str, Any] = {"name": node.name}

    insts = [
        {"cellname": inst.cell.name, "trans": inst.instance.trans.to_s()}
        for inst in node.insts
    ]
    shapes = {
        node.layout().get_info(layer).to_s(): [
            shape.to_s() for shape in node.shapes(layer).each()
        ]
        for layer in node.layout().layer_indexes()
        if not node.shapes(layer).is_empty()
    }
    ports: list[dict[str, Any]] = []
    for port in node.ports:
        l_ = node.kcl.get_info(port.layer)
        p: dict[str, Any] = {
            "name": port.name,
            "layer": [l_.layer, l_.datatype],
            "port_type": port.port_type,
        }
        if port.base.trans:
            p["trans"] = port.base.trans.to_s()
            p["width"] = port.width
        else:
            assert port.base.dcplx_trans is not None
            p["dcplx_trans"] = port.base.dcplx_trans.to_s()
            p["dwidth"] = port.dwidth
        p["info"] = {
            name: serialize_setting(setting)
            for name, setting in node.info.model_dump().items()
        }
        ports.append(p)

    d["ports"] = ports

    if insts:
        d["insts"] = insts
    if shapes:
        d["shapes"] = shapes
    d["settings"] = {
        name: serialize_setting(setting)
        for name, setting in node.settings.model_dump().items()
    }
    d["info"] = {
        name: serialize_setting(info)
        for name, info in node.info.model_dump().items()
    }
    return representer.represent_mapping(cls.yaml_tag, d)

KCellEnclosure pydantic-model

Bases: BaseModel

Collection of enclosures for cells.

Fields:

Source code in kfactory/enclosure.py
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
class KCellEnclosure(BaseModel):
    """Collection of [enclosures][kfactory.enclosure.LayerEnclosure] for cells."""

    enclosures: LayerEnclosureCollection

    def __init__(self, enclosures: Iterable[LayerEnclosure]) -> None:
        """Init. Allow usage of an iterable object instead of a collection."""
        super().__init__(
            enclosures=LayerEnclosureCollection(enclosures=list(enclosures))
        )

    def __hash__(self) -> int:
        """Hash of the KCellEnclosure."""
        return hash(tuple(self.enclosures.enclosures))

    def minkowski_region(
        self,
        r: kdb.Region,
        d: int | None,
        shape: Callable[[int], list[kdb.Point] | kdb.Box | kdb.Edge | kdb.Polygon],
    ) -> kdb.Region:
        """Calculaste a region from a minkowski sum.

        If the distance is negative, the function will take the inverse region and apply
        the minkowski and take the inverse again.

        Args:
            r: Target region.
            d: Distance to pass to the shape. Can be any integer. [dbu]
            shape: Function returning a shape for the minkowski region.
        """
        if d is None:
            return kdb.Region()
        if d == 0:
            return r.dup()
        if d > 0:
            return r.minkowski_sum(shape(d))
        shape_ = shape(abs(d))
        if isinstance(shape_, list):
            box_shape = kdb.Polygon(shape_)
            bbox_maxsize = max(
                box_shape.bbox().width(),
                box_shape.bbox().height(),
            )
        else:
            bbox_maxsize = max(
                shape_.bbox().width(),
                shape_.bbox().height(),
            )
        bbox_r = kdb.Region(r.bbox().enlarged(bbox_maxsize))
        return r - (bbox_r - r).minkowski_sum(shape_)

    def apply_minkowski_enc(
        self,
        c: KCell,
        direction: Direction = Direction.BOTH,
    ) -> None:
        """Apply an enclosure with a vector in y-direction.

        This can be used for tapers/
        waveguides or similar that are straight.

        Args:
            c: Cell to apply the enclosure to.
            direction: X/Y or both directions, see [kfactory.enclosure.DIRECTION].
                Uses a box if both directions are selected.
        """
        match direction:
            case Direction.BOTH:

                def box(d: int) -> kdb.Box:
                    return kdb.Box(-d, -d, d, d)

                self.apply_minkowski_custom(c, shape=box)

            case Direction.Y:

                def edge(d: int) -> kdb.Edge:
                    return kdb.Edge(0, -d, 0, d)

                self.apply_minkowski_custom(c, shape=edge)

            case Direction.X:

                def edge(d: int) -> kdb.Edge:
                    return kdb.Edge(-d, 0, d, 0)

                self.apply_minkowski_custom(c, shape=edge)

            case _:
                raise ValueError("Undefined direction")

    def apply_minkowski_y(self, c: KCell) -> None:
        """Apply an enclosure with a vector in y-direction.

        This can be used for tapers/
        waveguides or similar that are straight.

        Args:
            c: Cell to apply the enclosure to.
        """
        return self.apply_minkowski_enc(c, direction=Direction.Y)

    def apply_minkowski_x(self, c: KCell) -> None:
        """Apply an enclosure with a vector in x-direction.

        This can be used for tapers/
        waveguides or similar that are straight.

        Args:
            c: Cell to apply the enclosure to.
        """
        return self.apply_minkowski_enc(c, direction=Direction.X)

    def apply_minkowski_custom(
        self,
        c: KCell,
        shape: Callable[[int], kdb.Edge | kdb.Polygon | kdb.Box],
    ) -> None:
        """Apply an enclosure with a custom shape.

        This can be used for tapers/
        waveguides or similar that are straight.

        Args:
            c: Cell to apply the enclosure to.
            shape: A function that will return a shape which takes one argument
                the size of the section in dbu.
            shape: Reference to use as a base for the enclosure.
        """
        regions = {}
        for enc in self.enclosures.enclosures:
            main_layer = c.kcl.layer(enc.main_layer)
            if not c.bbox(main_layer).empty():
                rsi = c.begin_shapes_rec(main_layer)
                r = kdb.Region(rsi)
                for layer, layersec in enc.layer_sections.items():
                    if layer not in regions:
                        reg = kdb.Region()
                        regions[layer] = reg
                    else:
                        reg = regions[layer]
                    for section in layersec.sections:
                        reg += self.minkowski_region(
                            r, section.d_max, shape
                        ) - self.minkowski_region(r, section.d_min, shape)

                        reg.merge()

        for layer, region in regions.items():
            c.shapes(c.kcl.layer(layer)).insert(region)

    def apply_minkowski_tiled(
        self,
        c: KCell,
        tile_size: float | None = None,
        n_pts: int = 64,
        n_threads: int | None = None,
        carve_out_ports: bool = True,
    ) -> None:
        """Minkowski regions with tiling processor.

        Useful if the target is a big or complicated enclosure. Will split target ref
        into tiles and calculate them in parallel. Uses a circle as a shape for the
        minkowski sum.

        Args:
            c: Target KCell to apply the enclosures into.
            tile_size: Tile size. This should be in the order off 10+ maximum size
                of the maximum size of sections. [um]
                If None is set, the minimum size is set to 10xmax(d_max) of all sections
                or 200um whichever is bigger.
            n_pts: Number of points in the circle. < 3 will create a triangle. 4 a
                diamond, etc.
            n_threads: Number o threads to use. By default (`None`) it will use as many
                threads as are set to the process (usually all cores of the machine).
            carve_out_ports: Carves out a box of port_width +
        """
        tp = kdb.TilingProcessor()
        tp.frame = c.dbbox()  # type: ignore[misc, assignment]
        tp.dbu = c.kcl.dbu
        tp.threads = n_threads or config.n_threads
        inputs: set[str] = set()
        port_hole_map: dict[kdb.LayerInfo, kdb.Region] = defaultdict(kdb.Region)
        ports_by_layer: dict[kdb.LayerInfo, list[Port]] = defaultdict(list)
        for port in c.ports:
            ports_by_layer[c.kcl.layer(c.kcl.get_info(port.layer))].append(port)

        maxsize = 0
        for enc in self.enclosures.enclosures:
            assert enc.main_layer is not None
            main_layer = c.kcl.layer(enc.main_layer)
            for layer, layersection in enc.layer_sections.items():
                li = c.kcl.layer(layer)
                size = layersection.sections[-1].d_max
                maxsize = max(maxsize, size)

                for port in ports_by_layer[main_layer]:
                    if port._base.trans:
                        port_hole_map[li].insert(
                            port_hole(port.width, size).transformed(port.trans)
                        )
                    else:
                        port_hole_map[li].insert(
                            port_hole(port.width, size).transformed(
                                kdb.ICplxTrans(port.dcplx_trans, port.kcl.dbu)
                            )
                        )

        min_tile_size_rec = 10 * maxsize * tp.dbu

        if tile_size is None:
            tile_size = max(min_tile_size_rec * 2, 200)

        if float(tile_size) <= min_tile_size_rec:
            logger.warning(
                "Tile size should be larger than the maximum of "
                "the enclosures (recommendation: {} / {})",
                tile_size,
                min_tile_size_rec,
            )
        tp.tile_border(maxsize * tp.dbu, maxsize * tp.dbu)
        tp.tile_size(tile_size, tile_size)
        layer_regiontilesoperators: dict[
            tuple[int, LayerSection], RegionTilesOperator
        ] = {}

        logger.debug("Starting KCellEnclosure on {}", c.kcl.future_cell_name or c.name)

        n_enc = len(self.enclosures.enclosures)

        for i, enc in enumerate(self.enclosures.enclosures):
            assert enc.main_layer is not None
            if not c.bbox(c.kcl.layer(enc.main_layer)).empty():
                main_layer = c.kcl.layer(enc.main_layer)
                inp = f"main_layer_{main_layer}"
                if enc.main_layer not in inputs:
                    tp.input(
                        inp,
                        c.kcl.layout,
                        c.cell_index(),
                        main_layer,
                    )
                    inputs.add(main_layer)
                    logger.debug("Created input {}", inp)

                for layer, layer_section in enc.layer_sections.items():
                    li = c.kcl.layer(layer)
                    if (main_layer, layer_section) in layer_regiontilesoperators:
                        layer_regiontilesoperators[
                            main_layer, layer_section
                        ].layers.append(li)
                    else:
                        out = f"target_{li}"
                        operator = RegionTilesOperator(cell=c, layers=[li])
                        layer_regiontilesoperators[main_layer, layer_section] = operator
                        tp.output(out, operator)
                        logger.debug("Created output {}", out)

                    for section in reversed(layer_section.sections):
                        queue_str = (
                            "var max_shape = Polygon.ellipse("
                            f"Box.new({section.d_max * 2},{section.d_max * 2}),"
                            f" {n_pts});"
                            f"var tile_reg = _tile & _frame.sized({maxsize});"
                        )
                        match section.d_max:
                            case d if d > 0:
                                max_region = (
                                    "var max_reg = "
                                    f"{inp}.minkowski_sum(max_shape).merged();"
                                )
                            case d if d < 0:
                                max_region = (
                                    f"var max_reg = tile_reg - (tile_reg - {inp});"
                                )
                            case 0:
                                max_region = f"var max_reg = {inp} & tile_reg;"
                        queue_str += max_region
                        if section.d_min is not None:
                            queue_str += (
                                "var min_shape = Polygon.ellipse("
                                f"Box.new({section.d_min * 2},{section.d_min * 2}),"
                                " 64);"
                            )
                            match section.d_min:
                                case d if d > 0:
                                    min_region = (
                                        f"var min_reg = {inp}.minkowski_sum(min_shape);"
                                    )
                                case d if d < 0:
                                    min_region = (
                                        "var min_reg = tile_reg - (tile_reg - "
                                        f"{inp}).minkowski_sum(min_shape);"
                                    )
                                case 0:
                                    min_region = f"var min_reg = {inp} & tile_reg;"
                            queue_str += min_region
                            queue_str += (
                                f"_output({out},(max_reg - min_reg) & _tile, true);"
                            )
                        else:
                            queue_str += f"_output({out}, max_reg & _tile, true);"

                        logger.debug(
                            "{}/{}: Queuing string for {} on layer {}: '{}'",
                            i + 1,
                            n_enc,
                            c.kcl.future_cell_name or c.name,
                            layer,
                            queue_str,
                        )
                        tp.queue(queue_str)

        c.kcl.start_changes()
        logger.debug(
            "Starting enclosure {}",
            c.kcl.future_cell_name or c.name,
            enc.name,
        )
        tp.execute(f"Minkowski {c.name}")
        c.kcl.end_changes()
        logger.debug("Finished enclosure {}", enc.name)

        if carve_out_ports:
            for operator in layer_regiontilesoperators.values():
                # for layer in operator.layers:
                operator.insert(port_hole_map=port_hole_map)
        else:
            for operator in layer_regiontilesoperators.values():
                operator.insert()
        logger.debug("Finished KCellEnclosure on {}", c.kcl.future_cell_name or c.name)

__hash__

__hash__() -> int

Hash of the KCellEnclosure.

Source code in kfactory/enclosure.py
1286
1287
1288
def __hash__(self) -> int:
    """Hash of the KCellEnclosure."""
    return hash(tuple(self.enclosures.enclosures))

__init__

__init__(enclosures: Iterable[LayerEnclosure]) -> None

Init. Allow usage of an iterable object instead of a collection.

Source code in kfactory/enclosure.py
1280
1281
1282
1283
1284
def __init__(self, enclosures: Iterable[LayerEnclosure]) -> None:
    """Init. Allow usage of an iterable object instead of a collection."""
    super().__init__(
        enclosures=LayerEnclosureCollection(enclosures=list(enclosures))
    )

apply_minkowski_custom

apply_minkowski_custom(
    c: KCell, shape: Callable[[int], Edge | Polygon | Box]
) -> None

Apply an enclosure with a custom shape.

This can be used for tapers/ waveguides or similar that are straight.

Parameters:

Name Type Description Default
c KCell

Cell to apply the enclosure to.

required
shape Callable[[int], Edge | Polygon | Box]

A function that will return a shape which takes one argument the size of the section in dbu.

required
shape Callable[[int], Edge | Polygon | Box]

Reference to use as a base for the enclosure.

required
Source code in kfactory/enclosure.py
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
def apply_minkowski_custom(
    self,
    c: KCell,
    shape: Callable[[int], kdb.Edge | kdb.Polygon | kdb.Box],
) -> None:
    """Apply an enclosure with a custom shape.

    This can be used for tapers/
    waveguides or similar that are straight.

    Args:
        c: Cell to apply the enclosure to.
        shape: A function that will return a shape which takes one argument
            the size of the section in dbu.
        shape: Reference to use as a base for the enclosure.
    """
    regions = {}
    for enc in self.enclosures.enclosures:
        main_layer = c.kcl.layer(enc.main_layer)
        if not c.bbox(main_layer).empty():
            rsi = c.begin_shapes_rec(main_layer)
            r = kdb.Region(rsi)
            for layer, layersec in enc.layer_sections.items():
                if layer not in regions:
                    reg = kdb.Region()
                    regions[layer] = reg
                else:
                    reg = regions[layer]
                for section in layersec.sections:
                    reg += self.minkowski_region(
                        r, section.d_max, shape
                    ) - self.minkowski_region(r, section.d_min, shape)

                    reg.merge()

    for layer, region in regions.items():
        c.shapes(c.kcl.layer(layer)).insert(region)

apply_minkowski_enc

apply_minkowski_enc(
    c: KCell, direction: Direction = Direction.BOTH
) -> None

Apply an enclosure with a vector in y-direction.

This can be used for tapers/ waveguides or similar that are straight.

Parameters:

Name Type Description Default
c KCell

Cell to apply the enclosure to.

required
direction Direction

X/Y or both directions, see [kfactory.enclosure.DIRECTION]. Uses a box if both directions are selected.

BOTH
Source code in kfactory/enclosure.py
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
def apply_minkowski_enc(
    self,
    c: KCell,
    direction: Direction = Direction.BOTH,
) -> None:
    """Apply an enclosure with a vector in y-direction.

    This can be used for tapers/
    waveguides or similar that are straight.

    Args:
        c: Cell to apply the enclosure to.
        direction: X/Y or both directions, see [kfactory.enclosure.DIRECTION].
            Uses a box if both directions are selected.
    """
    match direction:
        case Direction.BOTH:

            def box(d: int) -> kdb.Box:
                return kdb.Box(-d, -d, d, d)

            self.apply_minkowski_custom(c, shape=box)

        case Direction.Y:

            def edge(d: int) -> kdb.Edge:
                return kdb.Edge(0, -d, 0, d)

            self.apply_minkowski_custom(c, shape=edge)

        case Direction.X:

            def edge(d: int) -> kdb.Edge:
                return kdb.Edge(-d, 0, d, 0)

            self.apply_minkowski_custom(c, shape=edge)

        case _:
            raise ValueError("Undefined direction")

apply_minkowski_tiled

apply_minkowski_tiled(
    c: KCell,
    tile_size: float | None = None,
    n_pts: int = 64,
    n_threads: int | None = None,
    carve_out_ports: bool = True,
) -> None

Minkowski regions with tiling processor.

Useful if the target is a big or complicated enclosure. Will split target ref into tiles and calculate them in parallel. Uses a circle as a shape for the minkowski sum.

Parameters:

Name Type Description Default
c KCell

Target KCell to apply the enclosures into.

required
tile_size float | None

Tile size. This should be in the order off 10+ maximum size of the maximum size of sections. [um] If None is set, the minimum size is set to 10xmax(d_max) of all sections or 200um whichever is bigger.

None
n_pts int

Number of points in the circle. < 3 will create a triangle. 4 a diamond, etc.

64
n_threads int | None

Number o threads to use. By default (None) it will use as many threads as are set to the process (usually all cores of the machine).

None
carve_out_ports bool

Carves out a box of port_width +

True
Source code in kfactory/enclosure.py
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
def apply_minkowski_tiled(
    self,
    c: KCell,
    tile_size: float | None = None,
    n_pts: int = 64,
    n_threads: int | None = None,
    carve_out_ports: bool = True,
) -> None:
    """Minkowski regions with tiling processor.

    Useful if the target is a big or complicated enclosure. Will split target ref
    into tiles and calculate them in parallel. Uses a circle as a shape for the
    minkowski sum.

    Args:
        c: Target KCell to apply the enclosures into.
        tile_size: Tile size. This should be in the order off 10+ maximum size
            of the maximum size of sections. [um]
            If None is set, the minimum size is set to 10xmax(d_max) of all sections
            or 200um whichever is bigger.
        n_pts: Number of points in the circle. < 3 will create a triangle. 4 a
            diamond, etc.
        n_threads: Number o threads to use. By default (`None`) it will use as many
            threads as are set to the process (usually all cores of the machine).
        carve_out_ports: Carves out a box of port_width +
    """
    tp = kdb.TilingProcessor()
    tp.frame = c.dbbox()  # type: ignore[misc, assignment]
    tp.dbu = c.kcl.dbu
    tp.threads = n_threads or config.n_threads
    inputs: set[str] = set()
    port_hole_map: dict[kdb.LayerInfo, kdb.Region] = defaultdict(kdb.Region)
    ports_by_layer: dict[kdb.LayerInfo, list[Port]] = defaultdict(list)
    for port in c.ports:
        ports_by_layer[c.kcl.layer(c.kcl.get_info(port.layer))].append(port)

    maxsize = 0
    for enc in self.enclosures.enclosures:
        assert enc.main_layer is not None
        main_layer = c.kcl.layer(enc.main_layer)
        for layer, layersection in enc.layer_sections.items():
            li = c.kcl.layer(layer)
            size = layersection.sections[-1].d_max
            maxsize = max(maxsize, size)

            for port in ports_by_layer[main_layer]:
                if port._base.trans:
                    port_hole_map[li].insert(
                        port_hole(port.width, size).transformed(port.trans)
                    )
                else:
                    port_hole_map[li].insert(
                        port_hole(port.width, size).transformed(
                            kdb.ICplxTrans(port.dcplx_trans, port.kcl.dbu)
                        )
                    )

    min_tile_size_rec = 10 * maxsize * tp.dbu

    if tile_size is None:
        tile_size = max(min_tile_size_rec * 2, 200)

    if float(tile_size) <= min_tile_size_rec:
        logger.warning(
            "Tile size should be larger than the maximum of "
            "the enclosures (recommendation: {} / {})",
            tile_size,
            min_tile_size_rec,
        )
    tp.tile_border(maxsize * tp.dbu, maxsize * tp.dbu)
    tp.tile_size(tile_size, tile_size)
    layer_regiontilesoperators: dict[
        tuple[int, LayerSection], RegionTilesOperator
    ] = {}

    logger.debug("Starting KCellEnclosure on {}", c.kcl.future_cell_name or c.name)

    n_enc = len(self.enclosures.enclosures)

    for i, enc in enumerate(self.enclosures.enclosures):
        assert enc.main_layer is not None
        if not c.bbox(c.kcl.layer(enc.main_layer)).empty():
            main_layer = c.kcl.layer(enc.main_layer)
            inp = f"main_layer_{main_layer}"
            if enc.main_layer not in inputs:
                tp.input(
                    inp,
                    c.kcl.layout,
                    c.cell_index(),
                    main_layer,
                )
                inputs.add(main_layer)
                logger.debug("Created input {}", inp)

            for layer, layer_section in enc.layer_sections.items():
                li = c.kcl.layer(layer)
                if (main_layer, layer_section) in layer_regiontilesoperators:
                    layer_regiontilesoperators[
                        main_layer, layer_section
                    ].layers.append(li)
                else:
                    out = f"target_{li}"
                    operator = RegionTilesOperator(cell=c, layers=[li])
                    layer_regiontilesoperators[main_layer, layer_section] = operator
                    tp.output(out, operator)
                    logger.debug("Created output {}", out)

                for section in reversed(layer_section.sections):
                    queue_str = (
                        "var max_shape = Polygon.ellipse("
                        f"Box.new({section.d_max * 2},{section.d_max * 2}),"
                        f" {n_pts});"
                        f"var tile_reg = _tile & _frame.sized({maxsize});"
                    )
                    match section.d_max:
                        case d if d > 0:
                            max_region = (
                                "var max_reg = "
                                f"{inp}.minkowski_sum(max_shape).merged();"
                            )
                        case d if d < 0:
                            max_region = (
                                f"var max_reg = tile_reg - (tile_reg - {inp});"
                            )
                        case 0:
                            max_region = f"var max_reg = {inp} & tile_reg;"
                    queue_str += max_region
                    if section.d_min is not None:
                        queue_str += (
                            "var min_shape = Polygon.ellipse("
                            f"Box.new({section.d_min * 2},{section.d_min * 2}),"
                            " 64);"
                        )
                        match section.d_min:
                            case d if d > 0:
                                min_region = (
                                    f"var min_reg = {inp}.minkowski_sum(min_shape);"
                                )
                            case d if d < 0:
                                min_region = (
                                    "var min_reg = tile_reg - (tile_reg - "
                                    f"{inp}).minkowski_sum(min_shape);"
                                )
                            case 0:
                                min_region = f"var min_reg = {inp} & tile_reg;"
                        queue_str += min_region
                        queue_str += (
                            f"_output({out},(max_reg - min_reg) & _tile, true);"
                        )
                    else:
                        queue_str += f"_output({out}, max_reg & _tile, true);"

                    logger.debug(
                        "{}/{}: Queuing string for {} on layer {}: '{}'",
                        i + 1,
                        n_enc,
                        c.kcl.future_cell_name or c.name,
                        layer,
                        queue_str,
                    )
                    tp.queue(queue_str)

    c.kcl.start_changes()
    logger.debug(
        "Starting enclosure {}",
        c.kcl.future_cell_name or c.name,
        enc.name,
    )
    tp.execute(f"Minkowski {c.name}")
    c.kcl.end_changes()
    logger.debug("Finished enclosure {}", enc.name)

    if carve_out_ports:
        for operator in layer_regiontilesoperators.values():
            # for layer in operator.layers:
            operator.insert(port_hole_map=port_hole_map)
    else:
        for operator in layer_regiontilesoperators.values():
            operator.insert()
    logger.debug("Finished KCellEnclosure on {}", c.kcl.future_cell_name or c.name)

apply_minkowski_x

apply_minkowski_x(c: KCell) -> None

Apply an enclosure with a vector in x-direction.

This can be used for tapers/ waveguides or similar that are straight.

Parameters:

Name Type Description Default
c KCell

Cell to apply the enclosure to.

required
Source code in kfactory/enclosure.py
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
def apply_minkowski_x(self, c: KCell) -> None:
    """Apply an enclosure with a vector in x-direction.

    This can be used for tapers/
    waveguides or similar that are straight.

    Args:
        c: Cell to apply the enclosure to.
    """
    return self.apply_minkowski_enc(c, direction=Direction.X)

apply_minkowski_y

apply_minkowski_y(c: KCell) -> None

Apply an enclosure with a vector in y-direction.

This can be used for tapers/ waveguides or similar that are straight.

Parameters:

Name Type Description Default
c KCell

Cell to apply the enclosure to.

required
Source code in kfactory/enclosure.py
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
def apply_minkowski_y(self, c: KCell) -> None:
    """Apply an enclosure with a vector in y-direction.

    This can be used for tapers/
    waveguides or similar that are straight.

    Args:
        c: Cell to apply the enclosure to.
    """
    return self.apply_minkowski_enc(c, direction=Direction.Y)

minkowski_region

minkowski_region(
    r: Region,
    d: int | None,
    shape: Callable[
        [int], list[Point] | Box | Edge | Polygon
    ],
) -> kdb.Region

Calculaste a region from a minkowski sum.

If the distance is negative, the function will take the inverse region and apply the minkowski and take the inverse again.

Parameters:

Name Type Description Default
r Region

Target region.

required
d int | None

Distance to pass to the shape. Can be any integer. [dbu]

required
shape Callable[[int], list[Point] | Box | Edge | Polygon]

Function returning a shape for the minkowski region.

required
Source code in kfactory/enclosure.py
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
def minkowski_region(
    self,
    r: kdb.Region,
    d: int | None,
    shape: Callable[[int], list[kdb.Point] | kdb.Box | kdb.Edge | kdb.Polygon],
) -> kdb.Region:
    """Calculaste a region from a minkowski sum.

    If the distance is negative, the function will take the inverse region and apply
    the minkowski and take the inverse again.

    Args:
        r: Target region.
        d: Distance to pass to the shape. Can be any integer. [dbu]
        shape: Function returning a shape for the minkowski region.
    """
    if d is None:
        return kdb.Region()
    if d == 0:
        return r.dup()
    if d > 0:
        return r.minkowski_sum(shape(d))
    shape_ = shape(abs(d))
    if isinstance(shape_, list):
        box_shape = kdb.Polygon(shape_)
        bbox_maxsize = max(
            box_shape.bbox().width(),
            box_shape.bbox().height(),
        )
    else:
        bbox_maxsize = max(
            shape_.bbox().width(),
            shape_.bbox().height(),
        )
    bbox_r = kdb.Region(r.bbox().enlarged(bbox_maxsize))
    return r - (bbox_r - r).minkowski_sum(shape_)

KCellSettings pydantic-model

Bases: SettingMixin, BaseModel

Settings for a BaseKCell.

Validators:

Source code in kfactory/settings.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class KCellSettings(
    SettingMixin, BaseModel, extra="allow", validate_assignment=True, frozen=True
):
    """Settings for a BaseKCell."""

    def __init__(self, **kwargs: Any) -> None:
        """Initialize the settings."""
        super().__init__(**kwargs)

    @model_validator(mode="before")
    @classmethod
    def restrict_types(cls, data: dict[str, Any]) -> dict[str, MetaData]:
        """Restrict the types of the settings."""
        for name, value in data.items():
            data[name] = convert_metadata_type(value)
        return data

__init__

__init__(**kwargs: Any) -> None

Initialize the settings.

Source code in kfactory/settings.py
44
45
46
def __init__(self, **kwargs: Any) -> None:
    """Initialize the settings."""
    super().__init__(**kwargs)

restrict_types pydantic-validator

restrict_types(data: dict[str, Any]) -> dict[str, MetaData]

Restrict the types of the settings.

Source code in kfactory/settings.py
48
49
50
51
52
53
54
@model_validator(mode="before")
@classmethod
def restrict_types(cls, data: dict[str, Any]) -> dict[str, MetaData]:
    """Restrict the types of the settings."""
    for name, value in data.items():
        data[name] = convert_metadata_type(value)
    return data

LayerEnclosure pydantic-model

Bases: BaseModel

Definitions for calculation of enclosing (or smaller) shapes of a reference.

Attributes:

Name Type Description
layer_sections dict[LayerInfo, LayerSection]

Mapping of layers to their layer sections.

main_layer LayerInfo | None

Layer which to use unless specified otherwise.

Fields:

  • layer_sections (dict[LayerInfo, LayerSection])
  • _name (str | None)
  • main_layer (LayerInfo | None)
  • bbox_sections (dict[LayerInfo, int])
Source code in kfactory/enclosure.py
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
class LayerEnclosure(BaseModel, arbitrary_types_allowed=True, frozen=True):
    """Definitions for calculation of enclosing (or smaller) shapes of a reference.

    Attributes:
        layer_sections: Mapping of layers to their layer sections.
        main_layer: Layer which to use unless specified otherwise.
    """

    layer_sections: dict[kdb.LayerInfo, LayerSection]
    _name: str | None = PrivateAttr()
    main_layer: kdb.LayerInfo | None
    bbox_sections: dict[kdb.LayerInfo, int]

    def __init__(
        self,
        sections: Sequence[
            tuple[kdb.LayerInfo, int] | tuple[kdb.LayerInfo, int, int]
        ] = [],
        name: str | None = None,
        main_layer: kdb.LayerInfo | None = None,
        dsections: Sequence[
            tuple[kdb.LayerInfo, float] | tuple[kdb.LayerInfo, float, float]
        ]
        | None = None,
        bbox_sections: Sequence[tuple[kdb.LayerInfo, int]] = [],
        kcl: KCLayout | None = None,
    ) -> None:
        """Constructor of new enclosure.

        Args:
            sections: tuples containing info for the enclosure.
                Elements must be of the form (layer, max) or (layer, min, max)
            name: Optional name of the enclosure. If a name is given in the
                cell name this name will be used for enclosure arguments.
            main_layer: Main layer used if the functions don't get an explicit layer.
            dsections: Same as sections but min/max defined in um
            kcl: `KCLayout` Used for conversion dbu -> um or when copying.
                Must be specified if `desections` is not `None`. Also necessary
                if copying to another layout and not all layers used are LayerInfos.
        """
        layer_sections: dict[kdb.LayerInfo, LayerSection] = {}

        if dsections is not None:
            assert kcl is not None, "If sections in um are defined, kcl must be set"
            sections = list(sections)
            for section in dsections:
                if len(section) == 2:  # noqa: PLR2004
                    sections.append((section[0], kcl.to_dbu(section[1])))

                elif len(section) == 3:  # noqa: PLR2004
                    sections.append(
                        (
                            section[0],
                            kcl.to_dbu(section[1]),
                            kcl.to_dbu(section[2]),
                        )
                    )

        for sec in sorted(
            sections,
            key=lambda sec: (sec[0].name, sec[0].layer, sec[0].datatype, sec[1]),
        ):
            if sec[0] in layer_sections:
                ls = layer_sections[sec[0]]
            else:
                ls = LayerSection()
                layer_sections[sec[0]] = ls
            ls.add_section(Section(d_max=sec[1])) if len(sec) < 3 else ls.add_section(  # noqa: PLR2004
                Section(d_max=sec[2], d_min=sec[1])
            )
        super().__init__(
            main_layer=main_layer,
            kcl=kcl,
            layer_sections=layer_sections,
            bbox_sections={t[0]: t[1] for t in bbox_sections},
        )
        self._name = name

    @model_serializer
    def _serialize(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "sections": [
                (layer, s.d_max) if s.d_min is None else (layer, s.d_min, s.d_max)
                for layer, sections in self.layer_sections.items()
                for s in sections.sections
            ],
            "main_layer": self.main_layer,
        }

    def __hash__(self) -> int:  # make hashable BaseModel subclass
        """Calculate a unique hash of the enclosure."""
        return hash((str(self), self.main_layer, tuple(self.layer_sections.items())))

    def to_dtype(self, kcl: KCLayout) -> DLayerEnclosure:
        """Convert the enclosure to a um based enclosure."""
        if self.main_layer is None:
            raise ValueError("um based enclosures must have a main_layer")
        return DLayerEnclosure(
            name=self._name,
            sections=[
                (layer, kcl.to_um(section.d_max))
                if section.d_min is None
                else (layer, kcl.to_um(section.d_min), kcl.to_um(section.d_max))
                for layer, layer_section in self.layer_sections.items()
                for section in layer_section.sections
            ],
            main_layer=self.main_layer,
        )

    @property
    def name(self) -> str:
        """Get name of the Enclosure."""
        return self.__str__()

    def minkowski_region(
        self,
        r: kdb.Region,
        d: int | None,
        shape: Callable[[int], list[kdb.Point] | kdb.Box | kdb.Edge | kdb.Polygon],
    ) -> kdb.Region:
        """Calculaste a region from a minkowski sum.

        If the distance is negative, the function will take the inverse region and apply
        the minkowski and take the inverse again.

        Args:
            r: Target region.
            d: Distance to pass to the shape. Can be any integer. [dbu]
            shape: Function returning a shape for the minkowski region.
        """
        if d is None:
            return kdb.Region()
        if d == 0:
            return r.dup()
        if d > 0:
            return r.minkowski_sum(shape(d))
        shape_ = shape(abs(d))
        if isinstance(shape_, list):
            box_shape = kdb.Polygon(shape_)
            bbox_maxsize = max(
                box_shape.bbox().width(),
                box_shape.bbox().height(),
            )
        else:
            bbox_maxsize = max(
                shape_.bbox().width(),
                shape_.bbox().height(),
            )
        bbox_r = kdb.Region(r.bbox().enlarged(bbox_maxsize))
        return r - (bbox_r - r).minkowski_sum(shape_)

    def apply_minkowski_enc(
        self,
        c: KCell,
        ref: kdb.LayerInfo | kdb.Region | None,  # layer index or the region
        direction: Direction = Direction.BOTH,
    ) -> None:
        """Apply an enclosure with a vector in y-direction.

        This can be used for tapers/
        waveguides or similar that are straight.

        Args:
            c: Cell to apply the enclosure to.
            ref: Reference to use as a base for the enclosure.
            direction: X/Y or both directions.
                Uses a box if both directions are selected.
        """
        match direction:
            case Direction.BOTH:

                def box(d: int) -> kdb.Box:
                    return kdb.Box(-d, -d, d, d)

                self.apply_minkowski_custom(c, ref=ref, shape=box)

            case Direction.Y:

                def edge(d: int) -> kdb.Edge:
                    return kdb.Edge(0, -d, 0, d)

                self.apply_minkowski_custom(c, ref=ref, shape=edge)

            case Direction.X:

                def edge(d: int) -> kdb.Edge:
                    return kdb.Edge(-d, 0, d, 0)

                self.apply_minkowski_custom(c, ref=ref, shape=edge)

            case _:
                raise ValueError("Undefined direction")

    def apply_minkowski_y(
        self, c: KCell, ref: kdb.LayerInfo | kdb.Region | None = None
    ) -> None:
        """Apply an enclosure with a vector in y-direction.

        This can be used for tapers/
        waveguides or similar that are straight.

        Args:
            c: Cell to apply the enclosure to.
            ref: Reference to use as a base for the enclosure.
        """
        return self.apply_minkowski_enc(c, ref=ref, direction=Direction.Y)

    def apply_minkowski_x(
        self, c: KCell, ref: kdb.LayerInfo | kdb.Region | None
    ) -> None:
        """Apply an enclosure with a vector in x-direction.

        This can be used for tapers/
        waveguides or similar that are straight.

        Args:
            c: Cell to apply the enclosure to.
            ref: Reference to use as a base for the enclosure.
        """
        return self.apply_minkowski_enc(c, ref=ref, direction=Direction.X)

    def apply_minkowski_custom(
        self,
        c: KCell,
        shape: Callable[[int], kdb.Edge | kdb.Polygon | kdb.Box],
        ref: kdb.LayerInfo | kdb.Region | None = None,
    ) -> None:
        """Apply an enclosure with a custom shape.

        This can be used for tapers/
        waveguides or similar that are straight.

        Args:
            c: Cell to apply the enclosure to.
            shape: A function that will return a shape which takes one argument
                the size of the section in dbu.
            ref: Reference to use as a base for the enclosure.
        """
        if ref is None:
            ref = self.main_layer

            if ref is None:
                raise ValueError(
                    "The enclosure doesn't have  a reference `main_layer` defined."
                    " Therefore the layer must be defined in calls"
                )
        r = (
            kdb.Region(c.begin_shapes_rec(c.kcl.layer(ref)))
            if isinstance(ref, kdb.LayerInfo)
            else ref.dup()
        )
        r.merge()

        for layer, layersec in reversed(self.layer_sections.items()):
            for section in layersec.sections:
                c.shapes(c.kcl.layer(layer)).insert(
                    self.minkowski_region(r, section.d_max, shape)
                    - self.minkowski_region(r, section.d_min, shape)
                )

    def apply_minkowski_tiled(
        self,
        c: KCell,
        ref: kdb.LayerInfo | kdb.Region | None = None,
        tile_size: float | None = None,
        n_pts: int = 64,
        n_threads: int | None = None,
        carve_out_ports: Iterable[Port] = [],
    ) -> None:
        """Minkowski regions with tiling processor.

        Useful if the target is a big or complicated enclosure. Will split target ref
        into tiles and calculate them in parallel. Uses a circle as a shape for the
        minkowski sum.

        Args:
            c: Target KCell to apply the enclosures into.
            ref: The reference shapes to apply the enclosures to.
                Can be a layer or a region. If `None`, it will try to use the
                `main_layer` of the
                [enclosure][kfactory.enclosure.LayerEnclosure].
            tile_size: Tile size. This should be in the order off 10+ maximum size
                of the maximum size of sections.
            n_pts: Number of points in the circle. < 3 will create a triangle. 4 a
                diamond, etc.
            n_threads: Number o threads to use. By default (`None`) it will use as many
                threads as are set to the process (usually all cores of the machine).
            carve_out_ports: Carves out a box of port_width +
        """
        if ref is None:
            ref = self.main_layer

            if ref is None:
                raise ValueError(
                    "The enclosure doesn't have  a reference `main_layer` defined."
                    " Therefore the layer must be defined in calls"
                )
        tp = kdb.TilingProcessor()
        tp.frame = c.dbbox()  # type: ignore[misc, assignment]
        tp.dbu = c.kcl.dbu
        tp.threads = n_threads or config.n_threads
        maxsize = 0
        for layersection in self.layer_sections.values():
            maxsize = max(
                maxsize, *[section.d_max for section in layersection.sections]
            )

        min_tile_size_rec = 10 * maxsize * tp.dbu

        if tile_size is None:
            tile_size = min_tile_size_rec * 2

        if float(tile_size) <= min_tile_size_rec:
            logger.warning(
                "Tile size should be larger than the maximum of "
                "the enclosures (recommendation: {} / {})",
                tile_size,
                min_tile_size_rec,
            )

        tp.tile_border(maxsize * tp.dbu, maxsize * tp.dbu)

        tp.tile_size(tile_size, tile_size)
        if isinstance(ref, kdb.LayerInfo):
            tp.input("main_layer", c.kcl.layout, c.cell_index(), c.kcl.layer(ref))
        else:
            tp.input("main_layer", ref)

        operators = []
        port_holes: dict[int, kdb.Region] = defaultdict(kdb.Region)
        ports_by_layer: dict[int, list[Port]] = defaultdict(list)
        for port in c.ports:
            ports_by_layer[port.layer].append(port)

        for layer, sections in self.layer_sections.items():
            layer_index = c.kcl.layer(layer)
            operator = RegionOperator(cell=c, layer=layer_index)
            tp.output(f"target_{layer_index}", operator)
            max_size: int = _min_size
            for _i, section in enumerate(reversed(sections.sections)):
                max_size = max(max_size, section.d_max)
                queue_str = f"var tile_reg = (_tile & _frame).sized({maxsize});"
                queue_str += (
                    "var max_shape = Polygon.ellipse("
                    f"Box.new({section.d_max * 2},{section.d_max * 2}), {n_pts});"
                )
                match section.d_max:
                    case d if d > 0:
                        max_region = (
                            "var max_reg = "
                            "main_layer.minkowski_sum(max_shape).merged();"
                        )
                    case d if d < 0:
                        max_region = "var max_reg = tile_reg - (tile_reg - main_layer);"
                    case 0:
                        max_region = "var max_reg = main_layer & tile_reg;"
                queue_str += max_region
                if section.d_min is not None:
                    queue_str += (
                        "var min_shape = Polygon.ellipse("
                        f"Box.new({section.d_min * 2},{section.d_min * 2}), 64);"
                    )
                    match section.d_min:
                        case d if d > 0:
                            min_region = (
                                "var min_reg = main_layer.minkowski_sum(min_shape);"
                            )
                        case d if d < 0:
                            min_region = (
                                "var min_reg = tile_reg - "
                                "(tile_reg - main_layer).minkowski_sum(min_shape);"
                            )
                        case 0:
                            min_region = "var min_reg = main_layer & tile_reg;"
                    queue_str += min_region
                    queue_str += (
                        f"_output(target_{layer_index},"
                        "(max_reg - min_reg)& _tile, true);"
                    )
                else:
                    queue_str += f"_output(target_{layer_index},max_reg & _tile, true);"

                tp.queue(queue_str)
                logger.debug(
                    "String queued for {} on layer {}: {}", c.name, layer, queue_str
                )

            operators.append((layer_index, operator))
            if carve_out_ports:
                r = port_holes[layer_index]
                for port in carve_out_ports:
                    if port._base.trans is not None:
                        r.insert(
                            port_hole(port.width, max_size).transformed(port.trans)
                        )
                    else:
                        r.insert(
                            port_hole(port.width, max_size).transformed(
                                kdb.ICplxTrans(port.dcplx_trans, c.kcl.dbu)
                            )
                        )
                port_holes[layer_index] = r

        c.kcl.start_changes()
        logger.info("Starting minkowski on {}", c.name)
        tp.execute(f"Minkowski {c.name}")
        c.kcl.end_changes()

        if carve_out_ports:
            for layer_index, operator in operators:
                operator.insert(port_holes=port_holes[layer_index])
        else:
            for _, operator in operators:
                operator.insert()

    def apply_custom(
        self,
        c: KCell,
        shape: Callable[
            [int, int | None], kdb.Edge | kdb.Polygon | kdb.Box | kdb.Region
        ],
    ) -> None:
        """Apply a custom shape based on the section size.

        Args:
            c: The cell to apply the enclosure to.
            shape: A function taking the section size in dbu to calculate the
                full enclosure.
        """
        for layer, layersec in self.layer_sections.items():
            layer_index = c.kcl.layer(layer)
            for sec in layersec.sections:
                c.shapes(layer_index).insert(shape(sec.d_max, sec.d_min))

    def apply_bbox(
        self, c: KCell, ref: kdb.LayerInfo | kdb.Region | None = None
    ) -> None:
        """Apply an enclosure based on a bounding box.

        Args:
            c: Target cell.
            ref: Reference layer or region (the bounding box). If `None` use
                the `main_layer` of  the
                [enclosure][kfactory.enclosure.LayerEnclosure] if defined,
                else throw an error.
        """
        if ref is None:
            ref = self.main_layer

            if ref is None:
                raise ValueError(
                    "The enclosure doesn't have  a reference `main_layer` defined."
                    " Therefore the layer must be defined in calls"
                )

        if isinstance(ref, kdb.LayerInfo):
            ref_ = c.bbox(c.kcl.layer(ref))
        elif isinstance(ref, kdb.Region):
            ref_ = ref.bbox()

        def bbox_reg(d_max: int, d_min: int | None = None) -> kdb.Region:
            reg_max = kdb.Region(ref_)
            reg_max.size(d_max)
            if d_min is None:
                return reg_max
            reg_min = kdb.Region(ref_)
            reg_min.size(d_min)
            return reg_max - reg_min

        self.apply_custom(c, bbox_reg)

    def __str__(self) -> str:
        """String representation of an enclosure.

        Use [name][kfactory.enclosure.LayerEnclosure.name]
        if available. Use a hash of the sections and main_layer if the name is `None`.
        """
        if self._name is not None:
            return self._name
        list_to_hash: Any = [
            self.main_layer,
        ]
        for layer, layer_section in self.layer_sections.items():
            list_to_hash.append([str(layer), str(layer_section.sections)])
        return sha1(str(list_to_hash).encode("UTF-8")).hexdigest()[-8:]  # noqa: S324

    def extrude_path(
        self,
        c: KCell,
        path: list[kdb.DPoint],
        main_layer: kdb.LayerInfo | None,
        width: float,
        start_angle: float | None = None,
        end_angle: float | None = None,
    ) -> None:
        """Extrude a path and add it to a main layer.

        Start and end angle should be set in relation to the orientation of the path.
        If the path for example is starting E->W, the start angle should be 0 (if that
        that is the desired angle). End angle is the same if the end
        piece is stopping 2nd-last -> last in E->W.

        Args:
            c: The cell where to insert the path to
            path: Backbone of the path. [um]
            main_layer: Layer index where to put the main part of the path.
            width: Width of the core of the path
            start_angle: angle of the start piece
            end_angle: angle of the end piece
        """
        if main_layer is None:
            raise ValueError(
                "The enclosure doesn't have  a reference `main_layer` defined."
                " Therefore the layer must be defined in calls"
            )
        extrude_path(
            target=c,
            layer=main_layer,
            path=path,
            width=width,
            enclosure=self,
            start_angle=start_angle,
            end_angle=end_angle,
        )

    def extrude_path_dynamic(
        self,
        c: KCell,
        path: list[kdb.DPoint],
        main_layer: kdb.LayerInfo | None,
        widths: Callable[[float], float] | list[float],
    ) -> None:
        """Extrude a path and add it to a main layer.

        Supports a dynamic width of the path defined by a function
        returning the width for the interval [0,1], or as a list of
        widths of the same lengths as the points.

        Args:
            c: The cell where to insert the path to
            path: Backbone of the path. [um]
            main_layer: Layer index where to put the main part of the path.
            widths: Width of the core of the path
        """
        if main_layer is None:
            raise ValueError(
                "The enclosure doesn't have  a reference `main_layer` defined."
                " Therefore the layer must be defined in calls"
            )
        extrude_path_dynamic(
            target=c, layer=main_layer, path=path, widths=widths, enclosure=self
        )

    def model_copy(
        self, *, update: Mapping[str, Any] | None = {"name": None}, deep: bool = False
    ) -> LayerEnclosure:
        return super().model_copy(update=update, deep=deep)

name property

name: str

Get name of the Enclosure.

__hash__

__hash__() -> int

Calculate a unique hash of the enclosure.

Source code in kfactory/enclosure.py
589
590
591
def __hash__(self) -> int:  # make hashable BaseModel subclass
    """Calculate a unique hash of the enclosure."""
    return hash((str(self), self.main_layer, tuple(self.layer_sections.items())))

__init__

__init__(
    sections: Sequence[
        tuple[LayerInfo, int] | tuple[LayerInfo, int, int]
    ] = [],
    name: str | None = None,
    main_layer: LayerInfo | None = None,
    dsections: Sequence[
        tuple[LayerInfo, float]
        | tuple[LayerInfo, float, float]
    ]
    | None = None,
    bbox_sections: Sequence[tuple[LayerInfo, int]] = [],
    kcl: KCLayout | None = None,
) -> None

Constructor of new enclosure.

Parameters:

Name Type Description Default
sections Sequence[tuple[LayerInfo, int] | tuple[LayerInfo, int, int]]

tuples containing info for the enclosure. Elements must be of the form (layer, max) or (layer, min, max)

[]
name str | None

Optional name of the enclosure. If a name is given in the cell name this name will be used for enclosure arguments.

None
main_layer LayerInfo | None

Main layer used if the functions don't get an explicit layer.

None
dsections Sequence[tuple[LayerInfo, float] | tuple[LayerInfo, float, float]] | None

Same as sections but min/max defined in um

None
kcl KCLayout | None

KCLayout Used for conversion dbu -> um or when copying. Must be specified if desections is not None. Also necessary if copying to another layout and not all layers used are LayerInfos.

None
Source code in kfactory/enclosure.py
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
def __init__(
    self,
    sections: Sequence[
        tuple[kdb.LayerInfo, int] | tuple[kdb.LayerInfo, int, int]
    ] = [],
    name: str | None = None,
    main_layer: kdb.LayerInfo | None = None,
    dsections: Sequence[
        tuple[kdb.LayerInfo, float] | tuple[kdb.LayerInfo, float, float]
    ]
    | None = None,
    bbox_sections: Sequence[tuple[kdb.LayerInfo, int]] = [],
    kcl: KCLayout | None = None,
) -> None:
    """Constructor of new enclosure.

    Args:
        sections: tuples containing info for the enclosure.
            Elements must be of the form (layer, max) or (layer, min, max)
        name: Optional name of the enclosure. If a name is given in the
            cell name this name will be used for enclosure arguments.
        main_layer: Main layer used if the functions don't get an explicit layer.
        dsections: Same as sections but min/max defined in um
        kcl: `KCLayout` Used for conversion dbu -> um or when copying.
            Must be specified if `desections` is not `None`. Also necessary
            if copying to another layout and not all layers used are LayerInfos.
    """
    layer_sections: dict[kdb.LayerInfo, LayerSection] = {}

    if dsections is not None:
        assert kcl is not None, "If sections in um are defined, kcl must be set"
        sections = list(sections)
        for section in dsections:
            if len(section) == 2:  # noqa: PLR2004
                sections.append((section[0], kcl.to_dbu(section[1])))

            elif len(section) == 3:  # noqa: PLR2004
                sections.append(
                    (
                        section[0],
                        kcl.to_dbu(section[1]),
                        kcl.to_dbu(section[2]),
                    )
                )

    for sec in sorted(
        sections,
        key=lambda sec: (sec[0].name, sec[0].layer, sec[0].datatype, sec[1]),
    ):
        if sec[0] in layer_sections:
            ls = layer_sections[sec[0]]
        else:
            ls = LayerSection()
            layer_sections[sec[0]] = ls
        ls.add_section(Section(d_max=sec[1])) if len(sec) < 3 else ls.add_section(  # noqa: PLR2004
            Section(d_max=sec[2], d_min=sec[1])
        )
    super().__init__(
        main_layer=main_layer,
        kcl=kcl,
        layer_sections=layer_sections,
        bbox_sections={t[0]: t[1] for t in bbox_sections},
    )
    self._name = name

__str__

__str__() -> str

String representation of an enclosure.

Use name if available. Use a hash of the sections and main_layer if the name is None.

Source code in kfactory/enclosure.py
971
972
973
974
975
976
977
978
979
980
981
982
983
984
def __str__(self) -> str:
    """String representation of an enclosure.

    Use [name][kfactory.enclosure.LayerEnclosure.name]
    if available. Use a hash of the sections and main_layer if the name is `None`.
    """
    if self._name is not None:
        return self._name
    list_to_hash: Any = [
        self.main_layer,
    ]
    for layer, layer_section in self.layer_sections.items():
        list_to_hash.append([str(layer), str(layer_section.sections)])
    return sha1(str(list_to_hash).encode("UTF-8")).hexdigest()[-8:]  # noqa: S324

apply_bbox

apply_bbox(
    c: KCell, ref: LayerInfo | Region | None = None
) -> None

Apply an enclosure based on a bounding box.

Parameters:

Name Type Description Default
c KCell

Target cell.

required
ref LayerInfo | Region | None

Reference layer or region (the bounding box). If None use the main_layer of the enclosure if defined, else throw an error.

None
Source code in kfactory/enclosure.py
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
def apply_bbox(
    self, c: KCell, ref: kdb.LayerInfo | kdb.Region | None = None
) -> None:
    """Apply an enclosure based on a bounding box.

    Args:
        c: Target cell.
        ref: Reference layer or region (the bounding box). If `None` use
            the `main_layer` of  the
            [enclosure][kfactory.enclosure.LayerEnclosure] if defined,
            else throw an error.
    """
    if ref is None:
        ref = self.main_layer

        if ref is None:
            raise ValueError(
                "The enclosure doesn't have  a reference `main_layer` defined."
                " Therefore the layer must be defined in calls"
            )

    if isinstance(ref, kdb.LayerInfo):
        ref_ = c.bbox(c.kcl.layer(ref))
    elif isinstance(ref, kdb.Region):
        ref_ = ref.bbox()

    def bbox_reg(d_max: int, d_min: int | None = None) -> kdb.Region:
        reg_max = kdb.Region(ref_)
        reg_max.size(d_max)
        if d_min is None:
            return reg_max
        reg_min = kdb.Region(ref_)
        reg_min.size(d_min)
        return reg_max - reg_min

    self.apply_custom(c, bbox_reg)

apply_custom

apply_custom(
    c: KCell,
    shape: Callable[
        [int, int | None], Edge | Polygon | Box | Region
    ],
) -> None

Apply a custom shape based on the section size.

Parameters:

Name Type Description Default
c KCell

The cell to apply the enclosure to.

required
shape Callable[[int, int | None], Edge | Polygon | Box | Region]

A function taking the section size in dbu to calculate the full enclosure.

required
Source code in kfactory/enclosure.py
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
def apply_custom(
    self,
    c: KCell,
    shape: Callable[
        [int, int | None], kdb.Edge | kdb.Polygon | kdb.Box | kdb.Region
    ],
) -> None:
    """Apply a custom shape based on the section size.

    Args:
        c: The cell to apply the enclosure to.
        shape: A function taking the section size in dbu to calculate the
            full enclosure.
    """
    for layer, layersec in self.layer_sections.items():
        layer_index = c.kcl.layer(layer)
        for sec in layersec.sections:
            c.shapes(layer_index).insert(shape(sec.d_max, sec.d_min))

apply_minkowski_custom

apply_minkowski_custom(
    c: KCell,
    shape: Callable[[int], Edge | Polygon | Box],
    ref: LayerInfo | Region | None = None,
) -> None

Apply an enclosure with a custom shape.

This can be used for tapers/ waveguides or similar that are straight.

Parameters:

Name Type Description Default
c KCell

Cell to apply the enclosure to.

required
shape Callable[[int], Edge | Polygon | Box]

A function that will return a shape which takes one argument the size of the section in dbu.

required
ref LayerInfo | Region | None

Reference to use as a base for the enclosure.

None
Source code in kfactory/enclosure.py
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
def apply_minkowski_custom(
    self,
    c: KCell,
    shape: Callable[[int], kdb.Edge | kdb.Polygon | kdb.Box],
    ref: kdb.LayerInfo | kdb.Region | None = None,
) -> None:
    """Apply an enclosure with a custom shape.

    This can be used for tapers/
    waveguides or similar that are straight.

    Args:
        c: Cell to apply the enclosure to.
        shape: A function that will return a shape which takes one argument
            the size of the section in dbu.
        ref: Reference to use as a base for the enclosure.
    """
    if ref is None:
        ref = self.main_layer

        if ref is None:
            raise ValueError(
                "The enclosure doesn't have  a reference `main_layer` defined."
                " Therefore the layer must be defined in calls"
            )
    r = (
        kdb.Region(c.begin_shapes_rec(c.kcl.layer(ref)))
        if isinstance(ref, kdb.LayerInfo)
        else ref.dup()
    )
    r.merge()

    for layer, layersec in reversed(self.layer_sections.items()):
        for section in layersec.sections:
            c.shapes(c.kcl.layer(layer)).insert(
                self.minkowski_region(r, section.d_max, shape)
                - self.minkowski_region(r, section.d_min, shape)
            )

apply_minkowski_enc

apply_minkowski_enc(
    c: KCell,
    ref: LayerInfo | Region | None,
    direction: Direction = Direction.BOTH,
) -> None

Apply an enclosure with a vector in y-direction.

This can be used for tapers/ waveguides or similar that are straight.

Parameters:

Name Type Description Default
c KCell

Cell to apply the enclosure to.

required
ref LayerInfo | Region | None

Reference to use as a base for the enclosure.

required
direction Direction

X/Y or both directions. Uses a box if both directions are selected.

BOTH
Source code in kfactory/enclosure.py
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
def apply_minkowski_enc(
    self,
    c: KCell,
    ref: kdb.LayerInfo | kdb.Region | None,  # layer index or the region
    direction: Direction = Direction.BOTH,
) -> None:
    """Apply an enclosure with a vector in y-direction.

    This can be used for tapers/
    waveguides or similar that are straight.

    Args:
        c: Cell to apply the enclosure to.
        ref: Reference to use as a base for the enclosure.
        direction: X/Y or both directions.
            Uses a box if both directions are selected.
    """
    match direction:
        case Direction.BOTH:

            def box(d: int) -> kdb.Box:
                return kdb.Box(-d, -d, d, d)

            self.apply_minkowski_custom(c, ref=ref, shape=box)

        case Direction.Y:

            def edge(d: int) -> kdb.Edge:
                return kdb.Edge(0, -d, 0, d)

            self.apply_minkowski_custom(c, ref=ref, shape=edge)

        case Direction.X:

            def edge(d: int) -> kdb.Edge:
                return kdb.Edge(-d, 0, d, 0)

            self.apply_minkowski_custom(c, ref=ref, shape=edge)

        case _:
            raise ValueError("Undefined direction")

apply_minkowski_tiled

apply_minkowski_tiled(
    c: KCell,
    ref: LayerInfo | Region | None = None,
    tile_size: float | None = None,
    n_pts: int = 64,
    n_threads: int | None = None,
    carve_out_ports: Iterable[Port] = [],
) -> None

Minkowski regions with tiling processor.

Useful if the target is a big or complicated enclosure. Will split target ref into tiles and calculate them in parallel. Uses a circle as a shape for the minkowski sum.

Parameters:

Name Type Description Default
c KCell

Target KCell to apply the enclosures into.

required
ref LayerInfo | Region | None

The reference shapes to apply the enclosures to. Can be a layer or a region. If None, it will try to use the main_layer of the enclosure.

None
tile_size float | None

Tile size. This should be in the order off 10+ maximum size of the maximum size of sections.

None
n_pts int

Number of points in the circle. < 3 will create a triangle. 4 a diamond, etc.

64
n_threads int | None

Number o threads to use. By default (None) it will use as many threads as are set to the process (usually all cores of the machine).

None
carve_out_ports Iterable[Port]

Carves out a box of port_width +

[]
Source code in kfactory/enclosure.py
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
def apply_minkowski_tiled(
    self,
    c: KCell,
    ref: kdb.LayerInfo | kdb.Region | None = None,
    tile_size: float | None = None,
    n_pts: int = 64,
    n_threads: int | None = None,
    carve_out_ports: Iterable[Port] = [],
) -> None:
    """Minkowski regions with tiling processor.

    Useful if the target is a big or complicated enclosure. Will split target ref
    into tiles and calculate them in parallel. Uses a circle as a shape for the
    minkowski sum.

    Args:
        c: Target KCell to apply the enclosures into.
        ref: The reference shapes to apply the enclosures to.
            Can be a layer or a region. If `None`, it will try to use the
            `main_layer` of the
            [enclosure][kfactory.enclosure.LayerEnclosure].
        tile_size: Tile size. This should be in the order off 10+ maximum size
            of the maximum size of sections.
        n_pts: Number of points in the circle. < 3 will create a triangle. 4 a
            diamond, etc.
        n_threads: Number o threads to use. By default (`None`) it will use as many
            threads as are set to the process (usually all cores of the machine).
        carve_out_ports: Carves out a box of port_width +
    """
    if ref is None:
        ref = self.main_layer

        if ref is None:
            raise ValueError(
                "The enclosure doesn't have  a reference `main_layer` defined."
                " Therefore the layer must be defined in calls"
            )
    tp = kdb.TilingProcessor()
    tp.frame = c.dbbox()  # type: ignore[misc, assignment]
    tp.dbu = c.kcl.dbu
    tp.threads = n_threads or config.n_threads
    maxsize = 0
    for layersection in self.layer_sections.values():
        maxsize = max(
            maxsize, *[section.d_max for section in layersection.sections]
        )

    min_tile_size_rec = 10 * maxsize * tp.dbu

    if tile_size is None:
        tile_size = min_tile_size_rec * 2

    if float(tile_size) <= min_tile_size_rec:
        logger.warning(
            "Tile size should be larger than the maximum of "
            "the enclosures (recommendation: {} / {})",
            tile_size,
            min_tile_size_rec,
        )

    tp.tile_border(maxsize * tp.dbu, maxsize * tp.dbu)

    tp.tile_size(tile_size, tile_size)
    if isinstance(ref, kdb.LayerInfo):
        tp.input("main_layer", c.kcl.layout, c.cell_index(), c.kcl.layer(ref))
    else:
        tp.input("main_layer", ref)

    operators = []
    port_holes: dict[int, kdb.Region] = defaultdict(kdb.Region)
    ports_by_layer: dict[int, list[Port]] = defaultdict(list)
    for port in c.ports:
        ports_by_layer[port.layer].append(port)

    for layer, sections in self.layer_sections.items():
        layer_index = c.kcl.layer(layer)
        operator = RegionOperator(cell=c, layer=layer_index)
        tp.output(f"target_{layer_index}", operator)
        max_size: int = _min_size
        for _i, section in enumerate(reversed(sections.sections)):
            max_size = max(max_size, section.d_max)
            queue_str = f"var tile_reg = (_tile & _frame).sized({maxsize});"
            queue_str += (
                "var max_shape = Polygon.ellipse("
                f"Box.new({section.d_max * 2},{section.d_max * 2}), {n_pts});"
            )
            match section.d_max:
                case d if d > 0:
                    max_region = (
                        "var max_reg = "
                        "main_layer.minkowski_sum(max_shape).merged();"
                    )
                case d if d < 0:
                    max_region = "var max_reg = tile_reg - (tile_reg - main_layer);"
                case 0:
                    max_region = "var max_reg = main_layer & tile_reg;"
            queue_str += max_region
            if section.d_min is not None:
                queue_str += (
                    "var min_shape = Polygon.ellipse("
                    f"Box.new({section.d_min * 2},{section.d_min * 2}), 64);"
                )
                match section.d_min:
                    case d if d > 0:
                        min_region = (
                            "var min_reg = main_layer.minkowski_sum(min_shape);"
                        )
                    case d if d < 0:
                        min_region = (
                            "var min_reg = tile_reg - "
                            "(tile_reg - main_layer).minkowski_sum(min_shape);"
                        )
                    case 0:
                        min_region = "var min_reg = main_layer & tile_reg;"
                queue_str += min_region
                queue_str += (
                    f"_output(target_{layer_index},"
                    "(max_reg - min_reg)& _tile, true);"
                )
            else:
                queue_str += f"_output(target_{layer_index},max_reg & _tile, true);"

            tp.queue(queue_str)
            logger.debug(
                "String queued for {} on layer {}: {}", c.name, layer, queue_str
            )

        operators.append((layer_index, operator))
        if carve_out_ports:
            r = port_holes[layer_index]
            for port in carve_out_ports:
                if port._base.trans is not None:
                    r.insert(
                        port_hole(port.width, max_size).transformed(port.trans)
                    )
                else:
                    r.insert(
                        port_hole(port.width, max_size).transformed(
                            kdb.ICplxTrans(port.dcplx_trans, c.kcl.dbu)
                        )
                    )
            port_holes[layer_index] = r

    c.kcl.start_changes()
    logger.info("Starting minkowski on {}", c.name)
    tp.execute(f"Minkowski {c.name}")
    c.kcl.end_changes()

    if carve_out_ports:
        for layer_index, operator in operators:
            operator.insert(port_holes=port_holes[layer_index])
    else:
        for _, operator in operators:
            operator.insert()

apply_minkowski_x

apply_minkowski_x(
    c: KCell, ref: LayerInfo | Region | None
) -> None

Apply an enclosure with a vector in x-direction.

This can be used for tapers/ waveguides or similar that are straight.

Parameters:

Name Type Description Default
c KCell

Cell to apply the enclosure to.

required
ref LayerInfo | Region | None

Reference to use as a base for the enclosure.

required
Source code in kfactory/enclosure.py
707
708
709
710
711
712
713
714
715
716
717
718
719
def apply_minkowski_x(
    self, c: KCell, ref: kdb.LayerInfo | kdb.Region | None
) -> None:
    """Apply an enclosure with a vector in x-direction.

    This can be used for tapers/
    waveguides or similar that are straight.

    Args:
        c: Cell to apply the enclosure to.
        ref: Reference to use as a base for the enclosure.
    """
    return self.apply_minkowski_enc(c, ref=ref, direction=Direction.X)

apply_minkowski_y

apply_minkowski_y(
    c: KCell, ref: LayerInfo | Region | None = None
) -> None

Apply an enclosure with a vector in y-direction.

This can be used for tapers/ waveguides or similar that are straight.

Parameters:

Name Type Description Default
c KCell

Cell to apply the enclosure to.

required
ref LayerInfo | Region | None

Reference to use as a base for the enclosure.

None
Source code in kfactory/enclosure.py
693
694
695
696
697
698
699
700
701
702
703
704
705
def apply_minkowski_y(
    self, c: KCell, ref: kdb.LayerInfo | kdb.Region | None = None
) -> None:
    """Apply an enclosure with a vector in y-direction.

    This can be used for tapers/
    waveguides or similar that are straight.

    Args:
        c: Cell to apply the enclosure to.
        ref: Reference to use as a base for the enclosure.
    """
    return self.apply_minkowski_enc(c, ref=ref, direction=Direction.Y)

extrude_path

extrude_path(
    c: KCell,
    path: list[DPoint],
    main_layer: LayerInfo | None,
    width: float,
    start_angle: float | None = None,
    end_angle: float | None = None,
) -> None

Extrude a path and add it to a main layer.

Start and end angle should be set in relation to the orientation of the path. If the path for example is starting E->W, the start angle should be 0 (if that that is the desired angle). End angle is the same if the end piece is stopping 2nd-last -> last in E->W.

Parameters:

Name Type Description Default
c KCell

The cell where to insert the path to

required
path list[DPoint]

Backbone of the path. [um]

required
main_layer LayerInfo | None

Layer index where to put the main part of the path.

required
width float

Width of the core of the path

required
start_angle float | None

angle of the start piece

None
end_angle float | None

angle of the end piece

None
Source code in kfactory/enclosure.py
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
def extrude_path(
    self,
    c: KCell,
    path: list[kdb.DPoint],
    main_layer: kdb.LayerInfo | None,
    width: float,
    start_angle: float | None = None,
    end_angle: float | None = None,
) -> None:
    """Extrude a path and add it to a main layer.

    Start and end angle should be set in relation to the orientation of the path.
    If the path for example is starting E->W, the start angle should be 0 (if that
    that is the desired angle). End angle is the same if the end
    piece is stopping 2nd-last -> last in E->W.

    Args:
        c: The cell where to insert the path to
        path: Backbone of the path. [um]
        main_layer: Layer index where to put the main part of the path.
        width: Width of the core of the path
        start_angle: angle of the start piece
        end_angle: angle of the end piece
    """
    if main_layer is None:
        raise ValueError(
            "The enclosure doesn't have  a reference `main_layer` defined."
            " Therefore the layer must be defined in calls"
        )
    extrude_path(
        target=c,
        layer=main_layer,
        path=path,
        width=width,
        enclosure=self,
        start_angle=start_angle,
        end_angle=end_angle,
    )

extrude_path_dynamic

extrude_path_dynamic(
    c: KCell,
    path: list[DPoint],
    main_layer: LayerInfo | None,
    widths: Callable[[float], float] | list[float],
) -> None

Extrude a path and add it to a main layer.

Supports a dynamic width of the path defined by a function returning the width for the interval [0,1], or as a list of widths of the same lengths as the points.

Parameters:

Name Type Description Default
c KCell

The cell where to insert the path to

required
path list[DPoint]

Backbone of the path. [um]

required
main_layer LayerInfo | None

Layer index where to put the main part of the path.

required
widths Callable[[float], float] | list[float]

Width of the core of the path

required
Source code in kfactory/enclosure.py
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
def extrude_path_dynamic(
    self,
    c: KCell,
    path: list[kdb.DPoint],
    main_layer: kdb.LayerInfo | None,
    widths: Callable[[float], float] | list[float],
) -> None:
    """Extrude a path and add it to a main layer.

    Supports a dynamic width of the path defined by a function
    returning the width for the interval [0,1], or as a list of
    widths of the same lengths as the points.

    Args:
        c: The cell where to insert the path to
        path: Backbone of the path. [um]
        main_layer: Layer index where to put the main part of the path.
        widths: Width of the core of the path
    """
    if main_layer is None:
        raise ValueError(
            "The enclosure doesn't have  a reference `main_layer` defined."
            " Therefore the layer must be defined in calls"
        )
    extrude_path_dynamic(
        target=c, layer=main_layer, path=path, widths=widths, enclosure=self
    )

minkowski_region

minkowski_region(
    r: Region,
    d: int | None,
    shape: Callable[
        [int], list[Point] | Box | Edge | Polygon
    ],
) -> kdb.Region

Calculaste a region from a minkowski sum.

If the distance is negative, the function will take the inverse region and apply the minkowski and take the inverse again.

Parameters:

Name Type Description Default
r Region

Target region.

required
d int | None

Distance to pass to the shape. Can be any integer. [dbu]

required
shape Callable[[int], list[Point] | Box | Edge | Polygon]

Function returning a shape for the minkowski region.

required
Source code in kfactory/enclosure.py
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
def minkowski_region(
    self,
    r: kdb.Region,
    d: int | None,
    shape: Callable[[int], list[kdb.Point] | kdb.Box | kdb.Edge | kdb.Polygon],
) -> kdb.Region:
    """Calculaste a region from a minkowski sum.

    If the distance is negative, the function will take the inverse region and apply
    the minkowski and take the inverse again.

    Args:
        r: Target region.
        d: Distance to pass to the shape. Can be any integer. [dbu]
        shape: Function returning a shape for the minkowski region.
    """
    if d is None:
        return kdb.Region()
    if d == 0:
        return r.dup()
    if d > 0:
        return r.minkowski_sum(shape(d))
    shape_ = shape(abs(d))
    if isinstance(shape_, list):
        box_shape = kdb.Polygon(shape_)
        bbox_maxsize = max(
            box_shape.bbox().width(),
            box_shape.bbox().height(),
        )
    else:
        bbox_maxsize = max(
            shape_.bbox().width(),
            shape_.bbox().height(),
        )
    bbox_r = kdb.Region(r.bbox().enlarged(bbox_maxsize))
    return r - (bbox_r - r).minkowski_sum(shape_)

to_dtype

to_dtype(kcl: KCLayout) -> DLayerEnclosure

Convert the enclosure to a um based enclosure.

Source code in kfactory/enclosure.py
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
def to_dtype(self, kcl: KCLayout) -> DLayerEnclosure:
    """Convert the enclosure to a um based enclosure."""
    if self.main_layer is None:
        raise ValueError("um based enclosures must have a main_layer")
    return DLayerEnclosure(
        name=self._name,
        sections=[
            (layer, kcl.to_um(section.d_max))
            if section.d_min is None
            else (layer, kcl.to_um(section.d_min), kcl.to_um(section.d_max))
            for layer, layer_section in self.layer_sections.items()
            for section in layer_section.sections
        ],
        main_layer=self.main_layer,
    )

LayerEnum

Bases: int, Enum

Class for having the layers stored and a mapping int <-> layer,datatype.

This Enum can also be treated as a tuple, i.e. it implements __getitem__ and __len__.

Attributes:

Name Type Description
layer int

layer number

datatype int

layer datatype

Source code in kfactory/layer.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
class LayerEnum(int, Enum):  # type: ignore[misc]
    """Class for having the layers stored and a mapping int <-> layer,datatype.

    This Enum can also be treated as a tuple, i.e. it implements `__getitem__`
    and `__len__`.

    Attributes:
        layer: layer number
        datatype: layer datatype
    """

    layer: int
    datatype: int
    name: str
    layout: constant[kdb.Layout]

    def __init__(self, layer: int, datatype: int) -> None:
        """Just here to make sure klayout knows the layer name."""
        self.layout.set_info(self, kdb.LayerInfo(self.layer, self.datatype, self.name))

    def __new__(
        cls,
        layer: int,
        datatype: int,
    ) -> Self:
        """Create a new Enum.

        Because it needs to act like an integer an enum is created and expanded.

        Args:
            layer: Layer number of the layer.
            datatype: Datatype of the layer.
        """
        value = cls.layout.layer(layer, datatype)
        obj: int = int.__new__(cls, value)
        obj._value_ = value  # type: ignore[attr-defined]
        obj.layer = layer  # type: ignore[attr-defined]
        obj.datatype = datatype  # type: ignore[attr-defined]
        return obj  # type: ignore[return-value]

    def __getitem__(self, key: int) -> int:
        """Retrieve layer number[0] / datatype[1] of a layer."""
        if key == 0:
            return self.layer
        if key == 1:
            return self.datatype

        raise ValueError(
            "LayerMap only has two values accessible like"
            " a list, layer == [0] and datatype == [1]"
        )

    def __len__(self) -> int:
        """A layer has length 2, layer number and datatype."""
        return 2

    def __iter__(self) -> Iterator[int]:
        """Allow for loops to iterate over the LayerEnum."""
        yield from [self.layer, self.datatype]

    def __str__(self) -> str:
        """Return the name of the LayerEnum."""
        return self.name

__getitem__

__getitem__(key: int) -> int

Retrieve layer number[0] / datatype[1] of a layer.

Source code in kfactory/layer.py
111
112
113
114
115
116
117
118
119
120
121
def __getitem__(self, key: int) -> int:
    """Retrieve layer number[0] / datatype[1] of a layer."""
    if key == 0:
        return self.layer
    if key == 1:
        return self.datatype

    raise ValueError(
        "LayerMap only has two values accessible like"
        " a list, layer == [0] and datatype == [1]"
    )

__init__

__init__(layer: int, datatype: int) -> None

Just here to make sure klayout knows the layer name.

Source code in kfactory/layer.py
87
88
89
def __init__(self, layer: int, datatype: int) -> None:
    """Just here to make sure klayout knows the layer name."""
    self.layout.set_info(self, kdb.LayerInfo(self.layer, self.datatype, self.name))

__iter__

__iter__() -> Iterator[int]

Allow for loops to iterate over the LayerEnum.

Source code in kfactory/layer.py
127
128
129
def __iter__(self) -> Iterator[int]:
    """Allow for loops to iterate over the LayerEnum."""
    yield from [self.layer, self.datatype]

__len__

__len__() -> int

A layer has length 2, layer number and datatype.

Source code in kfactory/layer.py
123
124
125
def __len__(self) -> int:
    """A layer has length 2, layer number and datatype."""
    return 2

__new__

__new__(layer: int, datatype: int) -> Self

Create a new Enum.

Because it needs to act like an integer an enum is created and expanded.

Parameters:

Name Type Description Default
layer int

Layer number of the layer.

required
datatype int

Datatype of the layer.

required
Source code in kfactory/layer.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def __new__(
    cls,
    layer: int,
    datatype: int,
) -> Self:
    """Create a new Enum.

    Because it needs to act like an integer an enum is created and expanded.

    Args:
        layer: Layer number of the layer.
        datatype: Datatype of the layer.
    """
    value = cls.layout.layer(layer, datatype)
    obj: int = int.__new__(cls, value)
    obj._value_ = value  # type: ignore[attr-defined]
    obj.layer = layer  # type: ignore[attr-defined]
    obj.datatype = datatype  # type: ignore[attr-defined]
    return obj  # type: ignore[return-value]

__str__

__str__() -> str

Return the name of the LayerEnum.

Source code in kfactory/layer.py
131
132
133
def __str__(self) -> str:
    """Return the name of the LayerEnum."""
    return self.name

LayerInfos pydantic-model

Bases: BaseModel

Class to store and serialize LayerInfos used in KCLayout.

Parameters:

Name Type Description Default
kwargs Any

kdb.LayerInfo . if any extra field is not a kdb.LayerInfo, the validator will raise a ValidationError.

{}

Config:

  • arbitrary_types_allowed: True
  • extra: allow

Validators:

  • _validate_layers
Source code in kfactory/layer.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class LayerInfos(BaseModel):
    """Class to store and serialize LayerInfos used in KCLayout.

    Args:
        kwargs: kdb.LayerInfo . if any extra field is not a kdb.LayerInfo,
            the validator will raise a ValidationError.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")

    def __init__(self, **kwargs: Any) -> None:
        """Initialize the LayerInfos class.

        Args:
            kwargs: kdb.LayerInfo . if any extra field is not a kdb.LayerInfo,
                the validator will raise a ValidationError.
        """
        super().__init__(**kwargs)

    @model_validator(mode="after")
    def _validate_layers(self) -> Self:
        field_names = set(self.__class__.model_fields.keys())
        if self.model_extra is not None:
            field_names |= self.model_extra.keys()
        for field_name in field_names:
            f = getattr(self, field_name)
            if not isinstance(f, kdb.LayerInfo):
                raise InvalidLayerError(
                    "All fields in LayerInfos must be of type kdb.LayerInfo. "
                    f"Field {field_name} is of type {type(f)}"
                )
            if not f.name:
                f.name = field_name
            if f.layer == -1 or f.datatype == -1:
                raise InvalidLayerError(
                    "Layers must specify layer number and datatype."
                    f" {field_name} didn't specify them"
                )
        return self

    def __getitem__(self, value: str) -> kdb.LayerInfo:
        return getattr(self, value)  # type: ignore[no-any-return]

__init__

__init__(**kwargs: Any) -> None

Initialize the LayerInfos class.

Parameters:

Name Type Description Default
kwargs Any

kdb.LayerInfo . if any extra field is not a kdb.LayerInfo, the validator will raise a ValidationError.

{}
Source code in kfactory/layer.py
37
38
39
40
41
42
43
44
def __init__(self, **kwargs: Any) -> None:
    """Initialize the LayerInfos class.

    Args:
        kwargs: kdb.LayerInfo . if any extra field is not a kdb.LayerInfo,
            the validator will raise a ValidationError.
    """
    super().__init__(**kwargs)

LayerStack pydantic-model

Bases: BaseModel

For simulation and 3D rendering.

Parameters:

Name Type Description Default
layers LayerLevel

dict of layer_levels.

{}

Fields:

Source code in kfactory/layer.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
class LayerStack(BaseModel):
    """For simulation and 3D rendering.

    Parameters:
        layers: dict of layer_levels.
    """

    layers: dict[str, LayerLevel] = Field(default_factory=dict)

    def __init__(self, **layers: LayerLevel) -> None:
        """Add LayerLevels automatically for subclassed LayerStacks."""
        super().__init__(layers=layers)

    def get_layer_to_thickness(self) -> dict[tuple[int, int], float]:
        """Returns layer tuple to thickness (um)."""
        return {
            level.layer: level.thickness
            for level in self.layers.values()
            if level.thickness
        }

    def get_layer_to_zmin(self) -> dict[tuple[int, int], float]:
        """Returns layer tuple to z min position (um)."""
        return {
            level.layer: level.zmin for level in self.layers.values() if level.thickness
        }

    def get_layer_to_material(self) -> dict[tuple[int, int], str]:
        """Returns layer tuple to material name."""
        return {
            level.layer: level.material
            for level in self.layers.values()
            if level.thickness and level.material
        }

    def get_layer_to_sidewall_angle(self) -> dict[tuple[int, int], float]:
        """Returns layer tuple to material name."""
        return {
            level.layer: level.sidewall_angle
            for level in self.layers.values()
            if level.thickness
        }

    def get_layer_to_info(self) -> dict[tuple[int, int], Info]:
        """Returns layer tuple to info dict."""
        return {level.layer: level.info for level in self.layers.values()}

    def to_dict(self) -> dict[str, dict[str, dict[str, Any]]]:
        return {
            level_name: level.model_dump() for level_name, level in self.layers.items()
        }

    def __getitem__(self, key: str) -> LayerLevel:
        """Access layer stack elements."""
        if key not in self.layers:
            layers = list(self.layers.keys())
            raise KeyError(f"{key!r} not in {layers}")

        return self.layers[key]

    def __getattr__(self, attr: str) -> Any:
        return self.layers[attr]

__getitem__

__getitem__(key: str) -> LayerLevel

Access layer stack elements.

Source code in kfactory/layer.py
250
251
252
253
254
255
256
def __getitem__(self, key: str) -> LayerLevel:
    """Access layer stack elements."""
    if key not in self.layers:
        layers = list(self.layers.keys())
        raise KeyError(f"{key!r} not in {layers}")

    return self.layers[key]

__init__

__init__(**layers: LayerLevel) -> None

Add LayerLevels automatically for subclassed LayerStacks.

Source code in kfactory/layer.py
207
208
209
def __init__(self, **layers: LayerLevel) -> None:
    """Add LayerLevels automatically for subclassed LayerStacks."""
    super().__init__(layers=layers)

get_layer_to_info

get_layer_to_info() -> dict[tuple[int, int], Info]

Returns layer tuple to info dict.

Source code in kfactory/layer.py
241
242
243
def get_layer_to_info(self) -> dict[tuple[int, int], Info]:
    """Returns layer tuple to info dict."""
    return {level.layer: level.info for level in self.layers.values()}

get_layer_to_material

get_layer_to_material() -> dict[tuple[int, int], str]

Returns layer tuple to material name.

Source code in kfactory/layer.py
225
226
227
228
229
230
231
def get_layer_to_material(self) -> dict[tuple[int, int], str]:
    """Returns layer tuple to material name."""
    return {
        level.layer: level.material
        for level in self.layers.values()
        if level.thickness and level.material
    }

get_layer_to_sidewall_angle

get_layer_to_sidewall_angle() -> dict[
    tuple[int, int], float
]

Returns layer tuple to material name.

Source code in kfactory/layer.py
233
234
235
236
237
238
239
def get_layer_to_sidewall_angle(self) -> dict[tuple[int, int], float]:
    """Returns layer tuple to material name."""
    return {
        level.layer: level.sidewall_angle
        for level in self.layers.values()
        if level.thickness
    }

get_layer_to_thickness

get_layer_to_thickness() -> dict[tuple[int, int], float]

Returns layer tuple to thickness (um).

Source code in kfactory/layer.py
211
212
213
214
215
216
217
def get_layer_to_thickness(self) -> dict[tuple[int, int], float]:
    """Returns layer tuple to thickness (um)."""
    return {
        level.layer: level.thickness
        for level in self.layers.values()
        if level.thickness
    }

get_layer_to_zmin

get_layer_to_zmin() -> dict[tuple[int, int], float]

Returns layer tuple to z min position (um).

Source code in kfactory/layer.py
219
220
221
222
223
def get_layer_to_zmin(self) -> dict[tuple[int, int], float]:
    """Returns layer tuple to z min position (um)."""
    return {
        level.layer: level.zmin for level in self.layers.values() if level.thickness
    }

Netlist pydantic-model

Bases: BaseModel

This is still experimental.

Caution is advised when using this, as the API might suddenly change. In order to fix bugs etc.

Attributes:

Name Type Description
instances dict[str, NetlistInstance]

Dictionary with a mapping between instances and their settings.

nets list[Net]

Nets of the netlist. This is an abstraction and can in the Schema either be a route or a connection.

ports list[NetlistPort]

Ports/Pins of the netlist. Upstream exposed ports/pins. These can either be references to a subcircuit (instance) port or a new one.

Fields:

Validators:

  • _validate_model
Source code in kfactory/netlist.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
class Netlist(BaseModel, extra="forbid"):
    """This is still experimental.

    Caution is advised when using this, as the API might suddenly change.
    In order to fix bugs etc.


    Attributes:
        instances: Dictionary with a mapping between instances and their settings.
        nets: Nets of the netlist. This is an abstraction and can in the Schema either
            be a route or a connection.
        ports: Ports/Pins of the netlist. Upstream exposed ports/pins. These can either
            be references to a subcircuit (instance) port or a new one.
    """

    instances: dict[str, NetlistInstance] = Field(default_factory=dict)
    nets: list[Net] = Field(default_factory=list)
    ports: list[NetlistPort] = Field(default_factory=list)

    def sort(self) -> Self:
        if self.instances:
            self.instances = dict(sorted(self.instances.items()))
        for net in self.nets:
            net.sort()
        self.nets.sort()
        if self.ports:
            self.ports.sort()
        return self

    @model_validator(mode="before")
    @classmethod
    def _validate_model(cls, data: dict[str, Any]) -> dict[str, Any]:
        if not isinstance(data, dict):
            return data
        instances = data.get("instances")
        if instances:
            for name, instance in instances.items():
                if isinstance(instance, dict):
                    instance["name"] = name
        return data

    def create_port(self, name: str) -> NetlistPort:
        p = NetlistPort(name=name)
        self.ports.append(p)
        return p

    def create_inst(
        self, name: str, kcl: str, component: str, settings: dict[str, JSONSerializable]
    ) -> None:
        self.instances[name] = NetlistInstance(
            kcl=kcl, component=component, settings=settings, name=name
        )

    def create_net(self, *ports: PortRef | NetlistPort) -> None:
        net_ports: list[PortRef | NetlistPort] = []
        for port in ports:
            if isinstance(port, PortRef):
                if port.instance not in self.instances:
                    raise ValueError("Unknown instance ", port.instance)
                inst = self.instances[port.instance]
                if isinstance(port, PortArrayRef):
                    if port.ia == 1 and port.ib == 1:
                        net_ports.append(
                            PortRef(instance=port.instance, port=port.port)
                        )
                        continue
                    if not inst.array:
                        raise ValueError(
                            f"Instance {port.instance} is not an array instance. "
                            f"But an array portref was requested {port=}"
                        )
                    if port.ia > inst.array.na:
                        raise ValueError(
                            f"Instance {port.instance} has only {inst.array.na}"
                            " elements in `na` direction"
                        )
                    if port.ib > inst.array.nb:
                        raise ValueError(
                            f"Instance {port.instance} has only {inst.array.nb}"
                            " elements in `na` direction"
                        )
                net_ports.append(port)
            else:
                net_ports.append(NetlistPort(name=port.name))

        self.nets.append(Net(net_ports))

    def lvs_equivalent(
        self,
        cell_name: str,
        equivalent_ports: dict[str, list[list[str]]],
        port_mapping: dict[str, dict[str | None, str]] | None = None,
    ) -> Netlist:
        """Get an equivalent netlist.

        This is is useful for when there are components such as pads which have
        more than one port which electrically are equivalent (same metal plane).

        Args:
            cell_name: Name of the netlist. This is usually `c.name` or similar.
                Used to retrieve equivalent ports for self.
            equivalent_ports: Dict containing cellname mapping vs lists of equivalent
                port names.
            port_mapping: Passed as a dict of
                `{c_name: {port_name: equivalent_name, ...}, ...}`.
                If not given is constructed in function.

        Returns:
            New netlist with equivalent ports mapped to their equivalent (usually first
            port name in the list of equivalents).
        """
        if port_mapping is None:
            port_mapping = defaultdict(dict)
            for cell_name_, list_of_port_lists in equivalent_ports.items():
                for port_list in list_of_port_lists:
                    if port_list:
                        p1 = port_list[0]
                        for port in port_list:
                            port_mapping[cell_name_][port] = p1
        ports_per_inst: dict[str, list[PortRef]] = defaultdict(list)
        net_for_port: dict[PortRef, Net] = {}

        matched_insts: list[str] = [
            inst.name
            for inst in self.instances.values()
            if inst.component in equivalent_ports
        ]
        changed_nets_dict: dict[PortRef, set[Net]] = defaultdict(set)
        all_changed_nets: set[Net] = set()
        nl = self.model_copy(deep=True)
        for net in nl.nets:
            for netport in net.root:
                if isinstance(netport, PortRef):
                    ports_per_inst[netport.instance].append(netport)
                    net_for_port[netport] = net
                    if netport.instance in matched_insts:
                        port_name = port_mapping[
                            nl.instances[netport.instance].component
                        ].get(netport.port)
                        if port_name is not None:
                            netport.port = port_name
                            changed_nets_dict[netport].add(net)
                            all_changed_nets.add(net)

        targets = {net: NetMergeTarget() for net in all_changed_nets}

        for changed_nets in changed_nets_dict.values():
            if len(changed_nets) > 1:
                changed_nets_ = list(changed_nets)
                t = targets[changed_nets_[0]].find_target()
                for net in changed_nets_:
                    target = targets[net].find_target()
                    target.set_target(t)

        nets_per_target: dict[NetMergeTarget, set[Net]] = defaultdict(set)
        for net in all_changed_nets:
            nets_per_target[targets[net].find_target()].add(net)

        del_nets: set[Net] = set()
        new_nets: list[Net] = []
        ports = {port.name: port for port in nl.ports}
        for nets in nets_per_target.values():
            new_net = Net(root=[])
            refs: set[PortRef | NetlistPort] = set()
            for net in nets:
                for portorref in net.root:
                    if isinstance(portorref, PortRef):
                        refs.add(portorref)
                    else:
                        refs.add(ports[port_mapping[cell_name][portorref.name]])
                del_nets.add(net)
            new_net.root.extend(list(refs))
            new_nets.append(new_net)

        nl.ports = list(set(ports.values()))
        nl.nets = list(set(nl.nets) - del_nets) + new_nets

        nl.sort()

        return nl

lvs_equivalent

lvs_equivalent(
    cell_name: str,
    equivalent_ports: dict[str, list[list[str]]],
    port_mapping: dict[str, dict[str | None, str]]
    | None = None,
) -> Netlist

Get an equivalent netlist.

This is is useful for when there are components such as pads which have more than one port which electrically are equivalent (same metal plane).

Parameters:

Name Type Description Default
cell_name str

Name of the netlist. This is usually c.name or similar. Used to retrieve equivalent ports for self.

required
equivalent_ports dict[str, list[list[str]]]

Dict containing cellname mapping vs lists of equivalent port names.

required
port_mapping dict[str, dict[str | None, str]] | None

Passed as a dict of {c_name: {port_name: equivalent_name, ...}, ...}. If not given is constructed in function.

None

Returns:

Type Description
Netlist

New netlist with equivalent ports mapped to their equivalent (usually first

Netlist

port name in the list of equivalents).

Source code in kfactory/netlist.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
def lvs_equivalent(
    self,
    cell_name: str,
    equivalent_ports: dict[str, list[list[str]]],
    port_mapping: dict[str, dict[str | None, str]] | None = None,
) -> Netlist:
    """Get an equivalent netlist.

    This is is useful for when there are components such as pads which have
    more than one port which electrically are equivalent (same metal plane).

    Args:
        cell_name: Name of the netlist. This is usually `c.name` or similar.
            Used to retrieve equivalent ports for self.
        equivalent_ports: Dict containing cellname mapping vs lists of equivalent
            port names.
        port_mapping: Passed as a dict of
            `{c_name: {port_name: equivalent_name, ...}, ...}`.
            If not given is constructed in function.

    Returns:
        New netlist with equivalent ports mapped to their equivalent (usually first
        port name in the list of equivalents).
    """
    if port_mapping is None:
        port_mapping = defaultdict(dict)
        for cell_name_, list_of_port_lists in equivalent_ports.items():
            for port_list in list_of_port_lists:
                if port_list:
                    p1 = port_list[0]
                    for port in port_list:
                        port_mapping[cell_name_][port] = p1
    ports_per_inst: dict[str, list[PortRef]] = defaultdict(list)
    net_for_port: dict[PortRef, Net] = {}

    matched_insts: list[str] = [
        inst.name
        for inst in self.instances.values()
        if inst.component in equivalent_ports
    ]
    changed_nets_dict: dict[PortRef, set[Net]] = defaultdict(set)
    all_changed_nets: set[Net] = set()
    nl = self.model_copy(deep=True)
    for net in nl.nets:
        for netport in net.root:
            if isinstance(netport, PortRef):
                ports_per_inst[netport.instance].append(netport)
                net_for_port[netport] = net
                if netport.instance in matched_insts:
                    port_name = port_mapping[
                        nl.instances[netport.instance].component
                    ].get(netport.port)
                    if port_name is not None:
                        netport.port = port_name
                        changed_nets_dict[netport].add(net)
                        all_changed_nets.add(net)

    targets = {net: NetMergeTarget() for net in all_changed_nets}

    for changed_nets in changed_nets_dict.values():
        if len(changed_nets) > 1:
            changed_nets_ = list(changed_nets)
            t = targets[changed_nets_[0]].find_target()
            for net in changed_nets_:
                target = targets[net].find_target()
                target.set_target(t)

    nets_per_target: dict[NetMergeTarget, set[Net]] = defaultdict(set)
    for net in all_changed_nets:
        nets_per_target[targets[net].find_target()].add(net)

    del_nets: set[Net] = set()
    new_nets: list[Net] = []
    ports = {port.name: port for port in nl.ports}
    for nets in nets_per_target.values():
        new_net = Net(root=[])
        refs: set[PortRef | NetlistPort] = set()
        for net in nets:
            for portorref in net.root:
                if isinstance(portorref, PortRef):
                    refs.add(portorref)
                else:
                    refs.add(ports[port_mapping[cell_name][portorref.name]])
            del_nets.add(net)
        new_net.root.extend(list(refs))
        new_nets.append(new_net)

    nl.ports = list(set(ports.values()))
    nl.nets = list(set(nl.nets) - del_nets) + new_nets

    nl.sort()

    return nl

Pin

Bases: ProtoPin[int]

Source code in kfactory/pin.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
class Pin(ProtoPin[int]):
    def __getitem__(self, key: int | str | None) -> Port:
        if isinstance(key, int):
            return Port(base=list(self._base.ports)[key])
        try:
            return Port(
                base=next(
                    filter(lambda port_base: port_base.name == key, self._base.ports)
                )
            )
        except StopIteration as e:
            raise KeyError(
                f"{key=} is not a valid port name or index within the pin. "
                f"Available ports: {[v.name for v in self.ports]}"
            ) from e

    @property
    def ports(self) -> list[Port]:
        return [Port(base=pb) for pb in self._base.ports]

    @ports.setter
    def ports(self, value: Iterable[ProtoPort[Any]]) -> None:
        self._base.ports = [p.base for p in value]

    def copy(
        self,
        trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
        post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    ) -> Pin:
        """Get a copy of a pin.

        Transformation order which results in `copy.trans`:
            - Trans: `trans * pin.trans * post_trans`
            - DCplxTrans: `trans * pin.dcplx_trans * post_trans`

        Args:
            trans: an optional transformation applied to the pin to be copied.
            post_trans: transformation to apply to the pin after copying.

        Returns:
            pin: a copy of the pin
        """
        return Pin(base=self._base.transformed(trans=trans, post_trans=post_trans))

__getitem__

__getitem__(key: int | str | None) -> Port

Get a port in the pin by index or name.

Source code in kfactory/pin.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def __getitem__(self, key: int | str | None) -> Port:
    if isinstance(key, int):
        return Port(base=list(self._base.ports)[key])
    try:
        return Port(
            base=next(
                filter(lambda port_base: port_base.name == key, self._base.ports)
            )
        )
    except StopIteration as e:
        raise KeyError(
            f"{key=} is not a valid port name or index within the pin. "
            f"Available ports: {[v.name for v in self.ports]}"
        ) from e

copy

copy(
    trans: Trans | DCplxTrans = kdb.Trans.R0,
    post_trans: Trans | DCplxTrans = kdb.Trans.R0,
) -> Pin

Get a copy of a pin.

Transformation order which results in copy.trans: - Trans: trans * pin.trans * post_trans - DCplxTrans: trans * pin.dcplx_trans * post_trans

Parameters:

Name Type Description Default
trans Trans | DCplxTrans

an optional transformation applied to the pin to be copied.

R0
post_trans Trans | DCplxTrans

transformation to apply to the pin after copying.

R0

Returns:

Name Type Description
pin Pin

a copy of the pin

Source code in kfactory/pin.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def copy(
    self,
    trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
) -> Pin:
    """Get a copy of a pin.

    Transformation order which results in `copy.trans`:
        - Trans: `trans * pin.trans * post_trans`
        - DCplxTrans: `trans * pin.dcplx_trans * post_trans`

    Args:
        trans: an optional transformation applied to the pin to be copied.
        post_trans: transformation to apply to the pin after copying.

    Returns:
        pin: a copy of the pin
    """
    return Pin(base=self._base.transformed(trans=trans, post_trans=post_trans))

Pins

Bases: ProtoPins[int]

Source code in kfactory/pins.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
class Pins(ProtoPins[int]):
    yaml_tag: ClassVar[str] = "!Pins"

    def __iter__(self) -> Iterator[Pin]:
        """Iterator, that allows for loops etc to directly access the object."""
        yield from (Pin(base=b) for b in self._bases)

    def __getitem__(self, key: int | str | None) -> Pin:
        """Get a specific pin by name."""
        if isinstance(key, int):
            return Pin(base=self._bases[key])
        try:
            return Pin(base=next(filter(lambda base: base.name == key, self._bases)))
        except StopIteration as e:
            raise KeyError(
                f"{key=} is not a valid pin name or index. "
                f"Available pins: {[v.name for v in self._bases]}"
            ) from e

    def create_pin(
        self,
        *,
        ports: Iterable[ProtoPort[Any]],
        name: str | None = None,
        pin_type: str = "DC",
        info: dict[str, int | float | str] | None = None,
    ) -> Pin:
        """Add a pin to Pins."""
        if info is None:
            info = {}
        info_ = Info(**info)
        if len(list(ports)) < 1:
            raise ValueError(
                f"At least one port must provided to create pin named {name}."
            )
        port_bases = []
        for port in ports:
            port_base = port.base
            if port.kcl != self.kcl:
                raise ValueError(
                    "Cannot add a pin which belongs to a different layout or cell to a"
                    f" cell. {port=}, {self.kcl!r}"
                )
            port_bases.append(port_base)

        base_ = BasePin(
            name=name, kcl=self.kcl, ports=port_bases, pin_type=pin_type, info=info_
        )
        self._bases.append(base_)

        return Pin(base=base_)

    def get_all_named(self) -> Mapping[str, Pin]:
        """Get all pins in a dictionary with names as keys."""
        return {v.name: Pin(base=v) for v in self._bases if v.name is not None}

    def print(self, unit: Literal["dbu", "um"] | None = None) -> None:
        """Pretty print ports."""
        config.console.print(pprint_pins(self, unit=unit))

__getitem__

__getitem__(key: int | str | None) -> Pin

Get a specific pin by name.

Source code in kfactory/pins.py
122
123
124
125
126
127
128
129
130
131
132
def __getitem__(self, key: int | str | None) -> Pin:
    """Get a specific pin by name."""
    if isinstance(key, int):
        return Pin(base=self._bases[key])
    try:
        return Pin(base=next(filter(lambda base: base.name == key, self._bases)))
    except StopIteration as e:
        raise KeyError(
            f"{key=} is not a valid pin name or index. "
            f"Available pins: {[v.name for v in self._bases]}"
        ) from e

__iter__

__iter__() -> Iterator[Pin]

Iterator, that allows for loops etc to directly access the object.

Source code in kfactory/pins.py
118
119
120
def __iter__(self) -> Iterator[Pin]:
    """Iterator, that allows for loops etc to directly access the object."""
    yield from (Pin(base=b) for b in self._bases)

create_pin

create_pin(
    *,
    ports: Iterable[ProtoPort[Any]],
    name: str | None = None,
    pin_type: str = "DC",
    info: dict[str, int | float | str] | None = None,
) -> Pin

Add a pin to Pins.

Source code in kfactory/pins.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def create_pin(
    self,
    *,
    ports: Iterable[ProtoPort[Any]],
    name: str | None = None,
    pin_type: str = "DC",
    info: dict[str, int | float | str] | None = None,
) -> Pin:
    """Add a pin to Pins."""
    if info is None:
        info = {}
    info_ = Info(**info)
    if len(list(ports)) < 1:
        raise ValueError(
            f"At least one port must provided to create pin named {name}."
        )
    port_bases = []
    for port in ports:
        port_base = port.base
        if port.kcl != self.kcl:
            raise ValueError(
                "Cannot add a pin which belongs to a different layout or cell to a"
                f" cell. {port=}, {self.kcl!r}"
            )
        port_bases.append(port_base)

    base_ = BasePin(
        name=name, kcl=self.kcl, ports=port_bases, pin_type=pin_type, info=info_
    )
    self._bases.append(base_)

    return Pin(base=base_)

get_all_named

get_all_named() -> Mapping[str, Pin]

Get all pins in a dictionary with names as keys.

Source code in kfactory/pins.py
167
168
169
def get_all_named(self) -> Mapping[str, Pin]:
    """Get all pins in a dictionary with names as keys."""
    return {v.name: Pin(base=v) for v in self._bases if v.name is not None}

print

print(unit: Literal['dbu', 'um'] | None = None) -> None

Pretty print ports.

Source code in kfactory/pins.py
171
172
173
def print(self, unit: Literal["dbu", "um"] | None = None) -> None:
    """Pretty print ports."""
    config.console.print(pprint_pins(self, unit=unit))

Port

Bases: ProtoPort[int]

A port is the photonics equivalent to a pin in electronics.

In addition to the location and layer that defines a pin, a port also contains an orientation and a width. This can be fully represented with a transformation, integer and layer_index.

Attributes:

Name Type Description
name str | None

String to name the port.

width int

The width of the port in dbu.

trans Trans

Transformation in dbu. If the port can be represented in 90° intervals this is the safe way to do so.

dcplx_trans

Transformation in micrometer. The port will autoconvert between trans and dcplx_trans on demand.

port_type str

A string defining the type of the port

layer LayerEnum | int

Index of the layer or a LayerEnum that acts like an integer, but can contain layer number and datatype

info Info

A dictionary with additional info. Not reflected in GDS. Copy will make a (shallow) copy of it.

d Info

Access port info in micrometer basis such as width and center / angle.

kcl KCLayout

Link to the layout this port resides in.

Source code in kfactory/port.py
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
class Port(ProtoPort[int]):
    """A port is the photonics equivalent to a pin in electronics.

    In addition to the location and layer
    that defines a pin, a port also contains an orientation and a width.
    This can be fully represented with a transformation, integer and layer_index.


    Attributes:
        name: String to name the port.
        width: The width of the port in dbu.
        trans: Transformation in dbu. If the port can be represented in 90° intervals
            this is the safe way to do so.
        dcplx_trans: Transformation in micrometer. The port will autoconvert between
            trans and dcplx_trans on demand.
        port_type: A string defining the type of the port
        layer: Index of the layer or a LayerEnum that acts like an integer, but can
            contain layer number and datatype
        info: A dictionary with additional info. Not reflected in GDS. Copy will make a
            (shallow) copy of it.
        d: Access port info in micrometer basis such as width and center / angle.
        kcl: Link to the layout this port resides in.
    """

    @overload
    def __init__(
        self,
        *,
        name: str | None = None,
        width: int,
        layer: LayerEnum | int,
        trans: kdb.Trans | str,
        kcl: KCLayout | None = None,
        port_type: str = "optical",
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        *,
        name: str | None = None,
        width: int,
        layer: LayerEnum | int,
        dcplx_trans: kdb.DCplxTrans | str,
        kcl: KCLayout | None = None,
        port_type: str = "optical",
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: int,
        layer: LayerEnum | int,
        port_type: str = "optical",
        angle: int,
        center: tuple[int, int],
        mirror_x: bool = False,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: int,
        layer_info: kdb.LayerInfo,
        trans: kdb.Trans | str,
        kcl: KCLayout | None = None,
        port_type: str = "optical",
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: int,
        layer_info: kdb.LayerInfo,
        dcplx_trans: kdb.DCplxTrans | str,
        kcl: KCLayout | None = None,
        port_type: str = "optical",
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        width: int,
        layer_info: kdb.LayerInfo,
        port_type: str = "optical",
        angle: int,
        center: tuple[int, int],
        mirror_x: bool = False,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        cross_section: CrossSection | SymmetricalCrossSection,
        port_type: str = "optical",
        angle: int,
        center: tuple[int, int],
        mirror_x: bool = False,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        cross_section: CrossSection | SymmetricalCrossSection,
        trans: kdb.Trans | str,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
        port_type: str = "optical",
    ) -> None: ...

    @overload
    def __init__(
        self,
        name: str | None = None,
        *,
        cross_section: CrossSection | SymmetricalCrossSection,
        dcplx_trans: kdb.DCplxTrans | str,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
        port_type: str = "optical",
    ) -> None: ...

    @overload
    def __init__(self, *, base: BasePort) -> None: ...

    @overload
    def __init__(self, *, port: ProtoPort[Any]) -> None: ...

    def __init__(
        self,
        name: str | None = None,
        *,
        width: int | None = None,
        layer: int | None = None,
        layer_info: kdb.LayerInfo | None = None,
        port_type: str = "optical",
        trans: kdb.Trans | str | None = None,
        dcplx_trans: kdb.DCplxTrans | str | None = None,
        angle: int | None = None,
        center: tuple[int, int] | None = None,
        mirror_x: bool = False,
        port: ProtoPort[Any] | None = None,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] | None = None,
        cross_section: CrossSection | SymmetricalCrossSection | None = None,
        base: BasePort | None = None,
    ) -> None:
        """Create a port from dbu or um based units."""
        if info is None:
            info = {}
        if base is not None:
            self._base = base
            return
        if port is not None:
            self._base = port.base.__copy__()
            return
        info_ = Info(**info)
        from .layout import get_default_kcl

        kcl_ = kcl or get_default_kcl()
        if cross_section is None:
            if layer_info is None:
                if layer is None:
                    raise ValueError("layer or layer_info for a port must be defined")
                layer_info = kcl_.layout.get_info(layer)
            if width is None:
                raise ValueError(
                    "any width and layer, or a cross_section must be given if the"
                    " 'port is None'"
                )
            cross_section_ = kcl_.get_symmetrical_cross_section(
                CrossSectionSpec(layer=layer_info, width=width)
            )
        elif isinstance(cross_section, SymmetricalCrossSection):
            cross_section_ = cross_section
        else:
            cross_section_ = cross_section.base
        if trans is not None:
            trans_ = kdb.Trans.from_s(trans) if isinstance(trans, str) else trans.dup()
            self._base = BasePort(
                name=name,
                kcl=kcl_,
                cross_section=cross_section_,
                trans=trans_,
                info=info_,
                port_type=port_type,
            )
        elif dcplx_trans is not None:
            if isinstance(dcplx_trans, str):
                dcplx_trans_ = kdb.DCplxTrans.from_s(dcplx_trans)
            else:
                dcplx_trans_ = dcplx_trans.dup()
            self._base = BasePort(
                name=name,
                kcl=kcl_,
                cross_section=cross_section_,
                trans=kdb.Trans.R0,
                info=info_,
                port_type=port_type,
            )
            self.dcplx_trans = dcplx_trans_
        elif angle is not None:
            assert center is not None
            trans_ = kdb.Trans(angle, mirror_x, *center)
            self._base = BasePort(
                name=name,
                kcl=kcl_,
                cross_section=cross_section_,
                trans=trans_,
                info=info_,
                port_type=port_type,
            )
        else:
            raise ValueError("Missing port parameters given")

    def copy(
        self,
        trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
        post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    ) -> Port:
        """Get a copy of a port.

        Transformation order which results in `copy.trans`:
            - Trans: `trans * port.trans * post_trans`
            - DCplxTrans: `trans * port.dcplx_trans * post_trans`

        Args:
            trans: an optional transformation applied to the port to be copied.
            post_trans: transformation to apply to the port after copying.

        Returns:
            port: a copy of the port
        """
        return Port(base=self._base.transformed(trans=trans, post_trans=post_trans))

    def copy_polar(
        self, d: int = 0, d_orth: int = 0, angle: int = 2, mirror: bool = False
    ) -> Port:
        """Get a polar copy of the port.

        This will return a port which is transformed relatively to the original port's
        transformation (orientation, angle and position).

        Args:
            d: The distance to the old port
            d_orth: Orthogonal distance (positive is positive y for a port which is
                facing angle=0°)
            angle: Relative angle to the original port (0=0°,1=90°,2=180°,3=270°).
            mirror: Whether to mirror the port relative to the original port.

        Returns:
            Port copied relative to it's current position and angle/orientation.
        """
        return self.copy(post_trans=kdb.Trans(angle, mirror, d, d_orth))

    @property
    def x(self) -> int:
        """X coordinate of the port in dbu."""
        return self.ix

    @x.setter
    def x(self, value: int) -> None:
        self.ix = value

    @property
    def y(self) -> int:
        """Y coordinate of the port in dbu."""
        return self.iy

    @y.setter
    def y(self, value: int) -> None:
        self.iy = value

    @property
    def width(self) -> int:
        """Width of the port in um."""
        return self.iwidth

    @property
    def cross_section(self) -> CrossSection:
        """Get the cross section of the port."""
        return CrossSection(kcl=self._base.kcl, base=self._base.cross_section)

    @cross_section.setter
    def cross_section(
        self, value: SymmetricalCrossSection | TCrossSection[Any]
    ) -> None:
        if isinstance(value, SymmetricalCrossSection):
            self._base.cross_section = value
            return
        self._base.cross_section = value.base

cross_section property writable

cross_section: CrossSection

Get the cross section of the port.

dcplx_trans instance-attribute

dcplx_trans = dcplx_trans_

Complex transformation (um based).

If the internal transformation is simple, return a complex copy.

The setter will set a complex transformation and overwrite the internal transformation (set simple to None and the complex to the provided value.

width property

width: int

Width of the port in um.

x property writable

x: int

X coordinate of the port in dbu.

y property writable

y: int

Y coordinate of the port in dbu.

__init__

__init__(
    *,
    name: str | None = None,
    width: int,
    layer: LayerEnum | int,
    trans: Trans | str,
    kcl: KCLayout | None = None,
    port_type: str = "optical",
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    *,
    name: str | None = None,
    width: int,
    layer: LayerEnum | int,
    dcplx_trans: DCplxTrans | str,
    kcl: KCLayout | None = None,
    port_type: str = "optical",
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    width: int,
    layer: LayerEnum | int,
    port_type: str = "optical",
    angle: int,
    center: tuple[int, int],
    mirror_x: bool = False,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    width: int,
    layer_info: LayerInfo,
    trans: Trans | str,
    kcl: KCLayout | None = None,
    port_type: str = "optical",
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    width: int,
    layer_info: LayerInfo,
    dcplx_trans: DCplxTrans | str,
    kcl: KCLayout | None = None,
    port_type: str = "optical",
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    width: int,
    layer_info: LayerInfo,
    port_type: str = "optical",
    angle: int,
    center: tuple[int, int],
    mirror_x: bool = False,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    cross_section: CrossSection | SymmetricalCrossSection,
    port_type: str = "optical",
    angle: int,
    center: tuple[int, int],
    mirror_x: bool = False,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
) -> None
__init__(
    name: str | None = None,
    *,
    cross_section: CrossSection | SymmetricalCrossSection,
    trans: Trans | str,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
    port_type: str = "optical",
) -> None
__init__(
    name: str | None = None,
    *,
    cross_section: CrossSection | SymmetricalCrossSection,
    dcplx_trans: DCplxTrans | str,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
    port_type: str = "optical",
) -> None
__init__(*, base: BasePort) -> None
__init__(*, port: ProtoPort[Any]) -> None
__init__(
    name: str | None = None,
    *,
    width: int | None = None,
    layer: int | None = None,
    layer_info: LayerInfo | None = None,
    port_type: str = "optical",
    trans: Trans | str | None = None,
    dcplx_trans: DCplxTrans | str | None = None,
    angle: int | None = None,
    center: tuple[int, int] | None = None,
    mirror_x: bool = False,
    port: ProtoPort[Any] | None = None,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] | None = None,
    cross_section: CrossSection
    | SymmetricalCrossSection
    | None = None,
    base: BasePort | None = None,
) -> None

Create a port from dbu or um based units.

Source code in kfactory/port.py
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
def __init__(
    self,
    name: str | None = None,
    *,
    width: int | None = None,
    layer: int | None = None,
    layer_info: kdb.LayerInfo | None = None,
    port_type: str = "optical",
    trans: kdb.Trans | str | None = None,
    dcplx_trans: kdb.DCplxTrans | str | None = None,
    angle: int | None = None,
    center: tuple[int, int] | None = None,
    mirror_x: bool = False,
    port: ProtoPort[Any] | None = None,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] | None = None,
    cross_section: CrossSection | SymmetricalCrossSection | None = None,
    base: BasePort | None = None,
) -> None:
    """Create a port from dbu or um based units."""
    if info is None:
        info = {}
    if base is not None:
        self._base = base
        return
    if port is not None:
        self._base = port.base.__copy__()
        return
    info_ = Info(**info)
    from .layout import get_default_kcl

    kcl_ = kcl or get_default_kcl()
    if cross_section is None:
        if layer_info is None:
            if layer is None:
                raise ValueError("layer or layer_info for a port must be defined")
            layer_info = kcl_.layout.get_info(layer)
        if width is None:
            raise ValueError(
                "any width and layer, or a cross_section must be given if the"
                " 'port is None'"
            )
        cross_section_ = kcl_.get_symmetrical_cross_section(
            CrossSectionSpec(layer=layer_info, width=width)
        )
    elif isinstance(cross_section, SymmetricalCrossSection):
        cross_section_ = cross_section
    else:
        cross_section_ = cross_section.base
    if trans is not None:
        trans_ = kdb.Trans.from_s(trans) if isinstance(trans, str) else trans.dup()
        self._base = BasePort(
            name=name,
            kcl=kcl_,
            cross_section=cross_section_,
            trans=trans_,
            info=info_,
            port_type=port_type,
        )
    elif dcplx_trans is not None:
        if isinstance(dcplx_trans, str):
            dcplx_trans_ = kdb.DCplxTrans.from_s(dcplx_trans)
        else:
            dcplx_trans_ = dcplx_trans.dup()
        self._base = BasePort(
            name=name,
            kcl=kcl_,
            cross_section=cross_section_,
            trans=kdb.Trans.R0,
            info=info_,
            port_type=port_type,
        )
        self.dcplx_trans = dcplx_trans_
    elif angle is not None:
        assert center is not None
        trans_ = kdb.Trans(angle, mirror_x, *center)
        self._base = BasePort(
            name=name,
            kcl=kcl_,
            cross_section=cross_section_,
            trans=trans_,
            info=info_,
            port_type=port_type,
        )
    else:
        raise ValueError("Missing port parameters given")

copy

copy(
    trans: Trans | DCplxTrans = kdb.Trans.R0,
    post_trans: Trans | DCplxTrans = kdb.Trans.R0,
) -> Port

Get a copy of a port.

Transformation order which results in copy.trans: - Trans: trans * port.trans * post_trans - DCplxTrans: trans * port.dcplx_trans * post_trans

Parameters:

Name Type Description Default
trans Trans | DCplxTrans

an optional transformation applied to the port to be copied.

R0
post_trans Trans | DCplxTrans

transformation to apply to the port after copying.

R0

Returns:

Name Type Description
port Port

a copy of the port

Source code in kfactory/port.py
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
def copy(
    self,
    trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
) -> Port:
    """Get a copy of a port.

    Transformation order which results in `copy.trans`:
        - Trans: `trans * port.trans * post_trans`
        - DCplxTrans: `trans * port.dcplx_trans * post_trans`

    Args:
        trans: an optional transformation applied to the port to be copied.
        post_trans: transformation to apply to the port after copying.

    Returns:
        port: a copy of the port
    """
    return Port(base=self._base.transformed(trans=trans, post_trans=post_trans))

copy_polar

copy_polar(
    d: int = 0,
    d_orth: int = 0,
    angle: int = 2,
    mirror: bool = False,
) -> Port

Get a polar copy of the port.

This will return a port which is transformed relatively to the original port's transformation (orientation, angle and position).

Parameters:

Name Type Description Default
d int

The distance to the old port

0
d_orth int

Orthogonal distance (positive is positive y for a port which is facing angle=0°)

0
angle int

Relative angle to the original port (0=0°,1=90°,2=180°,3=270°).

2
mirror bool

Whether to mirror the port relative to the original port.

False

Returns:

Type Description
Port

Port copied relative to it's current position and angle/orientation.

Source code in kfactory/port.py
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
def copy_polar(
    self, d: int = 0, d_orth: int = 0, angle: int = 2, mirror: bool = False
) -> Port:
    """Get a polar copy of the port.

    This will return a port which is transformed relatively to the original port's
    transformation (orientation, angle and position).

    Args:
        d: The distance to the old port
        d_orth: Orthogonal distance (positive is positive y for a port which is
            facing angle=0°)
        angle: Relative angle to the original port (0=0°,1=90°,2=180°,3=270°).
        mirror: Whether to mirror the port relative to the original port.

    Returns:
        Port copied relative to it's current position and angle/orientation.
    """
    return self.copy(post_trans=kdb.Trans(angle, mirror, d, d_orth))

Ports

Bases: ProtoPorts[int], ICreatePort

A collection of dbu ports.

It is not a traditional dictionary. Elements can be retrieved as in a traditional dictionary. But to keep tabs on names etc, the ports are stored as a list

Source code in kfactory/ports.py
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
class Ports(ProtoPorts[int], ICreatePort):
    """A collection of dbu ports.

    It is not a traditional dictionary. Elements can be retrieved as in a traditional
    dictionary. But to keep tabs on names etc, the ports are stored as a list
    """

    yaml_tag: ClassVar[str] = "!Ports"

    def __iter__(self) -> Iterator[Port]:
        """Iterator, that allows for loops etc to directly access the object."""
        yield from (Port(base=b) for b in self._bases)

    def add_port(
        self,
        *,
        port: ProtoPort[Any],
        name: str | None = None,
        keep_mirror: bool = False,
    ) -> Port:
        """Add a port object.

        Args:
            port: The port to add
            name: Overwrite the name of the port
            keep_mirror: Keep the mirror flag from the original port if `True`,
                else set [Port.trans.mirror][kfactory.kcell.Port.trans] (or the complex
                equivalent) to `False`.
        """
        if port.kcl == self.kcl:
            base = port.base.model_copy()
            if not keep_mirror:
                if base.trans is not None:
                    base.trans.mirror = False
                elif base.dcplx_trans is not None:
                    base.dcplx_trans.mirror = False
            if name is not None:
                base.name = name
            self._bases.append(base)
            port_ = Port(base=base)
        else:
            dcplx_trans = port.dcplx_trans.dup()
            if not keep_mirror:
                dcplx_trans.mirror = False
            base = port.base.model_copy()
            base.trans = kdb.Trans.R0
            base.dcplx_trans = None
            base.kcl = self.kcl
            base.cross_section = self.kcl.get_symmetrical_cross_section(
                port.cross_section.base.to_dtype(port.kcl)
            )
            if name is not None:
                base.name = name
            port_ = Port(base=base)
            port_.dcplx_trans = dcplx_trans
            self._bases.append(port_.base)
        return port_

    def get_all_named(self) -> Mapping[str, Port]:
        """Get all ports in a dictionary with names as keys."""
        return {v.name: Port(base=v) for v in self._bases if v.name is not None}

    @overload
    def __getitem__(self, key: int | str | None) -> Port:
        """Get a port by index or name."""

    @overload
    def __getitem__(self, key: slice) -> Self:
        """Get ports by slice."""

    def __getitem__(self, key: slice | int | str | None) -> Self | Port:
        if isinstance(key, int):
            return Port(base=self._bases[key])
        if isinstance(key, slice):
            return self.__class__(bases=self._bases[key], kcl=self.kcl)
        try:
            return Port(base=next(filter(lambda base: base.name == key, self._bases)))
        except StopIteration as e:
            raise KeyError(
                f"{key=} is not a valid port name or index. "
                f"Available ports: {[v.name for v in self._bases]}"
            ) from e

    def copy(
        self, rename_function: Callable[[Sequence[Port]], None] | None = None
    ) -> Self:
        """Get a copy of each port."""
        bases = [b.__copy__() for b in self._bases]
        if rename_function is not None:
            rename_function([Port(base=b) for b in bases])
        return self.__class__(bases=bases, kcl=self.kcl)

    def filter(
        self,
        angle: Angle | None = None,
        orientation: float | None = None,
        layer: LayerEnum | int | None = None,
        port_type: str | None = None,
        regex: str | None = None,
    ) -> list[Port]:
        """Filter ports.

        Args:
            angle: Filter by angle. 0, 1, 2, 3.
            orientation: Filter by orientation in degrees.
            layer: Filter by layer.
            port_type: Filter by port type.
            regex: Filter by regex of the name.
        """
        return _filter_ports(
            (Port(base=b) for b in self._bases),
            angle,
            orientation,
            layer,
            port_type,
            regex,
        )

    def __repr__(self) -> str:
        """Representation of the Ports as strings."""
        return repr([repr(DPort(base=b)) for b in self._bases])

__iter__

__iter__() -> Iterator[Port]

Iterator, that allows for loops etc to directly access the object.

Source code in kfactory/ports.py
671
672
673
def __iter__(self) -> Iterator[Port]:
    """Iterator, that allows for loops etc to directly access the object."""
    yield from (Port(base=b) for b in self._bases)

__repr__

__repr__() -> str

Representation of the Ports as strings.

Source code in kfactory/ports.py
780
781
782
def __repr__(self) -> str:
    """Representation of the Ports as strings."""
    return repr([repr(DPort(base=b)) for b in self._bases])

add_port

add_port(
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> Port

Add a port object.

Parameters:

Name Type Description Default
port ProtoPort[Any]

The port to add

required
name str | None

Overwrite the name of the port

None
keep_mirror bool

Keep the mirror flag from the original port if True, else set Port.trans.mirror (or the complex equivalent) to False.

False
Source code in kfactory/ports.py
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
def add_port(
    self,
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> Port:
    """Add a port object.

    Args:
        port: The port to add
        name: Overwrite the name of the port
        keep_mirror: Keep the mirror flag from the original port if `True`,
            else set [Port.trans.mirror][kfactory.kcell.Port.trans] (or the complex
            equivalent) to `False`.
    """
    if port.kcl == self.kcl:
        base = port.base.model_copy()
        if not keep_mirror:
            if base.trans is not None:
                base.trans.mirror = False
            elif base.dcplx_trans is not None:
                base.dcplx_trans.mirror = False
        if name is not None:
            base.name = name
        self._bases.append(base)
        port_ = Port(base=base)
    else:
        dcplx_trans = port.dcplx_trans.dup()
        if not keep_mirror:
            dcplx_trans.mirror = False
        base = port.base.model_copy()
        base.trans = kdb.Trans.R0
        base.dcplx_trans = None
        base.kcl = self.kcl
        base.cross_section = self.kcl.get_symmetrical_cross_section(
            port.cross_section.base.to_dtype(port.kcl)
        )
        if name is not None:
            base.name = name
        port_ = Port(base=base)
        port_.dcplx_trans = dcplx_trans
        self._bases.append(port_.base)
    return port_

copy

copy(
    rename_function: Callable[[Sequence[Port]], None]
    | None = None,
) -> Self

Get a copy of each port.

Source code in kfactory/ports.py
745
746
747
748
749
750
751
752
def copy(
    self, rename_function: Callable[[Sequence[Port]], None] | None = None
) -> Self:
    """Get a copy of each port."""
    bases = [b.__copy__() for b in self._bases]
    if rename_function is not None:
        rename_function([Port(base=b) for b in bases])
    return self.__class__(bases=bases, kcl=self.kcl)

filter

filter(
    angle: Angle | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> list[Port]

Filter ports.

Parameters:

Name Type Description Default
angle Angle | None

Filter by angle. 0, 1, 2, 3.

None
orientation float | None

Filter by orientation in degrees.

None
layer LayerEnum | int | None

Filter by layer.

None
port_type str | None

Filter by port type.

None
regex str | None

Filter by regex of the name.

None
Source code in kfactory/ports.py
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
def filter(
    self,
    angle: Angle | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> list[Port]:
    """Filter ports.

    Args:
        angle: Filter by angle. 0, 1, 2, 3.
        orientation: Filter by orientation in degrees.
        layer: Filter by layer.
        port_type: Filter by port type.
        regex: Filter by regex of the name.
    """
    return _filter_ports(
        (Port(base=b) for b in self._bases),
        angle,
        orientation,
        layer,
        port_type,
        regex,
    )

get_all_named

get_all_named() -> Mapping[str, Port]

Get all ports in a dictionary with names as keys.

Source code in kfactory/ports.py
720
721
722
def get_all_named(self) -> Mapping[str, Port]:
    """Get all ports in a dictionary with names as keys."""
    return {v.name: Port(base=v) for v in self._bases if v.name is not None}

ProtoPin

Bases: ABC, Generic[TUnit]

Base class for kf.Pin, kf.DPin.

Source code in kfactory/pin.py
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
class ProtoPin(ABC, Generic[TUnit]):
    """Base class for kf.Pin, kf.DPin."""

    yaml_tag: str = "!Pin"
    _base: BasePin

    def __init__(self, *, base: BasePin) -> None:
        self._base = base

    @property
    def base(self) -> BasePin:
        """Get the BasePin associated with this Pin."""
        return self._base

    @property
    def name(self) -> str | None:
        """Name of the pin."""
        return self._base.name

    @name.setter
    def name(self, value: str | None) -> None:
        self._base.name = value

    @property
    def kcl(self) -> KCLayout:
        """KCLayout associated to the pin."""
        return self._base.kcl

    @kcl.setter
    def kcl(self, value: KCLayout) -> None:
        self._base.kcl = value

    @property
    def pin_type(self) -> str:
        """Type of the pin."""
        return self._base.pin_type

    @pin_type.setter
    def pin_type(self, value: str) -> None:
        self._base.pin_type = value

    @property
    def info(self) -> Info:
        """Additional info about the pin."""
        return self._base.info

    @info.setter
    def info(self, value: Info) -> None:
        self._base.info = value

    @property
    @abstractmethod
    def ports(self) -> list[Any]: ...  # because mypy... should be list[ProtoPort[Any]]

    @ports.setter
    def ports(self, value: Iterable[TPort_co]) -> None: ...

    def to_itype(self) -> Pin:
        """Convert the pin to a dbu pin."""
        return Pin(base=self._base)

    def to_dtype(self) -> DPin:
        """Convert the pin to a um pin."""
        return DPin(base=self._base)

    def __repr__(self) -> str:
        """String representation of pin."""
        return (
            f"{self.__class__.__name__}({self.name},"
            f"ports={self.ports}, pin_type={self.pin_type})"
        )

    @abstractmethod
    def __getitem__(self, key: int | str | None) -> ProtoPort[TUnit]:
        """Get a port in the pin by index or name."""
        ...

    @abstractmethod
    def copy(
        self,
        trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
        post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    ) -> ProtoPin[TUnit]:
        """Copy the port with a transformation."""
        ...

base property

base: BasePin

Get the BasePin associated with this Pin.

info property writable

info: Info

Additional info about the pin.

kcl property writable

kcl: KCLayout

KCLayout associated to the pin.

name property writable

name: str | None

Name of the pin.

pin_type property writable

pin_type: str

Type of the pin.

__getitem__ abstractmethod

__getitem__(key: int | str | None) -> ProtoPort[TUnit]

Get a port in the pin by index or name.

Source code in kfactory/pin.py
126
127
128
129
@abstractmethod
def __getitem__(self, key: int | str | None) -> ProtoPort[TUnit]:
    """Get a port in the pin by index or name."""
    ...

__repr__

__repr__() -> str

String representation of pin.

Source code in kfactory/pin.py
119
120
121
122
123
124
def __repr__(self) -> str:
    """String representation of pin."""
    return (
        f"{self.__class__.__name__}({self.name},"
        f"ports={self.ports}, pin_type={self.pin_type})"
    )

copy abstractmethod

copy(
    trans: Trans | DCplxTrans = kdb.Trans.R0,
    post_trans: Trans | DCplxTrans = kdb.Trans.R0,
) -> ProtoPin[TUnit]

Copy the port with a transformation.

Source code in kfactory/pin.py
131
132
133
134
135
136
137
138
@abstractmethod
def copy(
    self,
    trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
) -> ProtoPin[TUnit]:
    """Copy the port with a transformation."""
    ...

to_dtype

to_dtype() -> DPin

Convert the pin to a um pin.

Source code in kfactory/pin.py
115
116
117
def to_dtype(self) -> DPin:
    """Convert the pin to a um pin."""
    return DPin(base=self._base)

to_itype

to_itype() -> Pin

Convert the pin to a dbu pin.

Source code in kfactory/pin.py
111
112
113
def to_itype(self) -> Pin:
    """Convert the pin to a dbu pin."""
    return Pin(base=self._base)

ProtoPort

Bases: Generic[TUnit], ABC

Base class for kf.Port, kf.DPort.

Source code in kfactory/port.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
class ProtoPort(Generic[TUnit], ABC):  # noqa: PYI059
    """Base class for kf.Port, kf.DPort."""

    yaml_tag: str = "!Port"
    _base: BasePort

    @abstractmethod
    def __init__(
        self,
        name: str | None = None,
        *,
        width: TUnit | None = None,
        layer: int | None = None,
        layer_info: kdb.LayerInfo | None = None,
        port_type: str = "optical",
        trans: kdb.Trans | str | None = None,
        dcplx_trans: kdb.DCplxTrans | str | None = None,
        angle: TUnit | None = None,
        center: tuple[TUnit, TUnit] | None = None,
        mirror_x: bool = False,
        port: Port | None = None,
        kcl: KCLayout | None = None,
        info: dict[str, int | float | str] = ...,
        cross_section: TCrossSection[TUnit] | None = None,
        base: BasePort | None = None,
    ) -> None:
        """Initialise a ProtoPort."""
        ...

    @property
    def base(self) -> BasePort:
        """Get the BasePort associated with this Port."""
        return self._base

    @property
    def kcl(self) -> KCLayout:
        """KCLayout associated to the prot."""
        return self._base.kcl

    @kcl.setter
    def kcl(self, value: KCLayout) -> None:
        self._base.kcl = value

    @property
    @abstractmethod
    def cross_section(self) -> TCrossSection[TUnit]:
        """Get the cross section of the port."""
        ...

    @cross_section.setter
    @abstractmethod
    def cross_section(
        self, value: SymmetricalCrossSection | TCrossSection[Any]
    ) -> None: ...

    @property
    def name(self) -> str | None:
        """Name of the port."""
        return self._base.name

    @name.setter
    def name(self, value: str | None) -> None:
        self._base.name = value

    @property
    def port_type(self) -> str:
        """Type of the port.

        Usually "optical" or "electrical".
        """
        return self._base.port_type

    @port_type.setter
    def port_type(self, value: str) -> None:
        self._base.port_type = value

    @property
    def info(self) -> Info:
        """Additional info about the port."""
        return self._base.info

    @info.setter
    def info(self, value: Info) -> None:
        self._base.info = value

    @property
    def layer(self) -> LayerEnum | int:
        """Get the layer index of the port.

        This corresponds to the port's cross section's main layer converted to the
        index.
        """
        return self.kcl.find_layer(
            self.cross_section.layer, allow_undefined_layers=True
        )

    @property
    def layer_info(self) -> kdb.LayerInfo:
        """Get the layer info of the port.

        This corresponds to the port's cross section's main layer.
        """
        return self.cross_section.layer

    def __eq__(self, other: object) -> bool:
        """Support for `port1 == port2` comparisons."""
        if isinstance(other, ProtoPort):
            return self._base == other._base
        return False

    @property
    def trans(self) -> kdb.Trans:
        """Simple Transformation of the Port.

        If this is set with the setter, it will overwrite any transformation or
        dcplx transformation
        """
        return (
            self._base.trans
            or kdb.ICplxTrans(self._base.dcplx_trans, self.kcl.layout.dbu).s_trans()
        )

    @trans.setter
    def trans(self, value: kdb.Trans) -> None:
        self._base.trans = value.dup()
        self._base.dcplx_trans = None

    @property
    def dcplx_trans(self) -> kdb.DCplxTrans:
        """Complex transformation (um based).

        If the internal transformation is simple, return a complex copy.

        The setter will set a complex transformation and overwrite the internal
        transformation (set simple to `None` and the complex to the provided value.
        """
        return self._base.dcplx_trans or kdb.DCplxTrans(
            self.trans.to_dtype(self.kcl.layout.dbu)
        )

    @dcplx_trans.setter
    def dcplx_trans(self, value: kdb.DCplxTrans) -> None:
        if value.is_complex() or value.disp != self.kcl.to_um(
            self.kcl.to_dbu(value.disp)
        ):
            self._base.dcplx_trans = value.dup()
            self._base.trans = None
        else:
            self._base.trans = kdb.ICplxTrans(value.dup(), self.kcl.dbu).s_trans()
            self._base.dcplx_trans = None

    def to_itype(self) -> Port:
        """Convert the port to a dbu port."""
        return Port(base=self._base)

    def to_dtype(self) -> DPort:
        """Convert the port to a um port."""
        return DPort(base=self._base)

    @property
    def angle(self) -> Angle:
        """Angle of the transformation.

        In the range of `[0,1,2,3]` which are increments in 90°.
        """
        return self.trans.angle

    @angle.setter
    def angle(self, value: int) -> None:
        self._base.trans = self.trans.dup()
        self._base.dcplx_trans = None
        self._base.trans.angle = value

    @property
    def orientation(self) -> float:
        """Returns orientation in degrees for gdsfactory compatibility.

        In the range of `[0,360)`
        """
        return self.dcplx_trans.angle

    @orientation.setter
    def orientation(self, value: float) -> None:
        """Set the orientation of the port."""
        if not self.dcplx_trans.is_complex():
            dcplx_trans = self.dcplx_trans
            dcplx_trans.angle = value
            self.dcplx_trans = dcplx_trans
        else:
            self._base.dcplx_trans = self.dcplx_trans
            self._base.dcplx_trans.angle = value

    @property
    def mirror(self) -> bool:
        """Returns `True`/`False` depending on the mirror flag on the transformation."""
        return self.trans.is_mirror()

    @mirror.setter
    def mirror(self, value: bool) -> None:
        """Setter for mirror flag on trans."""
        if self._base.trans:
            self._base.trans.mirror = value
        elif self._base.dcplx_trans:
            self._base.dcplx_trans.mirror = value

    @abstractmethod
    def copy(
        self,
        trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
        post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    ) -> ProtoPort[TUnit]:
        """Copy the port with a transformation."""
        ...

    @property
    def center(self) -> tuple[TUnit, TUnit]:
        """Returns port center."""
        return (self.x, self.y)

    @center.setter
    def center(self, value: tuple[TUnit, TUnit]) -> None:
        self.x = value[0]
        self.y = value[1]

    @property
    @abstractmethod
    def x(self) -> TUnit:
        """X coordinate of the port."""
        ...

    @x.setter
    @abstractmethod
    def x(self, value: TUnit) -> None: ...

    @property
    @abstractmethod
    def y(self) -> TUnit:
        """Y coordinate of the port."""
        ...

    @y.setter
    @abstractmethod
    def y(self, value: TUnit) -> None: ...

    @property
    @abstractmethod
    def width(self) -> TUnit:
        """Width of the port."""
        ...

    @property
    def ix(self) -> int:
        """X coordinate of the port in dbu."""
        return self.trans.disp.x

    @ix.setter
    def ix(self, value: int) -> None:
        if self._base.trans:
            vec = self._base.trans.disp
            vec.x = value
            self._base.trans.disp = vec
        elif self._base.dcplx_trans:
            vec = self.trans.disp
            vec.x = value
            self._base.dcplx_trans.disp = self.kcl.to_um(vec)

    @property
    def iy(self) -> int:
        """Y coordinate of the port in dbu."""
        return self.trans.disp.y

    @iy.setter
    def iy(self, value: int) -> None:
        if self._base.trans:
            vec = self._base.trans.disp
            vec.y = value
            self._base.trans.disp = vec
        elif self._base.dcplx_trans:
            vec = self.trans.disp
            vec.y = value
            self._base.dcplx_trans.disp = self.kcl.to_um(vec)

    @property
    def iwidth(self) -> int:
        """Width of the port in dbu."""
        return self._base.cross_section.width

    @property
    def dx(self) -> float:
        """X coordinate of the port in um."""
        return self.dcplx_trans.disp.x

    @dx.setter
    def dx(self, value: float) -> None:
        vec = self.dcplx_trans.disp
        vec.x = value
        if self._base.trans:
            self._base.trans.disp = self.kcl.to_dbu(vec)
        elif self._base.dcplx_trans:
            self._base.dcplx_trans.disp = vec

    @property
    def dy(self) -> float:
        """Y coordinate of the port in um."""
        return self.dcplx_trans.disp.y

    @dy.setter
    def dy(self, value: float) -> None:
        vec = self.dcplx_trans.disp
        vec.y = value
        if self._base.trans:
            self._base.trans.disp = self.kcl.to_dbu(vec)
        elif self._base.dcplx_trans:
            self._base.dcplx_trans.disp = vec

    @property
    def dcenter(self) -> tuple[float, float]:
        """Coordinate of the port in um."""
        vec = self.dcplx_trans.disp
        return (vec.x, vec.y)

    @dcenter.setter
    def dcenter(self, pos: tuple[float, float]) -> None:
        if self._base.trans:
            self._base.trans.disp = self.kcl.to_dbu(kdb.DVector(*pos))
        elif self._base.dcplx_trans:
            self._base.dcplx_trans.disp = kdb.DVector(*pos)

    @property
    def icenter(self) -> tuple[int, int]:
        """Coordinate of the port in dbu."""
        vec = self.trans.disp
        return (vec.x, vec.y)

    @icenter.setter
    def icenter(self, pos: tuple[int, int]) -> None:
        if self._base.trans:
            self._base.trans.disp = kdb.Vector(*pos)
        elif self._base.dcplx_trans:
            self._base.dcplx_trans.disp = self.kcl.to_um(kdb.Vector(*pos))

    @property
    def dwidth(self) -> float:
        """Width of the port in um."""
        return self.kcl.to_um(self._base.cross_section.width)

    def print(self, print_type: Literal["dbu", "um"] | None = None) -> None:
        """Print the port pretty."""
        config.console.print(pprint_ports([self], unit=print_type))

    def __repr__(self) -> str:
        """String representation of port."""
        return (
            f"{self.__class__.__name__}({self.name=}"
            f", {self.width=}, trans={self.dcplx_trans.to_s()}, layer="
            f"{self.layer_info}, port_type={self.port_type})"
        )

    def transform(
        self,
        trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
        post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    ) -> Self:
        """Get a transformed copy of the BasePort."""
        self.base.transform(trans=trans, post_trans=post_trans)
        return self

angle property writable

angle: Angle

Angle of the transformation.

In the range of [0,1,2,3] which are increments in 90°.

base property

base: BasePort

Get the BasePort associated with this Port.

center property writable

center: tuple[TUnit, TUnit]

Returns port center.

cross_section abstractmethod property writable

cross_section: TCrossSection[TUnit]

Get the cross section of the port.

dcenter property writable

dcenter: tuple[float, float]

Coordinate of the port in um.

dcplx_trans property writable

dcplx_trans: DCplxTrans

Complex transformation (um based).

If the internal transformation is simple, return a complex copy.

The setter will set a complex transformation and overwrite the internal transformation (set simple to None and the complex to the provided value.

dwidth property

dwidth: float

Width of the port in um.

dx property writable

dx: float

X coordinate of the port in um.

dy property writable

dy: float

Y coordinate of the port in um.

icenter property writable

icenter: tuple[int, int]

Coordinate of the port in dbu.

info property writable

info: Info

Additional info about the port.

iwidth property

iwidth: int

Width of the port in dbu.

ix property writable

ix: int

X coordinate of the port in dbu.

iy property writable

iy: int

Y coordinate of the port in dbu.

kcl property writable

kcl: KCLayout

KCLayout associated to the prot.

layer property

layer: LayerEnum | int

Get the layer index of the port.

This corresponds to the port's cross section's main layer converted to the index.

layer_info property

layer_info: LayerInfo

Get the layer info of the port.

This corresponds to the port's cross section's main layer.

mirror property writable

mirror: bool

Returns True/False depending on the mirror flag on the transformation.

name property writable

name: str | None

Name of the port.

orientation property writable

orientation: float

Returns orientation in degrees for gdsfactory compatibility.

In the range of [0,360)

port_type property writable

port_type: str

Type of the port.

Usually "optical" or "electrical".

trans property writable

trans: Trans

Simple Transformation of the Port.

If this is set with the setter, it will overwrite any transformation or dcplx transformation

width abstractmethod property

width: TUnit

Width of the port.

x abstractmethod property writable

x: TUnit

X coordinate of the port.

y abstractmethod property writable

y: TUnit

Y coordinate of the port.

__eq__

__eq__(other: object) -> bool

Support for port1 == port2 comparisons.

Source code in kfactory/port.py
362
363
364
365
366
def __eq__(self, other: object) -> bool:
    """Support for `port1 == port2` comparisons."""
    if isinstance(other, ProtoPort):
        return self._base == other._base
    return False

__init__ abstractmethod

__init__(
    name: str | None = None,
    *,
    width: TUnit | None = None,
    layer: int | None = None,
    layer_info: LayerInfo | None = None,
    port_type: str = "optical",
    trans: Trans | str | None = None,
    dcplx_trans: DCplxTrans | str | None = None,
    angle: TUnit | None = None,
    center: tuple[TUnit, TUnit] | None = None,
    mirror_x: bool = False,
    port: Port | None = None,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
    cross_section: TCrossSection[TUnit] | None = None,
    base: BasePort | None = None,
) -> None

Initialise a ProtoPort.

Source code in kfactory/port.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
@abstractmethod
def __init__(
    self,
    name: str | None = None,
    *,
    width: TUnit | None = None,
    layer: int | None = None,
    layer_info: kdb.LayerInfo | None = None,
    port_type: str = "optical",
    trans: kdb.Trans | str | None = None,
    dcplx_trans: kdb.DCplxTrans | str | None = None,
    angle: TUnit | None = None,
    center: tuple[TUnit, TUnit] | None = None,
    mirror_x: bool = False,
    port: Port | None = None,
    kcl: KCLayout | None = None,
    info: dict[str, int | float | str] = ...,
    cross_section: TCrossSection[TUnit] | None = None,
    base: BasePort | None = None,
) -> None:
    """Initialise a ProtoPort."""
    ...

__repr__

__repr__() -> str

String representation of port.

Source code in kfactory/port.py
608
609
610
611
612
613
614
def __repr__(self) -> str:
    """String representation of port."""
    return (
        f"{self.__class__.__name__}({self.name=}"
        f", {self.width=}, trans={self.dcplx_trans.to_s()}, layer="
        f"{self.layer_info}, port_type={self.port_type})"
    )

copy abstractmethod

copy(
    trans: Trans | DCplxTrans = kdb.Trans.R0,
    post_trans: Trans | DCplxTrans = kdb.Trans.R0,
) -> ProtoPort[TUnit]

Copy the port with a transformation.

Source code in kfactory/port.py
463
464
465
466
467
468
469
470
@abstractmethod
def copy(
    self,
    trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
) -> ProtoPort[TUnit]:
    """Copy the port with a transformation."""
    ...

print

print(
    print_type: Literal["dbu", "um"] | None = None,
) -> None

Print the port pretty.

Source code in kfactory/port.py
604
605
606
def print(self, print_type: Literal["dbu", "um"] | None = None) -> None:
    """Print the port pretty."""
    config.console.print(pprint_ports([self], unit=print_type))

to_dtype

to_dtype() -> DPort

Convert the port to a um port.

Source code in kfactory/port.py
413
414
415
def to_dtype(self) -> DPort:
    """Convert the port to a um port."""
    return DPort(base=self._base)

to_itype

to_itype() -> Port

Convert the port to a dbu port.

Source code in kfactory/port.py
409
410
411
def to_itype(self) -> Port:
    """Convert the port to a dbu port."""
    return Port(base=self._base)

transform

transform(
    trans: Trans | DCplxTrans = kdb.Trans.R0,
    post_trans: Trans | DCplxTrans = kdb.Trans.R0,
) -> Self

Get a transformed copy of the BasePort.

Source code in kfactory/port.py
616
617
618
619
620
621
622
623
def transform(
    self,
    trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
    post_trans: kdb.Trans | kdb.DCplxTrans = kdb.Trans.R0,
) -> Self:
    """Get a transformed copy of the BasePort."""
    self.base.transform(trans=trans, post_trans=post_trans)
    return self

ProtoTKCell

Bases: ProtoKCell[TUnit, TKCell], Generic[TUnit], ABC

Source code in kfactory/kcell.py
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
class ProtoTKCell(ProtoKCell[TUnit, TKCell], Generic[TUnit], ABC):  # noqa: PYI059
    def __init__(
        self,
        *,
        base: TKCell | None = None,
        name: str | None = None,
        kcl: KCLayout | None = None,
        kdb_cell: kdb.Cell | None = None,
        ports: Iterable[ProtoPort[Any]] | None = None,
        pins: Iterable[ProtoPin[Any]] | None = None,
        info: dict[str, Any] | None = None,
        settings: dict[str, Any] | None = None,
    ) -> None:
        if base is not None:
            self._base = base
            return

        from .layout import get_default_kcl, kcls

        kcl_ = kcl or get_default_kcl()

        if name is None:
            name_ = "Unnamed_!" if kdb_cell is None else kdb_cell.name
        else:
            name_ = name
            if kdb_cell is not None:
                kdb_cell.name = name
        kdb_cell_ = kdb_cell or kcl_.create_cell(name_)
        if name_ == "Unnamed_!":
            kdb_cell_.name = f"Unnamed_{kdb_cell_.cell_index()}"

        self._base = TKCell(
            kcl=kcl_,
            info=Info(**(info or {})),
            settings=KCellSettings(**(settings or {})),
            kdb_cell=kdb_cell_,
            ports=[port.base for port in ports] if ports else [],
            pins=[pin.base for pin in pins] if pins else [],
            vinsts=VInstances(),
        )
        if kdb_cell_.is_library_cell():
            if ports or info or settings or pins:
                raise ValueError(
                    "If a TKCell is created from a library cell (separate PDK/layout), "
                    "ports, info, settings, and pins must not be set."
                    f"Cell {kdb_cell_.name} in {kcl_.name}: {ports=}, {pins=}, {info=},"
                    f" {settings=}"
                )
            kcls[kdb_cell_.library().name()][
                kdb_cell_.library_cell_index()
            ].set_meta_data()
            self.get_meta_data()
        self.kcl.register_cell(self)

    @property
    def schematic(self) -> TSchematic[Any] | None:
        return self._base.schematic

    @schematic.setter
    def schematic(self, value: TSchematic[Any] | None) -> None:
        self._base.schematic = value

    @abstractmethod
    def __getitem__(self, key: int | str | None) -> ProtoPort[TUnit]:
        """Returns port from instance."""
        ...

    @property
    def name(self) -> str:
        return self._base.name

    @name.setter
    def name(self, value: str) -> None:
        self._base.name = value

    @property
    def virtual(self) -> bool:
        if self.kdb_cell.is_library_cell():
            return self.library_cell.virtual
        return self._base.virtual

    @property
    @property
    @abstractmethod
    def pins(self) -> ProtoPins[TUnit]: ...

    @pins.setter
    @abstractmethod
    def pins(self, new_pins: Iterable[ProtoPin[Any]]) -> None: ...

    def __hash__(self) -> int:
        """Hash the KCell."""
        return hash((self._base.kcl.library.name(), self._base.kdb_cell.cell_index()))

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, ProtoTKCell):
            return False
        return self._base is other._base

    @property
    def prop_id(self) -> int:
        """Gets the properties ID associated with the cell."""
        return self._base.kdb_cell.prop_id

    @prop_id.setter
    def prop_id(self, value: int) -> None:
        if self.locked:
            raise LockedError(self)
        self._base.kdb_cell.prop_id = value

    @property
    def ghost_cell(self) -> bool:
        """Returns a value indicating whether the cell is a "ghost cell"."""
        return self._base.kdb_cell.ghost_cell

    @ghost_cell.setter
    def ghost_cell(self, value: bool) -> None:
        if self.locked:
            raise LockedError(self)
        self._base.kdb_cell.ghost_cell = value

    def __getattr__(self, name: str) -> Any:
        """If KCell doesn't have an attribute, look in the KLayout Cell."""
        try:
            return super().__getattr__(name)  # type: ignore[misc]
        except Exception:
            return getattr(self._base, name)

    def cell_index(self) -> int:
        """Gets the cell index."""
        return self._base.kdb_cell.cell_index()

    def shapes(self, layer: int | kdb.LayerInfo) -> kdb.Shapes:
        return self._base.kdb_cell.shapes(layer)

    @property
    @abstractmethod
    def insts(self) -> ProtoTInstances[TUnit]: ...

    def __copy__(self) -> Self:
        """Enables use of `copy.copy` and `copy.deep_copy`."""
        return self.dup()

    def dup(self, new_name: str | None = None) -> Self:
        """Copy the full cell.

        Sets `_locked` to `False`

        Returns:
            cell: Exact copy of the current cell.
                The name will have `$1` as duplicate names are not allowed
        """
        kdb_copy = self._kdb_copy()
        if new_name:
            if new_name == self.name:
                if config.debug_names:
                    raise ValueError(
                        "When duplicating a Cell, avoid giving the duplicate the same "
                        "name, as this can cause naming conflicts and may render the "
                        "GDS/OASIS file unwritable. If you're using a @cell function, "
                        "ensure that the function has a different name than the one "
                        "being called."
                    )
                logger.error(
                    "When duplicating a Cell, avoid giving the duplicate the same "
                    "name, as this can cause naming conflicts and may render the "
                    "GDS/OASIS file unwritable. If you're using a @cell function, "
                    "ensure that the function has a different name than the one being "
                    "called."
                )
            kdb_copy.name = new_name

        c = self.__class__(kcl=self.kcl, kdb_cell=kdb_copy)
        c.ports = self.ports.copy()

        if self.pins:
            port_mapping = {id(p): i for i, p in enumerate(c.ports)}
            c._base.pins = [
                BasePin(
                    name=p.name,
                    kcl=self.kcl,
                    ports=[c.base.ports[port_mapping[id(port)]] for port in p.ports],
                    pin_type=p.pin_type,
                    info=p.info,
                )
                for p in self._base.pins
            ]

        c._base.settings = self.settings.model_copy()
        c._base.info = self.info.model_copy()
        c._base.vinsts = self._base.vinsts.copy()

        return c

    @property
    def kdb_cell(self) -> kdb.Cell:
        return self._base.kdb_cell

    def destroyed(self) -> bool:
        return self._base.kdb_cell._destroyed()

    @property
    def boundary(self) -> kdb.DPolygon | None:
        return self._base.boundary

    @boundary.setter
    def boundary(self, boundary: kdb.DPolygon | None) -> None:
        self._base.boundary = boundary

    def to_itype(self) -> KCell:
        """Convert the kcell to a dbu kcell."""
        return KCell(base=self._base)

    def to_dtype(self) -> DKCell:
        """Convert the kcell to a um kcell."""
        return DKCell(base=self._base)

    def show(
        self,
        lyrdb: rdb.ReportDatabase | Path | str | None = None,
        l2n: kdb.LayoutToNetlist | Path | str | None = None,
        keep_position: bool = True,
        save_options: kdb.SaveLayoutOptions | None = None,
        use_libraries: bool = True,
        library_save_options: kdb.SaveLayoutOptions | None = None,
        technology: str | None = None,
    ) -> None:
        """Stream the gds to klive.

        Will create a temporary file of the gds and load it in KLayout via klive
        """
        if save_options is None:
            save_options = save_layout_options()
        if library_save_options is None:
            library_save_options = save_layout_options()
        show_f: ShowFunction = config.show_function or show

        kwargs: dict[str, Any] = {}
        if technology is not None:
            kwargs["technology"] = technology
        if l2n is not None:
            kwargs["l2n"] = l2n
        if lyrdb is not None:
            kwargs["lyrdb"] = lyrdb

        show_f(
            self,
            keep_position=keep_position,
            save_options=save_options,
            use_libraries=use_libraries,
            library_save_options=library_save_options,
            **kwargs,
        )

    def plot(
        self,
        lyrdb: Path | str | None = None,
        display_type: Literal["image", "widget"] | None = None,
    ) -> None:
        """Display cell.

        Args:
            lyrdb: Path to the lyrdb file.
            display_type: Type of display. Options are "widget" or "image".

        """
        from .widgets.interactive import display_kcell

        display_kcell(self, lyrdb=lyrdb, display_type=display_type)

    def _ipython_display_(self) -> None:
        """Display a cell in a Jupyter Cell.

        Usage: Pass the kcell variable as an argument in the cell at the end
        """
        self.plot()

    def delete(self) -> None:
        """Delete the cell."""
        ci = self.cell_index()
        self._base.kdb_cell.locked = False
        self.kcl.delete_cell(ci)

    @abstractmethod
    def add_port(
        self,
        *,
        port: ProtoPort[Any],
        name: str | None = None,
        keep_mirror: bool = False,
    ) -> ProtoPort[Any]: ...

    @abstractmethod
    def create_pin(
        self,
        *,
        ports: Iterable[ProtoPort[Any]],
        name: str | None = None,
        pin_type: str = "DC",
        info: dict[str, int | float | str] | None = None,
    ) -> ProtoPin[TUnit]: ...

    @overload
    @abstractmethod
    def create_inst(
        self: ProtoTKCell[int],
        cell: ProtoTKCell[Any] | int,
        trans: kdb.Trans | kdb.Vector | kdb.ICplxTrans | None = None,
        *,
        a: kdb.Vector | None = None,
        b: kdb.Vector | None = None,
        na: int = 1,
        nb: int = 1,
        libcell_as_static: bool = False,
        static_name_separator: str = "__",
    ) -> Instance: ...

    @overload
    @abstractmethod
    def create_inst(
        self: ProtoTKCell[float],
        cell: ProtoTKCell[Any] | int,
        trans: kdb.DTrans | kdb.DVector | kdb.DCplxTrans | None = None,
        *,
        a: kdb.DVector | None = None,
        b: kdb.DVector | None = None,
        na: int = 1,
        nb: int = 1,
        libcell_as_static: bool = False,
        static_name_separator: str = "__",
    ) -> DInstance: ...

    @abstractmethod
    def create_inst(
        self,
        cell: ProtoTKCell[Any] | int,
        trans: kdb.Trans
        | kdb.Vector
        | kdb.ICplxTrans
        | kdb.DTrans
        | kdb.DVector
        | kdb.DCplxTrans
        | None = None,
        *,
        a: kdb.Vector | kdb.DVector | None = None,
        b: kdb.Vector | kdb.DVector | None = None,
        na: int = 1,
        nb: int = 1,
        libcell_as_static: bool = False,
        static_name_separator: str = "__",
    ) -> Instance | DInstance: ...

    def _get_ci(
        self,
        cell: ProtoTKCell[Any],
        libcell_as_static: bool = False,
        static_name_separator: str = "__",
    ) -> int:
        if cell.layout() == self.layout():
            return cell.cell_index()
        assert cell.layout().library() is not None
        lib_ci = self.kcl.layout.add_lib_cell(cell.kcl.library, cell.cell_index())
        if lib_ci not in self.kcl.tkcells:
            kcell = self.kcl[lib_ci]
            kcell.basename = cell.basename
            kcell.function_name = cell.function_name
            kcell.base._library_cell = KCell(base=cell.base)
        if libcell_as_static:
            cell.set_meta_data()
            ci = self.kcl.layout.convert_cell_to_static(lib_ci)
            if ci not in self.kcl.tkcells:
                kcell = self.kcl[ci]
                kcell.copy_meta_info(cell.kdb_cell)
                kcell.name = cell.kcl.name + static_name_separator + cell.name
                kcell.base.virtual = cell.virtual
                if cell.kcl.dbu != self.kcl.dbu:
                    for port, lib_port in zip(kcell.ports, cell.ports, strict=False):
                        port.cross_section = CrossSection(
                            kcl=kcell.kcl,
                            base=cell.kcl.get_symmetrical_cross_section(
                                lib_port.cross_section.base.to_dtype(cell.kcl)
                            ),
                        )
            return ci
        return lib_ci

    def icreate_inst(
        self,
        cell: ProtoTKCell[Any] | int,
        trans: kdb.Trans | kdb.Vector | kdb.ICplxTrans | None = None,
        *,
        a: kdb.Vector | None = None,
        b: kdb.Vector | None = None,
        na: int = 1,
        nb: int = 1,
        libcell_as_static: bool = False,
        static_name_separator: str = "__",
    ) -> Instance:
        """Add an instance of another KCell.

        Args:
            cell: The cell to be added
            trans: The integer transformation applied to the reference
            a: Vector for the array.
                Needs to be in positive X-direction. Usually this is only a
                Vector in x-direction. Some foundries won't allow other Vectors.
            b: Vector for the array.
                Needs to be in positive Y-direction. Usually this is only a
                Vector in x-direction. Some foundries won't allow other Vectors.
            na: Number of elements in direction of `a`
            nb: Number of elements in direction of `b`
            libcell_as_static: If the cell is a Library cell
                (different KCLayout object), convert it to a static cell. This can cause
                name collisions that are automatically resolved by appending $1[..n] on
                the newly created cell.
            static_name_separator: Stringt to separate the KCLayout name from the cell
                name when converting library cells (other KCLayout object than the one
                of this KCell) to static cells (copy them into this KCell's KCLayout).

        Returns:
            The created instance
        """
        if trans is None:
            trans = kdb.Trans()
        if isinstance(cell, int):
            ci = cell
        else:
            ci = self._get_ci(cell, libcell_as_static, static_name_separator)

        if a is None:
            inst = self._base.kdb_cell.insert(kdb.CellInstArray(ci, trans))
        else:
            if b is None:
                b = kdb.Vector()
            inst = self._base.kdb_cell.insert(
                kdb.CellInstArray(ci, trans, a, b, na, nb)
            )
        return Instance(kcl=self.kcl, instance=inst)

    def dcreate_inst(
        self,
        cell: ProtoTKCell[Any] | int,
        trans: kdb.DTrans | kdb.DVector | kdb.DCplxTrans | None = None,
        *,
        a: kdb.DVector | None = None,
        b: kdb.DVector | None = None,
        na: int = 1,
        nb: int = 1,
        libcell_as_static: bool = False,
        static_name_separator: str = "__",
    ) -> DInstance:
        """Add an instance of another KCell.

        Args:
            cell: The cell to be added
            trans: The integer transformation applied to the reference
            a: Vector for the array.
                Needs to be in positive X-direction. Usually this is only a
                Vector in x-direction. Some foundries won't allow other Vectors.
            b: Vector for the array.
                Needs to be in positive Y-direction. Usually this is only a
                Vector in x-direction. Some foundries won't allow other Vectors.
            na: Number of elements in direction of `a`
            nb: Number of elements in direction of `b`
            libcell_as_static: If the cell is a Library cell
                (different KCLayout object), convert it to a static cell. This can cause
                name collisions that are automatically resolved by appending $1[..n] on
                the newly created cell.
            static_name_separator: Stringt to separate the KCLayout name from the cell
                name when converting library cells (other KCLayout object than the one
                of this KCell) to static cells (copy them into this KCell's KCLayout).

        Returns:
            The created instance
        """
        if trans is None:
            trans = kdb.DTrans()
        if isinstance(cell, int):
            ci = cell
        else:
            ci = self._get_ci(cell, libcell_as_static, static_name_separator)

        if a is None:
            inst = self._base.kdb_cell.insert(kdb.DCellInstArray(ci, trans))
        else:
            if b is None:
                b = kdb.DVector()
            inst = self._base.kdb_cell.insert(
                kdb.DCellInstArray(ci, trans, a, b, na, nb)
            )
        return DInstance(kcl=self.kcl, instance=inst)

    def _kdb_copy(self) -> kdb.Cell:
        return self._base.kdb_cell.dup()

    def layout(self) -> kdb.Layout:
        return self._base.kdb_cell.layout()

    def library(self) -> kdb.Library:
        return self._base.kdb_cell.library()

    @property
    @abstractmethod
    def library_cell(self) -> ProtoTKCell[TUnit]: ...

    @abstractmethod
    def __lshift__(self, cell: AnyTKCell) -> ProtoTInstance[TUnit]: ...

    def auto_rename_ports(self, rename_func: Callable[..., None] | None = None) -> None:
        """Rename the ports with the schema angle -> "NSWE" and sort by x and y.

        Args:
            rename_func: Function that takes Iterable[Port] and renames them.
                This can of course contain a filter and only rename some of the ports
        """
        if self.locked:
            raise LockedError(self)
        if rename_func is None:
            self.kcl.rename_function(self.ports)
        else:
            rename_func(self.ports)

    def flatten(self, merge: bool = True) -> None:
        """Flatten the cell.

        Args:
            merge: Merge the shapes on all layers.
        """
        if self.locked:
            raise LockedError(self)
        for vinst in self._base.vinsts:
            vinst.insert_into_flat(self)
        self._base.vinsts = VInstances()
        self._base.kdb_cell.flatten(False)

        if merge:
            for layer in self.kcl.layout.layer_indexes():
                reg = kdb.Region(self.shapes(layer))
                reg = reg.merge()
                texts = kdb.Texts(self.shapes(layer))
                self.kdb_cell.clear(layer)
                self.shapes(layer).insert(reg)
                self.shapes(layer).insert(texts)

    def convert_to_static(self, recursive: bool = True) -> None:
        """Convert the KCell to a static cell if it is pdk KCell."""
        if self.library().name() == self.kcl.name:
            raise ValueError(f"KCell {self.qname()} is already a static KCell.")
        from .layout import kcls

        lib_cell = kcls[self.library().name()][self.library_cell_index()]
        lib_cell.set_meta_data()
        kdb_cell = self.kcl.layout_cell(
            self.kcl.convert_cell_to_static(self.cell_index())
        )
        assert kdb_cell is not None
        kdb_cell.name = self.qname()
        ci_ = kdb_cell.cell_index()
        old_kdb_cell = self._base.kdb_cell
        kdb_cell.copy_meta_info(lib_cell.kdb_cell)
        self.get_meta_data()

        if recursive:
            for ci in self.called_cells():
                kc = self.kcl[ci]
                if kc.is_library_cell():
                    kc.convert_to_static(recursive=recursive)

        self._base.kdb_cell = kdb_cell
        for ci in old_kdb_cell.caller_cells():
            c = self.kcl.layout_cell(ci)
            assert c is not None
            it = kdb.RecursiveInstanceIterator(self.kcl.layout, c)
            it.targets = [old_kdb_cell.cell_index()]
            it.max_depth = 0
            insts = [instit.current_inst_element().inst() for instit in it.each()]
            locked = c.locked
            c.locked = False
            for inst in insts:
                ca = inst.cell_inst
                ca.cell_index = ci_
                c.replace(inst, ca)
            c.locked = locked

        self.kcl.layout.delete_cell(old_kdb_cell.cell_index())

    def draw_ports(self) -> None:
        """Draw all the ports on their respective layer."""
        locked = self._base.kdb_cell.locked
        self._base.kdb_cell.locked = False
        polys: dict[int, kdb.Region] = {}

        for port in Ports(kcl=self.kcl, bases=self.ports.bases):
            w = port.width

            if w in polys:
                poly = polys[w]
            else:
                poly = kdb.Region()
                poly.insert(
                    kdb.Polygon(
                        [
                            kdb.Point(0, int(-w // 2)),
                            kdb.Point(0, int(w // 2)),
                            kdb.Point(int(w // 2), 0),
                        ]
                    )
                )
                if w > 20:  # noqa: PLR2004
                    poly -= kdb.Region(
                        kdb.Polygon(
                            [
                                kdb.Point(int(w // 20), 0),
                                kdb.Point(
                                    int(w // 20), int(-w // 2 + int(w * 2.5 // 20))
                                ),
                                kdb.Point(int(w // 2 - int(w * 1.41 / 20)), 0),
                            ]
                        )
                    )
            polys[w] = poly
            if port.base.trans:
                self.shapes(port.layer).insert(poly.transformed(port.trans))
                self.shapes(port.layer).insert(kdb.Text(port.name or "", port.trans))
            else:
                self.shapes(port.layer).insert(poly, port.dcplx_trans)
                self.shapes(port.layer).insert(kdb.Text(port.name or "", port.trans))
        self._base.kdb_cell.locked = locked

    def write(
        self,
        filename: str | Path,
        save_options: kdb.SaveLayoutOptions | None = None,
        convert_external_cells: bool = False,
        set_meta_data: bool = True,
        autoformat_from_file_extension: bool = True,
    ) -> None:
        """Write a KCell to a GDS.

        See [KCLayout.write][kfactory.kcell.KCLayout.write] for more info.
        """
        if save_options is None:
            save_options = save_layout_options()
        self.insert_vinsts()
        match set_meta_data, convert_external_cells:
            case True, True:
                self.kcl.set_meta_data()
                for kcell in (self.kcl[ci] for ci in self.called_cells()):
                    if not kcell._destroyed():
                        if kcell.is_library_cell():
                            kcell.convert_to_static(recursive=True)
                        kcell.set_meta_data()
                if self.is_library_cell():
                    self.convert_to_static(recursive=True)
                self.set_meta_data()
            case True, False:
                self.kcl.set_meta_data()
                for kcell in (self.kcl[ci] for ci in self.called_cells()):
                    if not kcell._destroyed():
                        kcell.set_meta_data()
                self.set_meta_data()
            case False, True:
                for kcell in (self.kcl[ci] for ci in self.called_cells()):
                    if kcell.is_library_cell() and not kcell._destroyed():
                        kcell.convert_to_static(recursive=True)
                if self.is_library_cell():
                    self.convert_to_static(recursive=True)
            case _:
                ...

        for kci in set(self._base.kdb_cell.called_cells()) & self.kcl.tkcells.keys():
            kc = self.kcl[kci]
            kc.insert_vinsts()

        filename = str(filename)
        if autoformat_from_file_extension:
            save_options.set_format_from_filename(filename)
        self._base.kdb_cell.write(filename, save_options)

    def write_bytes(
        self,
        save_options: kdb.SaveLayoutOptions | None = None,
        convert_external_cells: bool = False,
        set_meta_data: bool = True,
    ) -> bytes:
        """Write a KCell to a binary format as oasis.

        See [KCLayout.write][kfactory.kcell.KCLayout.write] for more info.
        """
        if save_options is None:
            save_options = save_layout_options()
        self.insert_vinsts()
        match set_meta_data, convert_external_cells:
            case True, True:
                self.kcl.set_meta_data()
                for kcell in (self.kcl[ci] for ci in self.called_cells()):
                    if not kcell._destroyed():
                        if kcell.is_library_cell():
                            kcell.convert_to_static(recursive=True)
                        kcell.set_meta_data()
                if self.is_library_cell():
                    self.convert_to_static(recursive=True)
                self.set_meta_data()
            case True, False:
                self.kcl.set_meta_data()
                for kcell in (self.kcl[ci] for ci in self.called_cells()):
                    if not kcell._destroyed():
                        kcell.set_meta_data()
                self.set_meta_data()
            case False, True:
                for kcell in (self.kcl[ci] for ci in self.called_cells()):
                    if kcell.is_library_cell() and not kcell._destroyed():
                        kcell.convert_to_static(recursive=True)
                if self.is_library_cell():
                    self.convert_to_static(recursive=True)
            case _:
                ...

        for kci in set(self._base.kdb_cell.called_cells()) & self.kcl.tkcells.keys():
            kc = self.kcl[kci]
            kc.insert_vinsts()

        save_options.format = save_options.format or "OASIS"
        save_options.clear_cells()
        save_options.select_cell(self.cell_index())
        return self.kcl.layout.write_bytes(save_options)

    def read(
        self,
        filename: str | Path,
        options: kdb.LoadLayoutOptions | None = None,
        register_cells: bool = False,
        test_merge: bool = True,
        update_kcl_meta_data: Literal["overwrite", "skip", "drop"] = "drop",
        meta_format: Literal["v1", "v2", "v3"] | None = None,
    ) -> list[int]:
        """Read a GDS file into the existing KCell.

        Any existing meta info (KCell.info and KCell.settings) will be overwritten if
        a KCell already exists. Instead of overwriting the cells, they can also be
        loaded into new cells by using the corresponding cell_conflict_resolution.

        Layout meta infos are ignored from the loaded layout.

        Args:
            filename: Path of the GDS file.
            options: KLayout options to load from the GDS. Can determine how merge
                conflicts are handled for example. See
                https://www.klayout.de/doc-qt5/code/class_LoadLayoutOptions.html
            register_cells: If `True` create KCells for all cells in the GDS.
            test_merge: Check the layouts first whether they are compatible
                (no differences).
            update_kcl_meta_data: How to treat loaded KCLayout info.
                overwrite: overwrite existing info entries
                skip: keep existing info values
                drop: don't add any new info
            meta_format: How to read KCell metainfo from the gds. `v1` had stored port
                transformations as strings, never versions have them stored and loaded
                in their native KLayout formats.
        """
        # see: wait for KLayout update https://github.com/KLayout/klayout/issues/1609
        logger.critical(
            "KLayout <=0.28.15 (last update 2024-02-02) cannot read LayoutMetaInfo on"
            " 'Cell.read'. kfactory uses these extensively for ports, info, and "
            "settings. Therefore proceed at your own risk."
        )
        if meta_format is None:
            meta_format = config.meta_format
        if options is None:
            options = load_layout_options()
        fn = str(Path(filename).expanduser().resolve())
        if test_merge and (
            options.cell_conflict_resolution
            != kdb.LoadLayoutOptions.CellConflictResolution.RenameCell
        ):
            self.kcl.set_meta_data()
            for kcell in self.kcl.kcells.values():
                kcell.set_meta_data()
            layout_b = kdb.Layout()
            layout_b.read(fn, options)
            layout_a = self.kcl.layout.dup()
            layout_a.delete_cell(layout_a.cell(self.name).cell_index())
            diff = MergeDiff(
                layout_a=layout_a,
                layout_b=layout_b,
                name_a=self.name,
                name_b=Path(filename).stem,
            )
            diff.compare()
            if diff.dbu_differs:
                raise MergeError("Layouts' DBU differ. Check the log for more info.")
            if diff.diff_xor.cells() > 0 or diff.layout_meta_diff:
                diff_kcl = KCLayout(self.name + "_XOR")
                diff_kcl.layout.assign(diff.diff_xor)
                show(diff_kcl)

                err_msg = (
                    f"Layout {self.name} cannot merge with layout "
                    f"{Path(filename).stem} safely. See the error messages "
                    f"or check with KLayout."
                )

                if diff.layout_meta_diff:
                    yaml = ruamel.yaml.YAML(typ=["rt", "string"])
                    err_msg += (
                        "\nLayout Meta Diff:\n```\n"
                        + yaml.dumps(dict(diff.layout_meta_diff))
                        + "\n```"
                    )
                if diff.cells_meta_diff:
                    yaml = ruamel.yaml.YAML(typ=["rt", "string"])
                    err_msg += (
                        "\nLayout Meta Diff:\n```\n"
                        + yaml.dumps(dict(diff.cells_meta_diff))
                        + "\n```"
                    )

                raise MergeError(err_msg)

        cell_ids = self._base.kdb_cell.read(fn, options)
        info, settings = self.kcl.get_meta_data()

        match update_kcl_meta_data:
            case "overwrite":
                for k, v in info.items():
                    self.kcl.info[k] = v
            case "skip":
                info_ = self.info.model_dump()

                info.update(info_)
                self.kcl.info = Info(**info)
            case "drop":
                ...
        meta_format = settings.get("meta_format") or meta_format

        if register_cells:
            new_cis = set(cell_ids)

            for c in new_cis:
                kc = self.kcl[c]
                kc.get_meta_data(meta_format=meta_format)
        else:
            cis = self.kcl.tkcells.keys()
            new_cis = set(cell_ids)

            for c in new_cis & cis:
                kc = self.kcl[c]
                kc.get_meta_data(meta_format=meta_format)

        self.get_meta_data(meta_format=meta_format)

        return cell_ids

    def each_inst(self) -> Iterator[Instance]:
        """Iterates over all child instances (which may actually be instance arrays)."""
        yield from (
            Instance(self.kcl, inst) for inst in self._base.kdb_cell.each_inst()
        )

    def each_overlapping_inst(self, b: kdb.Box | kdb.DBox) -> Iterator[Instance]:
        """Gets the instances overlapping the given rectangle."""
        yield from (
            Instance(self.kcl, inst)
            for inst in self._base.kdb_cell.each_overlapping_inst(b)
        )

    def each_touching_inst(self, b: kdb.Box | kdb.DBox) -> Iterator[Instance]:
        """Gets the instances overlapping the given rectangle."""
        yield from (
            Instance(self.kcl, inst)
            for inst in self._base.kdb_cell.each_touching_inst(b)
        )

    @overload
    def insert(
        self, inst: Instance | kdb.CellInstArray | kdb.DCellInstArray
    ) -> Instance: ...

    @overload
    def insert(
        self, inst: kdb.CellInstArray | kdb.DCellInstArray, property_id: int
    ) -> Instance: ...

    def insert(
        self,
        inst: Instance | kdb.CellInstArray | kdb.DCellInstArray,
        property_id: int | None = None,
    ) -> Instance:
        """Inserts a cell instance given by another reference."""
        if self.locked:
            raise LockedError(self)
        if isinstance(inst, Instance):
            return Instance(self.kcl, self._base.kdb_cell.insert(inst.instance))
        if not property_id:
            return Instance(self.kcl, self._base.kdb_cell.insert(inst))
        assert isinstance(inst, kdb.CellInstArray | kdb.DCellInstArray)
        return Instance(self.kcl, self._base.kdb_cell.insert(inst, property_id))

    @overload
    def transform(
        self,
        inst: kdb.Instance,
        trans: kdb.Trans | kdb.DTrans | kdb.ICplxTrans | kdb.DCplxTrans,
        /,
        *,
        transform_ports: bool = True,
    ) -> Instance: ...

    @overload
    def transform(
        self,
        trans: kdb.Trans | kdb.DTrans | kdb.ICplxTrans | kdb.DCplxTrans,
        /,
        *,
        transform_ports: bool = True,
    ) -> None: ...

    def transform(
        self,
        inst_or_trans: kdb.Instance
        | kdb.Trans
        | kdb.DTrans
        | kdb.ICplxTrans
        | kdb.DCplxTrans,
        trans: kdb.Trans | kdb.DTrans | kdb.ICplxTrans | kdb.DCplxTrans | None = None,
        /,
        *,
        transform_ports: bool = True,
    ) -> Instance | None:
        """Transforms the instance or cell with the transformation given."""
        if trans:
            return Instance(
                self.kcl,
                self._base.kdb_cell.transform(
                    inst_or_trans,  # type: ignore[arg-type]
                    trans,  # type: ignore[arg-type]
                ),
            )
        self._base.kdb_cell.transform(inst_or_trans)  # type:ignore[arg-type]
        if transform_ports:
            if isinstance(inst_or_trans, kdb.DTrans):
                inst_or_trans = kdb.DCplxTrans(inst_or_trans)
            elif isinstance(inst_or_trans, kdb.ICplxTrans):
                inst_or_trans = kdb.DCplxTrans(inst_or_trans, self.kcl.dbu)

            if isinstance(inst_or_trans, kdb.Trans):
                for port in self.ports:
                    port.trans = inst_or_trans * port.trans
            else:
                for port in self.ports:
                    port.dcplx_trans = inst_or_trans * port.dcplx_trans  # type: ignore[operator]
        return None

    def set_meta_data(self) -> None:
        """Set metadata of the Cell.

        Currently, ports, settings and info will be set.
        """
        self.clear_meta_info()
        if not self.is_library_cell():
            for i, port in enumerate(self.ports):
                if port.base.trans is not None:
                    meta_info: dict[str, MetaData] = {
                        "name": port.name,
                        "cross_section": port.cross_section.name,
                        "trans": port.base.trans,
                        "port_type": port.port_type,
                        "info": port.info.model_dump(),
                    }

                    self.add_meta_info(
                        kdb.LayoutMetaInfo(f"kfactory:ports:{i}", meta_info, None, True)
                    )
                else:
                    meta_info = {
                        "name": port.name,
                        "cross_section": port.cross_section.name,
                        "dcplx_trans": port.dcplx_trans,
                        "port_type": port.port_type,
                        "info": port.info.model_dump(),
                    }

                    self.add_meta_info(
                        kdb.LayoutMetaInfo(f"kfactory:ports:{i}", meta_info, None, True)
                    )
            for i, pin in enumerate(self.pins):
                meta_info = {
                    "name": pin.name,
                    "pin_type": pin.pin_type,
                    "info": pin.info.model_dump(),
                    "ports": [self.base.ports.index(port.base) for port in pin.ports],
                }
                self.add_meta_info(
                    kdb.LayoutMetaInfo(f"kfactory:pins:{i}", meta_info, None, True)
                )
            settings = self.settings.model_dump()
            if settings:
                self.add_meta_info(
                    kdb.LayoutMetaInfo("kfactory:settings", settings, None, True)
                )
            info = self.info.model_dump()
            if info:
                self.add_meta_info(
                    kdb.LayoutMetaInfo("kfactory:info", info, None, True)
                )
            settings_units = self.settings_units.model_dump()
            if settings_units:
                self.add_meta_info(
                    kdb.LayoutMetaInfo(
                        "kfactory:settings_units",
                        settings_units,
                        None,
                        True,
                    )
                )

            if self.function_name is not None:
                self.add_meta_info(
                    kdb.LayoutMetaInfo(
                        "kfactory:function_name", self.function_name, None, True
                    )
                )

            if self.basename is not None:
                self.add_meta_info(
                    kdb.LayoutMetaInfo("kfactory:basename", self.basename, None, True)
                )

    def get_meta_data(
        self,
        meta_format: Literal["v1", "v2", "v3"] | None = None,
    ) -> None:
        """Read metadata from the KLayout Layout object."""
        if meta_format is None:
            meta_format = config.meta_format
        port_dict: dict[str, Any] = {}
        pin_dict: dict[str, Any] = {}
        ports: dict[str, BasePort] = {}
        settings: dict[str, MetaData] = {}
        settings_units: dict[str, str] = {}
        from .layout import kcls

        match meta_format:
            case "v3":
                self.ports.clear()
                meta_iter = (
                    kcls[self.library().name()][
                        self.library_cell_index()
                    ].each_meta_info()
                    if self.is_library_cell()
                    else self.each_meta_info()
                )
                for meta in meta_iter:
                    if meta.name.startswith("kfactory:ports"):
                        i = meta.name.removeprefix("kfactory:ports:")
                        port_dict[i] = meta.value
                    elif meta.name.startswith("kfactory:pins"):
                        i = meta.name.removeprefix("kfactory:pins:")
                        pin_dict[i] = meta.value
                    elif meta.name.startswith("kfactory:info"):
                        self._base.info = Info(**meta.value)
                    elif meta.name.startswith("kfactory:settings_units"):
                        self._base.settings_units = KCellSettingsUnits(**meta.value)
                    elif meta.name.startswith("kfactory:settings"):
                        self._base.settings = KCellSettings(**meta.value)
                    elif meta.name == "kfactory:function_name":
                        self._base.function_name = meta.value
                    elif meta.name == "kfactory:basename":
                        self._base.basename = meta.value

                if not self.is_library_cell():
                    for index in sorted(port_dict.keys()):
                        v = port_dict[index]
                        trans_: kdb.Trans | None = v.get("trans")
                        if trans_ is not None:
                            ports[index] = self.create_port(
                                name=v.get("name"),
                                trans=trans_,
                                cross_section=self.kcl.get_symmetrical_cross_section(
                                    v["cross_section"]
                                ),
                                port_type=v["port_type"],
                                info=v["info"],
                            )
                        else:
                            ports[index] = self.create_port(
                                name=v.get("name"),
                                dcplx_trans=v["dcplx_trans"],
                                cross_section=self.kcl.get_symmetrical_cross_section(
                                    v["cross_section"]
                                ),
                                port_type=v["port_type"],
                                info=v["info"],
                            )
                    for index in sorted(pin_dict.keys()):
                        v = pin_dict[index]
                        self.create_pin(
                            name=v.get("name"),
                            ports=[ports[port_index] for port_index in v["ports"]],  # type: ignore[misc]
                            pin_type=v["pin_type"],
                            info=v["info"],
                        )
                else:
                    lib_name = self.library().name()
                    for index in sorted(port_dict.keys()):
                        v = port_dict[index]
                        trans_ = v.get("trans")
                        lib_kcl = kcls[lib_name]
                        cs = self.kcl.get_symmetrical_cross_section(
                            lib_kcl.get_symmetrical_cross_section(
                                v["cross_section"]
                            ).to_dtype(lib_kcl)
                        )

                        if trans_ is not None:
                            ports[index] = self.create_port(
                                name=v.get("name"),
                                trans=trans_.to_dtype(lib_kcl.dbu).to_itype(
                                    self.kcl.dbu
                                ),
                                cross_section=cs,
                                port_type=v["port_type"],
                            )
                        else:
                            ports[index] = self.create_port(
                                name=v.get("name"),
                                dcplx_trans=v["dcplx_trans"],
                                cross_section=cs,
                                port_type=v["port_type"],
                            )
                    for index in sorted(pin_dict):
                        v = pin_dict[index]
                        self.create_pin(
                            name=v.get("name"),
                            ports=[ports[str(port_index)] for port_index in v["ports"]],  # type: ignore[misc]
                            pin_type=v["pin_type"],
                            info=v["info"],
                        )

            case "v2":
                for meta in self.each_meta_info():
                    if meta.name.startswith("kfactory:ports"):
                        i, type_ = meta.name.removeprefix("kfactory:ports:").split(
                            ":", 1
                        )
                        if i not in port_dict:
                            port_dict[i] = {}
                        if not type_.startswith("info"):
                            port_dict[i][type_] = meta.value
                        else:
                            if "info" not in port_dict[i]:
                                port_dict[i]["info"] = {}
                            port_dict[i]["info"][type_.removeprefix("info:")] = (
                                meta.value
                            )
                    elif meta.name.startswith("kfactory:info"):
                        setattr(
                            self.info,
                            meta.name.removeprefix("kfactory:info:"),
                            meta.value,
                        )
                    elif meta.name.startswith("kfactory:settings_units"):
                        settings_units[
                            meta.name.removeprefix("kfactory:settings_units:")
                        ] = meta.value
                    elif meta.name.startswith("kfactory:settings"):
                        settings[meta.name.removeprefix("kfactory:settings:")] = (
                            meta.value
                        )

                    elif meta.name == "kfactory:function_name":
                        self.function_name = meta.value

                    elif meta.name == "kfactory:basename":
                        self.basename = meta.value

                self.settings = KCellSettings(**settings)
                self.settings_units = KCellSettingsUnits(**settings_units)

                self.ports.clear()
                for index in sorted(port_dict.keys()):
                    d = port_dict[index]
                    name = d.get("name", None)
                    port_type = d["port_type"]
                    layer_info = d["layer"]
                    width = d["width"]
                    trans = d.get("trans", None)
                    dcplx_trans = d.get("dcplx_trans", None)
                    port = Port(
                        name=name,
                        width=width,
                        layer_info=layer_info,
                        trans=kdb.Trans.R0,
                        kcl=self.kcl,
                        port_type=port_type,
                        info=d.get("info", {}),
                    )
                    if trans:
                        port.trans = trans
                    elif dcplx_trans:
                        port.dcplx_trans = dcplx_trans

                    self.add_port(port=port, keep_mirror=True)
            case "v1":
                for meta in self.each_meta_info():
                    if meta.name.startswith("kfactory:ports"):
                        i, type_ = meta.name.removeprefix("kfactory:ports:").split(
                            ":", 1
                        )
                        if i not in port_dict:
                            port_dict[i] = {}
                        if not type_.startswith("info"):
                            port_dict[i][type_] = meta.value
                        else:
                            if "info" not in port_dict[i]:
                                port_dict[i]["info"] = {}
                            port_dict[i]["info"][type_.removeprefix("info:")] = (
                                meta.value
                            )
                    elif meta.name.startswith("kfactory:info"):
                        setattr(
                            self.info,
                            meta.name.removeprefix("kfactory:info:"),
                            meta.value,
                        )
                    elif meta.name.startswith("kfactory:settings_units"):
                        settings_units[
                            meta.name.removeprefix("kfactory:settings_units:")
                        ] = meta.value
                    elif meta.name.startswith("kfactory:settings"):
                        settings[meta.name.removeprefix("kfactory:settings:")] = (
                            meta.value
                        )

                    elif meta.name == "kfactory:function_name":
                        self.function_name = meta.value

                    elif meta.name == "kfactory:basename":
                        self.basename = meta.value

                self.settings = KCellSettings(**settings)
                self.settings_units = KCellSettingsUnits(**settings_units)

                self.ports.clear()
                for index in sorted(port_dict.keys()):
                    d = port_dict[index]
                    name = d.get("name", None)
                    port_type = d["port_type"]
                    layer = d["layer"]
                    width = d["width"]
                    trans = d.get("trans", None)
                    dcplx_trans = d.get("dcplx_trans", None)
                    port = Port(
                        name=name,
                        width=width,
                        layer_info=layer,
                        trans=kdb.Trans.R0,
                        kcl=self.kcl,
                        port_type=port_type,
                        info=d.get("info", {}),
                    )
                    if trans:
                        port.trans = kdb.Trans.from_s(trans)
                    elif dcplx_trans:
                        port.dcplx_trans = kdb.DCplxTrans.from_s(dcplx_trans)

                    self.add_port(port=port, keep_mirror=True)

    def ibbox(self, layer: int | None = None) -> kdb.Box:
        if layer is None:
            return self._base.kdb_cell.bbox()
        return self._base.kdb_cell.bbox(layer)

    def dbbox(self, layer: int | None = None) -> kdb.DBox:
        if layer is None:
            return self._base.kdb_cell.dbbox()
        return self._base.kdb_cell.dbbox(layer)

    def l2n(self, port_types: Iterable[str] = ("optical",)) -> kdb.LayoutToNetlist:
        """Generate a LayoutToNetlist object from the port types.

        Args:
            port_types: The port types to consider for the netlist extraction.
        Returns:
            LayoutToNetlist extracted from instance and cell port positions.
        """
        logger.warning(
            "l2n is deprecated and will be removed in 2.0. Please use `l2n_ports`"
            " instead."
        )
        return self.l2n_ports(port_types=port_types)

    def l2n_ports(
        self,
        port_types: Iterable[str] = ("optical",),
        exclude_purposes: list[str] | None = None,
        ignore_unnamed: bool = False,
        allow_width_mismatch: bool = False,
    ) -> kdb.LayoutToNetlist:
        """Generate a LayoutToNetlist object from the port types.

        Uses kfactory ports as a basis for extraction.

        Args:
            port_types: The port types to consider for the netlist extraction.
            exclude_purposes: List of purposes, if an instance has that purpose, it will
                be ignored.
            ignore_unnamed: Ignore any instance without `.name` set.
        Returns:
            LayoutToNetlist extracted from instance and cell port positions.
        """
        l2n = kdb.LayoutToNetlist(self.name, self.kcl.dbu)
        l2n.extract_netlist()
        il = l2n.internal_layout()
        il.assign(self.kcl.layout)

        called_kcells = [self.kcl[ci] for ci in self.called_cells()]
        called_kcells.sort(key=lambda c: c.hierarchy_levels())

        for c in called_kcells:
            c.circuit(
                l2n,
                port_types=port_types,
                exclude_purposes=exclude_purposes,
                ignore_unnamed=ignore_unnamed,
                allow_width_mismatch=allow_width_mismatch,
            )
        self.circuit(
            l2n,
            port_types=port_types,
            exclude_purposes=exclude_purposes,
            ignore_unnamed=ignore_unnamed,
            allow_width_mismatch=allow_width_mismatch,
        )
        return l2n

    def l2n_elec(
        self,
        mark_port_types: Iterable[str] = ("electrical", "RF", "DC"),
        connectivity: Sequence[
            tuple[kdb.LayerInfo]
            | tuple[kdb.LayerInfo, kdb.LayerInfo]
            | tuple[kdb.LayerInfo, kdb.LayerInfo, kdb.LayerInfo],
        ]
        | None = None,
        port_mapping: dict[str, dict[str | None, str]] | None = None,
    ) -> kdb.LayoutToNetlist:
        """Generate a LayoutToNetlist object from the port types.

        Uses electrical connectivity for extraction.

        Args:
            mark_port_types: The port types to consider for the netlist extraction.
            connectivity: Define connectivity between layers. These can be single
                layers (just consider this layer as metal), two layers (two metals
                which touch each other), or three layers (two metals with a via)
            port_mapping: Remap ports of cells to others. This allows to define
                equivalent ports in the lvs. E.g. `{"cell_A": {"o3": "o1", "o2"}}`
                will remap "o2" and "o3" to "o1". Making the three ports the same
                one for LVS.
        Returns:
            LayoutToNetlist extracted from electrical connectivity.
        """
        connectivity = connectivity or self.kcl.connectivity
        ly_elec = self.kcl.layout.dup()

        port_mapping = port_mapping or {}
        c_elec: kdb.Cell = ly_elec.cell(self.name)

        for ci in [c_elec.cell_index(), *c_elec.called_cells()]:
            c_ = self.kcl[ci]
            c = ly_elec.cell(c_.name)
            assert c_.name == c.name
            c.locked = False
            mapping = port_mapping.get(
                c_.name,
                port_mapping.get(c_.factory_name, {}) if c_.has_factory_name() else {},
            )
            for port in c_.ports:
                port_name = mapping.get(port.name, port.name)
                if (
                    port_name == port.name
                    and port.port_type in mark_port_types
                    and port.name is not None
                ):
                    c.shapes(port.layer_info).insert(
                        kdb.Text(string=port.name, trans=port.trans)
                    )

        l2n: kdb.LayoutToNetlist = kdb.LayoutToNetlist(
            kdb.RecursiveShapeIterator(
                ly_elec,
                ly_elec.cell(self.name),
                [],
            )
        )

        connectivity = connectivity or self.kcl.connectivity

        layers: dict[int, kdb.Region] = {}

        layer_infos = {
            ly_elec.get_info(ly_elec.layer(info))
            for layer_set in connectivity
            for info in layer_set
        }
        for info in layer_infos:
            l_ = l2n.make_layer(ly_elec.layer(info), info.name)
            layers[ly_elec.layer(info)] = l_
            l2n.connect(l_)
        for conn in connectivity:
            old_layer = layers[ly_elec.layer(conn[0])]

            for layer in conn[1:]:
                li = layers[ly_elec.layer(layer)]
                l2n.connect(old_layer, li)
                old_layer = li
        l2n.extract_netlist()
        l2n.check_extraction_errors()

        return l2n

    def netlist(
        self,
        port_types: Iterable[str] = ("optical",),
        mark_port_types: Iterable[str] = ("electrical", "RF", "DC"),
        connectivity: Sequence[
            tuple[kdb.LayerInfo, kdb.LayerInfo]
            | tuple[kdb.LayerInfo, kdb.LayerInfo, kdb.LayerInfo]
        ]
        | None = None,
        *,
        equivalent_ports: dict[str, list[list[str]]] | None = None,
        ignore_unnamed: bool = False,
        exclude_purposes: list[str] | None = None,
        allow_width_mismatch: bool = False,
    ) -> dict[str, Netlist]:
        if equivalent_ports is None:
            equivalent_ports = {}
            for ci in [self.cell_index(), *self.called_cells()]:
                c_ = self.kcl[ci]
                eqps: list[list[str]] | None = c_.lvs_equivalent_ports or None
                if c_.has_factory_name():
                    if c_.is_library_cell():
                        if c_.virtual:
                            eqps = (
                                _get_orig_cell(c_)
                                .kcl.virtual_factories[c_.factory_name]
                                .lvs_equivalent_ports
                            )
                        else:
                            eqps = (
                                _get_orig_cell(c_)
                                .kcl.factories[c_.factory_name]
                                .lvs_equivalent_ports
                            )
                    elif c_.virtual:
                        eqps = c_.kcl.virtual_factories[
                            c_.factory_name
                        ].lvs_equivalent_ports
                    else:
                        eqps = c_.kcl.factories[c_.factory_name].lvs_equivalent_ports
                if eqps is not None:
                    equivalent_ports[c_.name] = eqps
        port_mapping: dict[str, dict[str | None, str]] = defaultdict(dict)
        for cell_name, list_of_port_lists in equivalent_ports.items():
            for port_list in list_of_port_lists:
                if port_list:
                    p1 = port_list[0]
                    for port in port_list:
                        port_mapping[cell_name][port] = p1
        l2n_elec = self.l2n_elec(
            mark_port_types=mark_port_types,
            connectivity=connectivity,
            port_mapping=port_mapping,
        )
        l2n_opt = self.l2n_ports(
            port_types=port_types,
            exclude_purposes=exclude_purposes,
            ignore_unnamed=ignore_unnamed,
            allow_width_mismatch=allow_width_mismatch,
        )

        netlists: dict[str, Netlist] = {}

        for cell_name, eqps in equivalent_ports.items():
            for eqp_list in eqps:
                if eqp_list:
                    p1 = eqp_list[0]
                    for p in eqp_list:
                        port_mapping[cell_name][p] = p1

        for ci in [self.cell_index(), *self.called_cells()]:
            c_ = self.kcl[ci]
            name = c_.name

            nl = _get_netlist(
                c=c_,
                l2n_opt=l2n_opt,
                l2n_elec=l2n_elec,
                ignore_unnamed=ignore_unnamed,
                exclude_purposes=exclude_purposes,
            )
            if equivalent_ports.get(c_.name) is not None:
                nl = nl.lvs_equivalent(
                    cell_name=c_.name,
                    equivalent_ports=equivalent_ports,
                    port_mapping=port_mapping,
                )
            netlists[name] = nl
        return netlists

    def circuit(
        self,
        l2n: kdb.LayoutToNetlist,
        port_types: Iterable[str] = ("optical",),
        ignore_unnamed: bool = False,
        exclude_purposes: list[str] | None = None,
        allow_width_mismatch: bool = False,
    ) -> None:
        """Create the circuit of the KCell in the given netlist."""
        netlist = l2n.netlist()

        def port_filter(num_port: tuple[int, ProtoPort[Any]]) -> bool:
            return num_port[1].port_type in port_types

        circ = kdb.Circuit()
        circ.name = self.name
        circ.cell_index = self.cell_index()
        circ.boundary = self.boundary or kdb.DPolygon(self.dbbox())

        inst_ports: dict[
            str,
            dict[str, list[tuple[int, int, Instance, Port, kdb.SubCircuit]]],
        ] = {}
        cell_ports: dict[str, dict[str, list[tuple[int, Port]]]] = {}

        # sort the cell's ports by position and layer

        portnames: set[str] = set()

        for i, port in filter(
            port_filter, enumerate(Ports(kcl=self.kcl, bases=self.ports.bases))
        ):
            trans = port.trans.dup()
            trans.angle %= 2
            trans.mirror = False
            layer_info = self.kcl.layout.get_info(port.layer)
            layer = f"{layer_info.layer}_{layer_info.datatype}"

            if port.name in portnames:
                raise ValueError(
                    "Netlist extraction is not possible with"
                    f" colliding port names. Duplicate name: {port.name}"
                )

            v = trans.disp
            h = f"{v.x}_{v.y}"
            if h not in cell_ports:
                cell_ports[h] = {}
            if layer not in cell_ports[h]:
                cell_ports[h][layer] = []
            cell_ports[h][layer].append((i, port))

            if port.name:
                portnames.add(port.name)

        # create nets and connect pins for each cell_port
        for layer_dict in cell_ports.values():
            for _ports in layer_dict.values():
                net = circ.create_net(
                    "-".join(_port[1].name or f"{_port[0]}" for _port in _ports)
                )
                for i, port in _ports:
                    pin = circ.create_pin(port.name or f"{i}")
                    circ.connect_pin(pin, net)

        # sort the ports of all instances by position and layer
        for i, inst in enumerate(self.insts):
            name = inst.name
            subc = circ.create_subcircuit(
                netlist.circuit_by_cell_index(inst.cell_index), name
            )
            subc.trans = inst.dcplx_trans

            for j, port in filter(
                port_filter,
                enumerate(Ports(kcl=self.kcl, bases=[p.base for p in inst.ports])),
            ):
                trans = port.trans.dup()
                trans.angle %= 2
                trans.mirror = False
                v = trans.disp
                h = f"{v.x}_{v.y}"
                layer_info = self.kcl.layout.get_info(port.layer)
                layer = f"{layer_info.layer}_{layer_info.datatype}"
                if h not in inst_ports:
                    inst_ports[h] = {}
                if layer not in inst_ports[h]:
                    inst_ports[h][layer] = []
                inst_ports[h][layer].append(
                    (
                        i,
                        j,
                        Instance(kcl=self.kcl, instance=inst.instance),
                        port,
                        subc,
                    )
                )

        # go through each position and layer and connect ports to their matching cell
        # port or connect the instance ports
        for h, inst_layer_dict in inst_ports.items():
            for layer, ports in inst_layer_dict.items():
                if h in cell_ports and layer in cell_ports[h]:
                    # connect a cell port to its matching instance port
                    cellports = cell_ports[h][layer]

                    assert len(cellports) == 1, (
                        "Netlists with directly connect cell ports"
                        " are currently not supported"
                    )
                    assert len(ports) == 1, (
                        "Multiple instance "
                        f"{[instance_port_name(p[2], p[3]) for p in ports]}"
                        f"ports connected to the cell port {cellports[0]}"
                        " this is currently not supported and most likely a bug"
                    )

                    inst_port = ports[0]
                    port = inst_port[3]
                    if allow_width_mismatch:
                        port_check(
                            cellports[0][1], port, PortCheck.port_type + PortCheck.layer
                        )
                    else:
                        port_check(cellports[0][1], port, PortCheck.all_overlap)
                    subc = inst_port[4]
                    pin = subc.circuit_ref().pin_by_name(port.name or str(inst_port[1]))
                    net = circ.net_by_name(cellports[0][1].name or f"{cellports[0][0]}")
                    assert pin is not None
                    assert net is not None
                    subc.connect_pin(pin, net)
                else:
                    # connect instance ports to each other
                    name = "-".join(
                        [
                            (inst.name or str(i)) + "_" + (port.name or str(j))
                            for i, j, inst, port, _ in ports
                        ]
                    )

                    net = circ.create_net(name)
                    assert len(ports) <= 2, (  # noqa: PLR2004
                        "Optical connection with more than two ports are not supported "
                        f"{[_port[3] for _port in ports]}"
                    )
                    if len(ports) == 2:  # noqa: PLR2004
                        if allow_width_mismatch:
                            port_check(
                                ports[0][3],
                                ports[1][3],
                                PortCheck.layer
                                + PortCheck.port_type
                                + PortCheck.opposite,
                            )
                        else:
                            port_check(ports[0][3], ports[1][3], PortCheck.all_opposite)
                        for _, j, _, port, subc in ports:
                            subc.connect_pin(
                                subc.circuit_ref().pin_by_name(port.name or str(j)), net
                            )

        del_subcs: list[kdb.SubCircuit] = []
        if ignore_unnamed:
            del_subcs = [
                circ.subcircuit_by_name(inst.name)
                for inst in self.insts
                if not inst.is_named()
            ]
        if exclude_purposes:
            del_subcs.extend(
                circ.subcircuit_by_name(inst.name)
                for inst in self.insts
                if inst.purpose in exclude_purposes
            )

        for subc in del_subcs:
            nets: list[kdb.Net] = []
            for net in circ.each_net():
                for sc_pin in net.each_subcircuit_pin():
                    if sc_pin.subcircuit().id() == subc.id():
                        nets.append(net)
                        break

            if nets:
                target_net = nets[0]
                for net in nets[1:]:
                    spinrefs = [
                        (spin.pin(), spin.subcircuit())
                        for spin in net.each_subcircuit_pin()
                    ]
                    for pin, _subc in spinrefs:
                        _subc.disconnect_pin(pin)
                        if _subc not in del_subcs:
                            _subc.connect_pin(pin, target_net)
                    net_pins = [pinref.pin() for pinref in net.each_pin()]
                    for pin in net_pins:
                        circ.disconnect_pin(pin)
                        circ.connect_pin(pin, target_net)
                    circ.remove_net(net)
        for subc in del_subcs:
            circ.remove_subcircuit(subc)

        netlist.add(circ)

    def connectivity_check(
        self,
        port_types: list[str] | None = None,
        layers: list[int] | None = None,
        db: rdb.ReportDatabase | None = None,
        recursive: bool = True,
        add_cell_ports: bool = False,
        check_layer_connectivity: bool = True,
    ) -> rdb.ReportDatabase:
        """Create a ReportDatabase for port problems.

        Problems are overlapping ports that aren't aligned, more than two ports
        overlapping, width mismatch, port_type mismatch.

        Args:
            port_types: Filter for certain port typers
            layers: Only create the report for certain layers
            db: Use an existing ReportDatabase instead of creating a new one
            recursive: Create the report not only for this cell, but all child cells as
                well.
            add_cell_ports: Also add a category "CellPorts" which contains all the cells
                selected ports.
            check_layer_connectivity: Check whether the layer overlaps with instances.
        """
        if layers is None:
            layers = []
        if port_types is None:
            port_types = []
        db_: rdb.ReportDatabase = db or rdb.ReportDatabase(
            f"Connectivity Check {self.name}"
        )
        assert isinstance(db_, rdb.ReportDatabase)
        if recursive:
            cc = self.called_cells()
            for c in self.kcl.each_cell_bottom_up():
                if c in cc:
                    self.kcl[c].connectivity_check(
                        port_types=port_types,
                        db=db_,
                        recursive=False,
                        add_cell_ports=add_cell_ports,
                        layers=layers,
                    )
        db_cell = db_.create_cell(self.name)
        cell_ports: dict[int, dict[tuple[float, float], list[ProtoPort[Any]]]] = {}
        layer_cats: dict[int, rdb.RdbCategory] = {}

        def layer_cat(layer: int) -> rdb.RdbCategory:
            if layer not in layer_cats:
                if isinstance(layer, LayerEnum):
                    ln = str(layer.name)
                else:
                    li = self.kcl.get_info(layer)
                    ln = str(li).replace("/", "_")
                layer_cats[layer] = db_.category_by_path(ln) or db_.create_category(ln)
            return layer_cats[layer]

        for port in Ports(kcl=self.kcl, bases=self.ports.bases):
            if (not port_types or port.port_type in port_types) and (
                not layers or port.layer in layers
            ):
                if add_cell_ports:
                    c_cat = db_.category_by_path(
                        f"{layer_cat(port.layer).path()}.CellPorts"
                    ) or db_.create_category(layer_cat(port.layer), "CellPorts")
                    it = db_.create_item(db_cell, c_cat)
                    if port.name:
                        it.add_value(f"Port name: {port.name}")
                    if port.base.trans:
                        it.add_value(
                            self.kcl.to_um(
                                port_polygon(port.width).transformed(port.trans)
                            )
                        )
                    else:
                        it.add_value(
                            self.kcl.to_um(port_polygon(port.width)).transformed(
                                port.dcplx_trans
                            )
                        )
                xy = (port.x, port.y)
                if port.layer not in cell_ports:
                    cell_ports[port.layer] = {xy: [port]}
                elif xy not in cell_ports[port.layer]:
                    cell_ports[port.layer][xy] = [port]
                else:
                    cell_ports[port.layer][xy].append(port)
                rec_it = kdb.RecursiveShapeIterator(
                    self.kcl.layout,
                    self._base.kdb_cell,
                    port.layer,
                    kdb.Box(2, port.width).transformed(port.trans),
                )
                edges = kdb.Region(rec_it).merge().edges().merge()
                port_edge = kdb.Edge(0, port.width // 2, 0, -port.width // 2)
                if port.base.trans:
                    port_edge = port_edge.transformed(port.trans)
                else:
                    port_edge = port_edge.transformed(
                        kdb.ICplxTrans(port.dcplx_trans, self.kcl.dbu)
                    )
                p_edges = kdb.Edges([port_edge])
                phys_overlap = p_edges & edges
                if not phys_overlap.is_empty() and phys_overlap[0] != port_edge:
                    p_cat = db_.category_by_path(
                        layer_cat(port.layer).path() + ".PartialPhysicalShape"
                    ) or db_.create_category(
                        layer_cat(port.layer), "PartialPhysicalShape"
                    )
                    it = db_.create_item(db_cell, p_cat)
                    it.add_value(
                        "Insufficient overlap, partial overlap with polygon of"
                        f" {(phys_overlap[0].p1 - phys_overlap[0].p2).abs()}/"
                        f"{port.width}"
                    )
                    it.add_value(
                        self.kcl.to_um(port_polygon(port.width).transformed(port.trans))
                        if port.base.trans
                        else self.kcl.to_um(port_polygon(port.width)).transformed(
                            port.dcplx_trans
                        )
                    )
                elif phys_overlap.is_empty():
                    p_cat = db_.category_by_path(
                        layer_cat(port.layer).path() + ".MissingPhysicalShape"
                    ) or db_.create_category(
                        layer_cat(port.layer), "MissingPhysicalShape"
                    )
                    it = db_.create_item(db_cell, p_cat)
                    it.add_value(
                        f"Found no overlapping Edge with Port {port.name or str(port)}"
                    )
                    it.add_value(
                        self.kcl.to_um(port_polygon(port.width).transformed(port.trans))
                        if port.base.trans
                        else self.kcl.to_um(port_polygon(port.width)).transformed(
                            port.dcplx_trans
                        )
                    )

        inst_ports: dict[
            LayerEnum | int, dict[tuple[int, int], list[tuple[Port, KCell]]]
        ] = {}
        for inst in self.insts:
            for port in Ports(kcl=self.kcl, bases=[p.base for p in inst.ports]):
                if (not port_types or port.port_type in port_types) and (
                    not layers or port.layer in layers
                ):
                    xy = (port.x, port.y)
                    if port.layer not in inst_ports:
                        inst_ports[port.layer] = {xy: [(port, inst.cell.to_itype())]}
                    elif xy not in inst_ports[port.layer]:
                        inst_ports[port.layer][xy] = [(port, inst.cell.to_itype())]
                    else:
                        inst_ports[port.layer][xy].append((port, inst.cell.to_itype()))

        for layer, port_coord_mapping in inst_ports.items():
            lc = layer_cat(layer)
            for coord, ports in port_coord_mapping.items():
                match len(ports):
                    case 1:
                        if layer in cell_ports and coord in cell_ports[layer]:
                            ccp = check_cell_ports(
                                cell_ports[layer][coord][0], ports[0][0]
                            )
                            if ccp & 1:
                                subc = db_.category_by_path(
                                    lc.path() + ".WidthMismatch"
                                ) or db_.create_category(lc, "WidthMismatch")
                                create_port_error(
                                    ports[0][0],
                                    cell_ports[layer][coord][0],
                                    ports[0][1],
                                    self,
                                    db_,
                                    db_cell,
                                    subc,
                                    self.kcl.dbu,
                                )

                            if ccp & 2:
                                subc = db_.category_by_path(
                                    lc.path() + ".AngleMismatch"
                                ) or db_.create_category(lc, "AngleMismatch")
                                create_port_error(
                                    ports[0][0],
                                    cell_ports[layer][coord][0],
                                    ports[0][1],
                                    self,
                                    db_,
                                    db_cell,
                                    subc,
                                    self.kcl.dbu,
                                )
                            if ccp & 4:
                                subc = db_.category_by_path(
                                    lc.path() + ".TypeMismatch"
                                ) or db_.create_category(lc, "TypeMismatch")
                                create_port_error(
                                    ports[0][0],
                                    cell_ports[layer][coord][0],
                                    ports[0][1],
                                    self,
                                    db_,
                                    db_cell,
                                    subc,
                                    self.kcl.dbu,
                                )
                        else:
                            subc = db_.category_by_path(
                                lc.path() + ".OrphanPort"
                            ) or db_.create_category(lc, "OrphanPort")
                            it = db_.create_item(db_cell, subc)
                            it.add_value(
                                f"Port Name: {ports[0][1].name}"
                                f"{ports[0][0].name or str(ports[0][0])})"
                            )
                            if ports[0][0]._base.trans:
                                it.add_value(
                                    self.kcl.to_um(
                                        port_polygon(ports[0][0].width).transformed(
                                            ports[0][0]._base.trans
                                        )
                                    )
                                )
                            else:
                                it.add_value(
                                    self.kcl.to_um(
                                        port_polygon(port.width)
                                    ).transformed(port.dcplx_trans)
                                )

                    case 2:
                        cip = check_inst_ports(ports[0][0], ports[1][0])
                        if cip & 1:
                            subc = db_.category_by_path(
                                lc.path() + ".WidthMismatch"
                            ) or db_.create_category(lc, "WidthMismatch")
                            create_port_error(
                                ports[0][0],
                                ports[1][0],
                                ports[0][1],
                                ports[1][1],
                                db_,
                                db_cell,
                                subc,
                                self.kcl.dbu,
                            )

                        if cip & 2:
                            subc = db_.category_by_path(
                                lc.path() + ".AngleMismatch"
                            ) or db_.create_category(lc, "AngleMismatch")
                            create_port_error(
                                ports[0][0],
                                ports[1][0],
                                ports[0][1],
                                ports[1][1],
                                db_,
                                db_cell,
                                subc,
                                self.kcl.dbu,
                            )
                        if cip & 4:
                            subc = db_.category_by_path(
                                lc.path() + ".TypeMismatch"
                            ) or db_.create_category(lc, "TypeMismatch")
                            create_port_error(
                                ports[0][0],
                                ports[1][0],
                                ports[0][1],
                                ports[1][1],
                                db_,
                                db_cell,
                                subc,
                                self.kcl.dbu,
                            )
                        if layer in cell_ports and coord in cell_ports[layer]:
                            subc = db_.category_by_path(
                                lc.path() + ".portoverlap"
                            ) or db_.create_category(lc, "portoverlap")
                            it = db_.create_item(db_cell, subc)
                            text = "Port Names: "
                            values: list[rdb.RdbItemValue] = []
                            cell_port = cell_ports[layer][coord][0]
                            text += (
                                f"{self.name}."
                                f"{cell_port.name or cell_port.trans.to_s()}/"
                            )
                            if cell_port.base.trans:
                                values.append(
                                    rdb.RdbItemValue(
                                        self.kcl.to_um(
                                            port_polygon(cell_port.width).transformed(
                                                cell_port.base.trans
                                            )
                                        )
                                    )
                                )
                            else:
                                values.append(
                                    rdb.RdbItemValue(
                                        self.kcl.to_um(
                                            port_polygon(cell_port.width)
                                        ).transformed(cell_port.dcplx_trans)
                                    )
                                )
                            for _port in ports:
                                text += (
                                    f"{_port[1].name}."
                                    f"{_port[0].name or _port[0].trans.to_s()}/"
                                )

                                values.append(
                                    rdb.RdbItemValue(
                                        self.kcl.to_um(
                                            port_polygon(_port[0].width).transformed(
                                                _port[0].trans
                                            )
                                        )
                                    )
                                )
                            it.add_value(text[:-1])
                            for value in values:
                                it.add_value(value)

                    case x if x > 2:  # noqa: PLR2004
                        subc = db_.category_by_path(
                            lc.path() + ".portoverlap"
                        ) or db_.create_category(lc, "portoverlap")
                        it = db_.create_item(db_cell, subc)
                        text = "Port Names: "
                        values = []
                        for _port in ports:
                            text += (
                                f"{_port[1].name}."
                                f"{_port[0].name or _port[0].trans.to_s()}/"
                            )

                            values.append(
                                rdb.RdbItemValue(
                                    self.kcl.to_um(
                                        port_polygon(_port[0].width).transformed(
                                            _port[0].trans
                                        )
                                    )
                                )
                            )
                        it.add_value(text[:-1])
                        for value in values:
                            it.add_value(value)
                    case _:
                        raise ValueError(f"Unexpected number of ports: {len(ports)}")
            if check_layer_connectivity:
                error_region_shapes = kdb.Region()
                error_region_instances = kdb.Region()
                reg = kdb.Region(self.shapes(layer))
                inst_regions: dict[int, kdb.Region] = {}
                inst_region = kdb.Region()
                for i, inst in enumerate(self.insts):
                    inst_region_ = kdb.Region(inst.ibbox(layer))
                    inst_shapes: kdb.Region | None = None
                    if not (inst_region & inst_region_).is_empty():
                        if inst_shapes is None:
                            inst_shapes = kdb.Region()
                            shape_it = self.begin_shapes_rec_overlapping(
                                layer, inst.bbox(layer)
                            )
                            shape_it.select_cells([inst.cell.cell_index()])
                            shape_it.min_depth = 1
                            shape_it.shape_flags = kdb.Shapes.SRegions
                            for _it in shape_it.each():
                                if _it.path()[0].inst() == inst.instance:
                                    inst_shapes.insert(
                                        _it.shape().polygon.transformed(_it.trans())
                                    )

                        for j, _reg in inst_regions.items():
                            if _reg & inst_region_:
                                reg_ = kdb.Region()
                                shape_it = self.begin_shapes_rec_touching(
                                    layer, (_reg & inst_region_).bbox()
                                )
                                shape_it.select_cells([self.insts[j].cell.cell_index()])
                                shape_it.min_depth = 1
                                shape_it.shape_flags = kdb.Shapes.SRegions
                                for _it in shape_it.each():
                                    if _it.path()[0].inst() == self.insts[j].instance:
                                        reg_.insert(
                                            _it.shape().polygon.transformed(_it.trans())
                                        )

                                error_region_instances.insert(reg_ & inst_shapes)

                    if not (inst_region_ & reg).is_empty():
                        rec_it = self.begin_shapes_rec_touching(
                            layer, (inst_region_ & reg).bbox()
                        )
                        rec_it.min_depth = 1
                        error_region_shapes += kdb.Region(rec_it) & reg
                    inst_region += inst_region_
                    inst_regions[i] = inst_region_
                if not error_region_shapes.is_empty():
                    sc = db_.category_by_path(
                        layer_cat(layer).path() + ".ShapeInstanceshapeOverlap"
                    ) or db_.create_category(
                        layer_cat(layer), "ShapeInstanceshapeOverlap"
                    )
                    for poly in error_region_shapes.merge().each():
                        it = db_.create_item(db_cell, sc)
                        it.add_value("Shapes overlapping with shapes of instances")
                        it.add_value(self.kcl.to_um(poly.downcast()))
                if not error_region_instances.is_empty():
                    sc = db_.category_by_path(
                        layer_cat(layer).path() + ".InstanceshapeOverlap"
                    ) or db_.create_category(layer_cat(layer), "InstanceshapeOverlap")
                    for poly in error_region_instances.merge().each():
                        it = db_.create_item(db_cell, sc)
                        it.add_value(
                            "Instance shapes overlapping with shapes of other instances"
                        )
                        it.add_value(self.kcl.to_um(poly.downcast()))

        return db_

    def insert_vinsts(self, recursive: bool = True) -> None:
        """Insert all virtual instances and create Instances of real KCells."""
        if not self._base.kdb_cell._destroyed():
            for vi in self._base.vinsts:
                vi.insert_into(self)
            self._base.vinsts.clear()
            called_cell_indexes = self._base.kdb_cell.called_cells()
            for c in sorted(
                {
                    self.kcl[ci]
                    for ci in called_cell_indexes
                    if not self.kcl[ci].kdb_cell._destroyed()
                }
                & self.kcl.tkcells.keys(),
                key=lambda c: c.hierarchy_levels(),
            ):
                for vi in c._base.vinsts:
                    vi.insert_into(c)
                c._base.vinsts.clear()

    @abstractmethod
    def get_cross_section(
        self,
        cross_section: str
        | dict[str, Any]
        | Callable[..., CrossSection | DCrossSection]
        | SymmetricalCrossSection,
        **cross_section_kwargs: Any,
    ) -> TCrossSection[TUnit]: ...

    @property
    def lvs_equivalent_ports(self) -> list[list[str]] | None:
        return self._base.lvs_equivalent_ports

ghost_cell property writable

ghost_cell: bool

Returns a value indicating whether the cell is a "ghost cell".

prop_id property writable

prop_id: int

Gets the properties ID associated with the cell.

__copy__

__copy__() -> Self

Enables use of copy.copy and copy.deep_copy.

Source code in kfactory/kcell.py
753
754
755
def __copy__(self) -> Self:
    """Enables use of `copy.copy` and `copy.deep_copy`."""
    return self.dup()

__getattr__

__getattr__(name: str) -> Any

If KCell doesn't have an attribute, look in the KLayout Cell.

Source code in kfactory/kcell.py
735
736
737
738
739
740
def __getattr__(self, name: str) -> Any:
    """If KCell doesn't have an attribute, look in the KLayout Cell."""
    try:
        return super().__getattr__(name)  # type: ignore[misc]
    except Exception:
        return getattr(self._base, name)

__getitem__ abstractmethod

__getitem__(key: int | str | None) -> ProtoPort[TUnit]

Returns port from instance.

Source code in kfactory/kcell.py
676
677
678
679
@abstractmethod
def __getitem__(self, key: int | str | None) -> ProtoPort[TUnit]:
    """Returns port from instance."""
    ...

__hash__

__hash__() -> int

Hash the KCell.

Source code in kfactory/kcell.py
704
705
706
def __hash__(self) -> int:
    """Hash the KCell."""
    return hash((self._base.kcl.library.name(), self._base.kdb_cell.cell_index()))

add_port abstractmethod

add_port(
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> ProtoPort[Any]

Add an existing port. E.g. from an instance to propagate the port.

Parameters:

Name Type Description Default
port ProtoPort[Any]

The port to add.

required
name str | None

Overwrite the name of the port

None
keep_mirror bool

Keep the mirror part of the transformation of a port if True, else set the mirror flag to False.

False
Source code in kfactory/kcell.py
897
898
899
900
901
902
903
904
@abstractmethod
def add_port(
    self,
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> ProtoPort[Any]: ...

auto_rename_ports

auto_rename_ports(
    rename_func: Callable[..., None] | None = None,
) -> None

Rename the ports with the schema angle -> "NSWE" and sort by x and y.

Parameters:

Name Type Description Default
rename_func Callable[..., None] | None

Function that takes Iterable[Port] and renames them. This can of course contain a filter and only rename some of the ports

None
Source code in kfactory/kcell.py
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
def auto_rename_ports(self, rename_func: Callable[..., None] | None = None) -> None:
    """Rename the ports with the schema angle -> "NSWE" and sort by x and y.

    Args:
        rename_func: Function that takes Iterable[Port] and renames them.
            This can of course contain a filter and only rename some of the ports
    """
    if self.locked:
        raise LockedError(self)
    if rename_func is None:
        self.kcl.rename_function(self.ports)
    else:
        rename_func(self.ports)

cell_index

cell_index() -> int

Gets the cell index.

Source code in kfactory/kcell.py
742
743
744
def cell_index(self) -> int:
    """Gets the cell index."""
    return self._base.kdb_cell.cell_index()

circuit

circuit(
    l2n: LayoutToNetlist,
    port_types: Iterable[str] = ("optical",),
    ignore_unnamed: bool = False,
    exclude_purposes: list[str] | None = None,
    allow_width_mismatch: bool = False,
) -> None

Create the circuit of the KCell in the given netlist.

Source code in kfactory/kcell.py
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
def circuit(
    self,
    l2n: kdb.LayoutToNetlist,
    port_types: Iterable[str] = ("optical",),
    ignore_unnamed: bool = False,
    exclude_purposes: list[str] | None = None,
    allow_width_mismatch: bool = False,
) -> None:
    """Create the circuit of the KCell in the given netlist."""
    netlist = l2n.netlist()

    def port_filter(num_port: tuple[int, ProtoPort[Any]]) -> bool:
        return num_port[1].port_type in port_types

    circ = kdb.Circuit()
    circ.name = self.name
    circ.cell_index = self.cell_index()
    circ.boundary = self.boundary or kdb.DPolygon(self.dbbox())

    inst_ports: dict[
        str,
        dict[str, list[tuple[int, int, Instance, Port, kdb.SubCircuit]]],
    ] = {}
    cell_ports: dict[str, dict[str, list[tuple[int, Port]]]] = {}

    # sort the cell's ports by position and layer

    portnames: set[str] = set()

    for i, port in filter(
        port_filter, enumerate(Ports(kcl=self.kcl, bases=self.ports.bases))
    ):
        trans = port.trans.dup()
        trans.angle %= 2
        trans.mirror = False
        layer_info = self.kcl.layout.get_info(port.layer)
        layer = f"{layer_info.layer}_{layer_info.datatype}"

        if port.name in portnames:
            raise ValueError(
                "Netlist extraction is not possible with"
                f" colliding port names. Duplicate name: {port.name}"
            )

        v = trans.disp
        h = f"{v.x}_{v.y}"
        if h not in cell_ports:
            cell_ports[h] = {}
        if layer not in cell_ports[h]:
            cell_ports[h][layer] = []
        cell_ports[h][layer].append((i, port))

        if port.name:
            portnames.add(port.name)

    # create nets and connect pins for each cell_port
    for layer_dict in cell_ports.values():
        for _ports in layer_dict.values():
            net = circ.create_net(
                "-".join(_port[1].name or f"{_port[0]}" for _port in _ports)
            )
            for i, port in _ports:
                pin = circ.create_pin(port.name or f"{i}")
                circ.connect_pin(pin, net)

    # sort the ports of all instances by position and layer
    for i, inst in enumerate(self.insts):
        name = inst.name
        subc = circ.create_subcircuit(
            netlist.circuit_by_cell_index(inst.cell_index), name
        )
        subc.trans = inst.dcplx_trans

        for j, port in filter(
            port_filter,
            enumerate(Ports(kcl=self.kcl, bases=[p.base for p in inst.ports])),
        ):
            trans = port.trans.dup()
            trans.angle %= 2
            trans.mirror = False
            v = trans.disp
            h = f"{v.x}_{v.y}"
            layer_info = self.kcl.layout.get_info(port.layer)
            layer = f"{layer_info.layer}_{layer_info.datatype}"
            if h not in inst_ports:
                inst_ports[h] = {}
            if layer not in inst_ports[h]:
                inst_ports[h][layer] = []
            inst_ports[h][layer].append(
                (
                    i,
                    j,
                    Instance(kcl=self.kcl, instance=inst.instance),
                    port,
                    subc,
                )
            )

    # go through each position and layer and connect ports to their matching cell
    # port or connect the instance ports
    for h, inst_layer_dict in inst_ports.items():
        for layer, ports in inst_layer_dict.items():
            if h in cell_ports and layer in cell_ports[h]:
                # connect a cell port to its matching instance port
                cellports = cell_ports[h][layer]

                assert len(cellports) == 1, (
                    "Netlists with directly connect cell ports"
                    " are currently not supported"
                )
                assert len(ports) == 1, (
                    "Multiple instance "
                    f"{[instance_port_name(p[2], p[3]) for p in ports]}"
                    f"ports connected to the cell port {cellports[0]}"
                    " this is currently not supported and most likely a bug"
                )

                inst_port = ports[0]
                port = inst_port[3]
                if allow_width_mismatch:
                    port_check(
                        cellports[0][1], port, PortCheck.port_type + PortCheck.layer
                    )
                else:
                    port_check(cellports[0][1], port, PortCheck.all_overlap)
                subc = inst_port[4]
                pin = subc.circuit_ref().pin_by_name(port.name or str(inst_port[1]))
                net = circ.net_by_name(cellports[0][1].name or f"{cellports[0][0]}")
                assert pin is not None
                assert net is not None
                subc.connect_pin(pin, net)
            else:
                # connect instance ports to each other
                name = "-".join(
                    [
                        (inst.name or str(i)) + "_" + (port.name or str(j))
                        for i, j, inst, port, _ in ports
                    ]
                )

                net = circ.create_net(name)
                assert len(ports) <= 2, (  # noqa: PLR2004
                    "Optical connection with more than two ports are not supported "
                    f"{[_port[3] for _port in ports]}"
                )
                if len(ports) == 2:  # noqa: PLR2004
                    if allow_width_mismatch:
                        port_check(
                            ports[0][3],
                            ports[1][3],
                            PortCheck.layer
                            + PortCheck.port_type
                            + PortCheck.opposite,
                        )
                    else:
                        port_check(ports[0][3], ports[1][3], PortCheck.all_opposite)
                    for _, j, _, port, subc in ports:
                        subc.connect_pin(
                            subc.circuit_ref().pin_by_name(port.name or str(j)), net
                        )

    del_subcs: list[kdb.SubCircuit] = []
    if ignore_unnamed:
        del_subcs = [
            circ.subcircuit_by_name(inst.name)
            for inst in self.insts
            if not inst.is_named()
        ]
    if exclude_purposes:
        del_subcs.extend(
            circ.subcircuit_by_name(inst.name)
            for inst in self.insts
            if inst.purpose in exclude_purposes
        )

    for subc in del_subcs:
        nets: list[kdb.Net] = []
        for net in circ.each_net():
            for sc_pin in net.each_subcircuit_pin():
                if sc_pin.subcircuit().id() == subc.id():
                    nets.append(net)
                    break

        if nets:
            target_net = nets[0]
            for net in nets[1:]:
                spinrefs = [
                    (spin.pin(), spin.subcircuit())
                    for spin in net.each_subcircuit_pin()
                ]
                for pin, _subc in spinrefs:
                    _subc.disconnect_pin(pin)
                    if _subc not in del_subcs:
                        _subc.connect_pin(pin, target_net)
                net_pins = [pinref.pin() for pinref in net.each_pin()]
                for pin in net_pins:
                    circ.disconnect_pin(pin)
                    circ.connect_pin(pin, target_net)
                circ.remove_net(net)
    for subc in del_subcs:
        circ.remove_subcircuit(subc)

    netlist.add(circ)

connectivity_check

connectivity_check(
    port_types: list[str] | None = None,
    layers: list[int] | None = None,
    db: ReportDatabase | None = None,
    recursive: bool = True,
    add_cell_ports: bool = False,
    check_layer_connectivity: bool = True,
) -> rdb.ReportDatabase

Create a ReportDatabase for port problems.

Problems are overlapping ports that aren't aligned, more than two ports overlapping, width mismatch, port_type mismatch.

Parameters:

Name Type Description Default
port_types list[str] | None

Filter for certain port typers

None
layers list[int] | None

Only create the report for certain layers

None
db ReportDatabase | None

Use an existing ReportDatabase instead of creating a new one

None
recursive bool

Create the report not only for this cell, but all child cells as well.

True
add_cell_ports bool

Also add a category "CellPorts" which contains all the cells selected ports.

False
check_layer_connectivity bool

Check whether the layer overlaps with instances.

True
Source code in kfactory/kcell.py
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
def connectivity_check(
    self,
    port_types: list[str] | None = None,
    layers: list[int] | None = None,
    db: rdb.ReportDatabase | None = None,
    recursive: bool = True,
    add_cell_ports: bool = False,
    check_layer_connectivity: bool = True,
) -> rdb.ReportDatabase:
    """Create a ReportDatabase for port problems.

    Problems are overlapping ports that aren't aligned, more than two ports
    overlapping, width mismatch, port_type mismatch.

    Args:
        port_types: Filter for certain port typers
        layers: Only create the report for certain layers
        db: Use an existing ReportDatabase instead of creating a new one
        recursive: Create the report not only for this cell, but all child cells as
            well.
        add_cell_ports: Also add a category "CellPorts" which contains all the cells
            selected ports.
        check_layer_connectivity: Check whether the layer overlaps with instances.
    """
    if layers is None:
        layers = []
    if port_types is None:
        port_types = []
    db_: rdb.ReportDatabase = db or rdb.ReportDatabase(
        f"Connectivity Check {self.name}"
    )
    assert isinstance(db_, rdb.ReportDatabase)
    if recursive:
        cc = self.called_cells()
        for c in self.kcl.each_cell_bottom_up():
            if c in cc:
                self.kcl[c].connectivity_check(
                    port_types=port_types,
                    db=db_,
                    recursive=False,
                    add_cell_ports=add_cell_ports,
                    layers=layers,
                )
    db_cell = db_.create_cell(self.name)
    cell_ports: dict[int, dict[tuple[float, float], list[ProtoPort[Any]]]] = {}
    layer_cats: dict[int, rdb.RdbCategory] = {}

    def layer_cat(layer: int) -> rdb.RdbCategory:
        if layer not in layer_cats:
            if isinstance(layer, LayerEnum):
                ln = str(layer.name)
            else:
                li = self.kcl.get_info(layer)
                ln = str(li).replace("/", "_")
            layer_cats[layer] = db_.category_by_path(ln) or db_.create_category(ln)
        return layer_cats[layer]

    for port in Ports(kcl=self.kcl, bases=self.ports.bases):
        if (not port_types or port.port_type in port_types) and (
            not layers or port.layer in layers
        ):
            if add_cell_ports:
                c_cat = db_.category_by_path(
                    f"{layer_cat(port.layer).path()}.CellPorts"
                ) or db_.create_category(layer_cat(port.layer), "CellPorts")
                it = db_.create_item(db_cell, c_cat)
                if port.name:
                    it.add_value(f"Port name: {port.name}")
                if port.base.trans:
                    it.add_value(
                        self.kcl.to_um(
                            port_polygon(port.width).transformed(port.trans)
                        )
                    )
                else:
                    it.add_value(
                        self.kcl.to_um(port_polygon(port.width)).transformed(
                            port.dcplx_trans
                        )
                    )
            xy = (port.x, port.y)
            if port.layer not in cell_ports:
                cell_ports[port.layer] = {xy: [port]}
            elif xy not in cell_ports[port.layer]:
                cell_ports[port.layer][xy] = [port]
            else:
                cell_ports[port.layer][xy].append(port)
            rec_it = kdb.RecursiveShapeIterator(
                self.kcl.layout,
                self._base.kdb_cell,
                port.layer,
                kdb.Box(2, port.width).transformed(port.trans),
            )
            edges = kdb.Region(rec_it).merge().edges().merge()
            port_edge = kdb.Edge(0, port.width // 2, 0, -port.width // 2)
            if port.base.trans:
                port_edge = port_edge.transformed(port.trans)
            else:
                port_edge = port_edge.transformed(
                    kdb.ICplxTrans(port.dcplx_trans, self.kcl.dbu)
                )
            p_edges = kdb.Edges([port_edge])
            phys_overlap = p_edges & edges
            if not phys_overlap.is_empty() and phys_overlap[0] != port_edge:
                p_cat = db_.category_by_path(
                    layer_cat(port.layer).path() + ".PartialPhysicalShape"
                ) or db_.create_category(
                    layer_cat(port.layer), "PartialPhysicalShape"
                )
                it = db_.create_item(db_cell, p_cat)
                it.add_value(
                    "Insufficient overlap, partial overlap with polygon of"
                    f" {(phys_overlap[0].p1 - phys_overlap[0].p2).abs()}/"
                    f"{port.width}"
                )
                it.add_value(
                    self.kcl.to_um(port_polygon(port.width).transformed(port.trans))
                    if port.base.trans
                    else self.kcl.to_um(port_polygon(port.width)).transformed(
                        port.dcplx_trans
                    )
                )
            elif phys_overlap.is_empty():
                p_cat = db_.category_by_path(
                    layer_cat(port.layer).path() + ".MissingPhysicalShape"
                ) or db_.create_category(
                    layer_cat(port.layer), "MissingPhysicalShape"
                )
                it = db_.create_item(db_cell, p_cat)
                it.add_value(
                    f"Found no overlapping Edge with Port {port.name or str(port)}"
                )
                it.add_value(
                    self.kcl.to_um(port_polygon(port.width).transformed(port.trans))
                    if port.base.trans
                    else self.kcl.to_um(port_polygon(port.width)).transformed(
                        port.dcplx_trans
                    )
                )

    inst_ports: dict[
        LayerEnum | int, dict[tuple[int, int], list[tuple[Port, KCell]]]
    ] = {}
    for inst in self.insts:
        for port in Ports(kcl=self.kcl, bases=[p.base for p in inst.ports]):
            if (not port_types or port.port_type in port_types) and (
                not layers or port.layer in layers
            ):
                xy = (port.x, port.y)
                if port.layer not in inst_ports:
                    inst_ports[port.layer] = {xy: [(port, inst.cell.to_itype())]}
                elif xy not in inst_ports[port.layer]:
                    inst_ports[port.layer][xy] = [(port, inst.cell.to_itype())]
                else:
                    inst_ports[port.layer][xy].append((port, inst.cell.to_itype()))

    for layer, port_coord_mapping in inst_ports.items():
        lc = layer_cat(layer)
        for coord, ports in port_coord_mapping.items():
            match len(ports):
                case 1:
                    if layer in cell_ports and coord in cell_ports[layer]:
                        ccp = check_cell_ports(
                            cell_ports[layer][coord][0], ports[0][0]
                        )
                        if ccp & 1:
                            subc = db_.category_by_path(
                                lc.path() + ".WidthMismatch"
                            ) or db_.create_category(lc, "WidthMismatch")
                            create_port_error(
                                ports[0][0],
                                cell_ports[layer][coord][0],
                                ports[0][1],
                                self,
                                db_,
                                db_cell,
                                subc,
                                self.kcl.dbu,
                            )

                        if ccp & 2:
                            subc = db_.category_by_path(
                                lc.path() + ".AngleMismatch"
                            ) or db_.create_category(lc, "AngleMismatch")
                            create_port_error(
                                ports[0][0],
                                cell_ports[layer][coord][0],
                                ports[0][1],
                                self,
                                db_,
                                db_cell,
                                subc,
                                self.kcl.dbu,
                            )
                        if ccp & 4:
                            subc = db_.category_by_path(
                                lc.path() + ".TypeMismatch"
                            ) or db_.create_category(lc, "TypeMismatch")
                            create_port_error(
                                ports[0][0],
                                cell_ports[layer][coord][0],
                                ports[0][1],
                                self,
                                db_,
                                db_cell,
                                subc,
                                self.kcl.dbu,
                            )
                    else:
                        subc = db_.category_by_path(
                            lc.path() + ".OrphanPort"
                        ) or db_.create_category(lc, "OrphanPort")
                        it = db_.create_item(db_cell, subc)
                        it.add_value(
                            f"Port Name: {ports[0][1].name}"
                            f"{ports[0][0].name or str(ports[0][0])})"
                        )
                        if ports[0][0]._base.trans:
                            it.add_value(
                                self.kcl.to_um(
                                    port_polygon(ports[0][0].width).transformed(
                                        ports[0][0]._base.trans
                                    )
                                )
                            )
                        else:
                            it.add_value(
                                self.kcl.to_um(
                                    port_polygon(port.width)
                                ).transformed(port.dcplx_trans)
                            )

                case 2:
                    cip = check_inst_ports(ports[0][0], ports[1][0])
                    if cip & 1:
                        subc = db_.category_by_path(
                            lc.path() + ".WidthMismatch"
                        ) or db_.create_category(lc, "WidthMismatch")
                        create_port_error(
                            ports[0][0],
                            ports[1][0],
                            ports[0][1],
                            ports[1][1],
                            db_,
                            db_cell,
                            subc,
                            self.kcl.dbu,
                        )

                    if cip & 2:
                        subc = db_.category_by_path(
                            lc.path() + ".AngleMismatch"
                        ) or db_.create_category(lc, "AngleMismatch")
                        create_port_error(
                            ports[0][0],
                            ports[1][0],
                            ports[0][1],
                            ports[1][1],
                            db_,
                            db_cell,
                            subc,
                            self.kcl.dbu,
                        )
                    if cip & 4:
                        subc = db_.category_by_path(
                            lc.path() + ".TypeMismatch"
                        ) or db_.create_category(lc, "TypeMismatch")
                        create_port_error(
                            ports[0][0],
                            ports[1][0],
                            ports[0][1],
                            ports[1][1],
                            db_,
                            db_cell,
                            subc,
                            self.kcl.dbu,
                        )
                    if layer in cell_ports and coord in cell_ports[layer]:
                        subc = db_.category_by_path(
                            lc.path() + ".portoverlap"
                        ) or db_.create_category(lc, "portoverlap")
                        it = db_.create_item(db_cell, subc)
                        text = "Port Names: "
                        values: list[rdb.RdbItemValue] = []
                        cell_port = cell_ports[layer][coord][0]
                        text += (
                            f"{self.name}."
                            f"{cell_port.name or cell_port.trans.to_s()}/"
                        )
                        if cell_port.base.trans:
                            values.append(
                                rdb.RdbItemValue(
                                    self.kcl.to_um(
                                        port_polygon(cell_port.width).transformed(
                                            cell_port.base.trans
                                        )
                                    )
                                )
                            )
                        else:
                            values.append(
                                rdb.RdbItemValue(
                                    self.kcl.to_um(
                                        port_polygon(cell_port.width)
                                    ).transformed(cell_port.dcplx_trans)
                                )
                            )
                        for _port in ports:
                            text += (
                                f"{_port[1].name}."
                                f"{_port[0].name or _port[0].trans.to_s()}/"
                            )

                            values.append(
                                rdb.RdbItemValue(
                                    self.kcl.to_um(
                                        port_polygon(_port[0].width).transformed(
                                            _port[0].trans
                                        )
                                    )
                                )
                            )
                        it.add_value(text[:-1])
                        for value in values:
                            it.add_value(value)

                case x if x > 2:  # noqa: PLR2004
                    subc = db_.category_by_path(
                        lc.path() + ".portoverlap"
                    ) or db_.create_category(lc, "portoverlap")
                    it = db_.create_item(db_cell, subc)
                    text = "Port Names: "
                    values = []
                    for _port in ports:
                        text += (
                            f"{_port[1].name}."
                            f"{_port[0].name or _port[0].trans.to_s()}/"
                        )

                        values.append(
                            rdb.RdbItemValue(
                                self.kcl.to_um(
                                    port_polygon(_port[0].width).transformed(
                                        _port[0].trans
                                    )
                                )
                            )
                        )
                    it.add_value(text[:-1])
                    for value in values:
                        it.add_value(value)
                case _:
                    raise ValueError(f"Unexpected number of ports: {len(ports)}")
        if check_layer_connectivity:
            error_region_shapes = kdb.Region()
            error_region_instances = kdb.Region()
            reg = kdb.Region(self.shapes(layer))
            inst_regions: dict[int, kdb.Region] = {}
            inst_region = kdb.Region()
            for i, inst in enumerate(self.insts):
                inst_region_ = kdb.Region(inst.ibbox(layer))
                inst_shapes: kdb.Region | None = None
                if not (inst_region & inst_region_).is_empty():
                    if inst_shapes is None:
                        inst_shapes = kdb.Region()
                        shape_it = self.begin_shapes_rec_overlapping(
                            layer, inst.bbox(layer)
                        )
                        shape_it.select_cells([inst.cell.cell_index()])
                        shape_it.min_depth = 1
                        shape_it.shape_flags = kdb.Shapes.SRegions
                        for _it in shape_it.each():
                            if _it.path()[0].inst() == inst.instance:
                                inst_shapes.insert(
                                    _it.shape().polygon.transformed(_it.trans())
                                )

                    for j, _reg in inst_regions.items():
                        if _reg & inst_region_:
                            reg_ = kdb.Region()
                            shape_it = self.begin_shapes_rec_touching(
                                layer, (_reg & inst_region_).bbox()
                            )
                            shape_it.select_cells([self.insts[j].cell.cell_index()])
                            shape_it.min_depth = 1
                            shape_it.shape_flags = kdb.Shapes.SRegions
                            for _it in shape_it.each():
                                if _it.path()[0].inst() == self.insts[j].instance:
                                    reg_.insert(
                                        _it.shape().polygon.transformed(_it.trans())
                                    )

                            error_region_instances.insert(reg_ & inst_shapes)

                if not (inst_region_ & reg).is_empty():
                    rec_it = self.begin_shapes_rec_touching(
                        layer, (inst_region_ & reg).bbox()
                    )
                    rec_it.min_depth = 1
                    error_region_shapes += kdb.Region(rec_it) & reg
                inst_region += inst_region_
                inst_regions[i] = inst_region_
            if not error_region_shapes.is_empty():
                sc = db_.category_by_path(
                    layer_cat(layer).path() + ".ShapeInstanceshapeOverlap"
                ) or db_.create_category(
                    layer_cat(layer), "ShapeInstanceshapeOverlap"
                )
                for poly in error_region_shapes.merge().each():
                    it = db_.create_item(db_cell, sc)
                    it.add_value("Shapes overlapping with shapes of instances")
                    it.add_value(self.kcl.to_um(poly.downcast()))
            if not error_region_instances.is_empty():
                sc = db_.category_by_path(
                    layer_cat(layer).path() + ".InstanceshapeOverlap"
                ) or db_.create_category(layer_cat(layer), "InstanceshapeOverlap")
                for poly in error_region_instances.merge().each():
                    it = db_.create_item(db_cell, sc)
                    it.add_value(
                        "Instance shapes overlapping with shapes of other instances"
                    )
                    it.add_value(self.kcl.to_um(poly.downcast()))

    return db_

convert_to_static

convert_to_static(recursive: bool = True) -> None

Convert the KCell to a static cell if it is pdk KCell.

Source code in kfactory/kcell.py
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
def convert_to_static(self, recursive: bool = True) -> None:
    """Convert the KCell to a static cell if it is pdk KCell."""
    if self.library().name() == self.kcl.name:
        raise ValueError(f"KCell {self.qname()} is already a static KCell.")
    from .layout import kcls

    lib_cell = kcls[self.library().name()][self.library_cell_index()]
    lib_cell.set_meta_data()
    kdb_cell = self.kcl.layout_cell(
        self.kcl.convert_cell_to_static(self.cell_index())
    )
    assert kdb_cell is not None
    kdb_cell.name = self.qname()
    ci_ = kdb_cell.cell_index()
    old_kdb_cell = self._base.kdb_cell
    kdb_cell.copy_meta_info(lib_cell.kdb_cell)
    self.get_meta_data()

    if recursive:
        for ci in self.called_cells():
            kc = self.kcl[ci]
            if kc.is_library_cell():
                kc.convert_to_static(recursive=recursive)

    self._base.kdb_cell = kdb_cell
    for ci in old_kdb_cell.caller_cells():
        c = self.kcl.layout_cell(ci)
        assert c is not None
        it = kdb.RecursiveInstanceIterator(self.kcl.layout, c)
        it.targets = [old_kdb_cell.cell_index()]
        it.max_depth = 0
        insts = [instit.current_inst_element().inst() for instit in it.each()]
        locked = c.locked
        c.locked = False
        for inst in insts:
            ca = inst.cell_inst
            ca.cell_index = ci_
            c.replace(inst, ca)
        c.locked = locked

    self.kcl.layout.delete_cell(old_kdb_cell.cell_index())

dcreate_inst

dcreate_inst(
    cell: ProtoTKCell[Any] | int,
    trans: DTrans | DVector | DCplxTrans | None = None,
    *,
    a: DVector | None = None,
    b: DVector | None = None,
    na: int = 1,
    nb: int = 1,
    libcell_as_static: bool = False,
    static_name_separator: str = "__",
) -> DInstance

Add an instance of another KCell.

Parameters:

Name Type Description Default
cell ProtoTKCell[Any] | int

The cell to be added

required
trans DTrans | DVector | DCplxTrans | None

The integer transformation applied to the reference

None
a DVector | None

Vector for the array. Needs to be in positive X-direction. Usually this is only a Vector in x-direction. Some foundries won't allow other Vectors.

None
b DVector | None

Vector for the array. Needs to be in positive Y-direction. Usually this is only a Vector in x-direction. Some foundries won't allow other Vectors.

None
na int

Number of elements in direction of a

1
nb int

Number of elements in direction of b

1
libcell_as_static bool

If the cell is a Library cell (different KCLayout object), convert it to a static cell. This can cause name collisions that are automatically resolved by appending $1[..n] on the newly created cell.

False
static_name_separator str

Stringt to separate the KCLayout name from the cell name when converting library cells (other KCLayout object than the one of this KCell) to static cells (copy them into this KCell's KCLayout).

'__'

Returns:

Type Description
DInstance

The created instance

Source code in kfactory/kcell.py
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
def dcreate_inst(
    self,
    cell: ProtoTKCell[Any] | int,
    trans: kdb.DTrans | kdb.DVector | kdb.DCplxTrans | None = None,
    *,
    a: kdb.DVector | None = None,
    b: kdb.DVector | None = None,
    na: int = 1,
    nb: int = 1,
    libcell_as_static: bool = False,
    static_name_separator: str = "__",
) -> DInstance:
    """Add an instance of another KCell.

    Args:
        cell: The cell to be added
        trans: The integer transformation applied to the reference
        a: Vector for the array.
            Needs to be in positive X-direction. Usually this is only a
            Vector in x-direction. Some foundries won't allow other Vectors.
        b: Vector for the array.
            Needs to be in positive Y-direction. Usually this is only a
            Vector in x-direction. Some foundries won't allow other Vectors.
        na: Number of elements in direction of `a`
        nb: Number of elements in direction of `b`
        libcell_as_static: If the cell is a Library cell
            (different KCLayout object), convert it to a static cell. This can cause
            name collisions that are automatically resolved by appending $1[..n] on
            the newly created cell.
        static_name_separator: Stringt to separate the KCLayout name from the cell
            name when converting library cells (other KCLayout object than the one
            of this KCell) to static cells (copy them into this KCell's KCLayout).

    Returns:
        The created instance
    """
    if trans is None:
        trans = kdb.DTrans()
    if isinstance(cell, int):
        ci = cell
    else:
        ci = self._get_ci(cell, libcell_as_static, static_name_separator)

    if a is None:
        inst = self._base.kdb_cell.insert(kdb.DCellInstArray(ci, trans))
    else:
        if b is None:
            b = kdb.DVector()
        inst = self._base.kdb_cell.insert(
            kdb.DCellInstArray(ci, trans, a, b, na, nb)
        )
    return DInstance(kcl=self.kcl, instance=inst)

delete

delete() -> None

Delete the cell.

Source code in kfactory/kcell.py
891
892
893
894
895
def delete(self) -> None:
    """Delete the cell."""
    ci = self.cell_index()
    self._base.kdb_cell.locked = False
    self.kcl.delete_cell(ci)

draw_ports

draw_ports() -> None

Draw all the ports on their respective layer.

Source code in kfactory/kcell.py
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
def draw_ports(self) -> None:
    """Draw all the ports on their respective layer."""
    locked = self._base.kdb_cell.locked
    self._base.kdb_cell.locked = False
    polys: dict[int, kdb.Region] = {}

    for port in Ports(kcl=self.kcl, bases=self.ports.bases):
        w = port.width

        if w in polys:
            poly = polys[w]
        else:
            poly = kdb.Region()
            poly.insert(
                kdb.Polygon(
                    [
                        kdb.Point(0, int(-w // 2)),
                        kdb.Point(0, int(w // 2)),
                        kdb.Point(int(w // 2), 0),
                    ]
                )
            )
            if w > 20:  # noqa: PLR2004
                poly -= kdb.Region(
                    kdb.Polygon(
                        [
                            kdb.Point(int(w // 20), 0),
                            kdb.Point(
                                int(w // 20), int(-w // 2 + int(w * 2.5 // 20))
                            ),
                            kdb.Point(int(w // 2 - int(w * 1.41 / 20)), 0),
                        ]
                    )
                )
        polys[w] = poly
        if port.base.trans:
            self.shapes(port.layer).insert(poly.transformed(port.trans))
            self.shapes(port.layer).insert(kdb.Text(port.name or "", port.trans))
        else:
            self.shapes(port.layer).insert(poly, port.dcplx_trans)
            self.shapes(port.layer).insert(kdb.Text(port.name or "", port.trans))
    self._base.kdb_cell.locked = locked

dup

dup(new_name: str | None = None) -> Self

Copy the full cell.

Sets _locked to False

Returns:

Name Type Description
cell Self

Exact copy of the current cell. The name will have $1 as duplicate names are not allowed

Source code in kfactory/kcell.py
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
def dup(self, new_name: str | None = None) -> Self:
    """Copy the full cell.

    Sets `_locked` to `False`

    Returns:
        cell: Exact copy of the current cell.
            The name will have `$1` as duplicate names are not allowed
    """
    kdb_copy = self._kdb_copy()
    if new_name:
        if new_name == self.name:
            if config.debug_names:
                raise ValueError(
                    "When duplicating a Cell, avoid giving the duplicate the same "
                    "name, as this can cause naming conflicts and may render the "
                    "GDS/OASIS file unwritable. If you're using a @cell function, "
                    "ensure that the function has a different name than the one "
                    "being called."
                )
            logger.error(
                "When duplicating a Cell, avoid giving the duplicate the same "
                "name, as this can cause naming conflicts and may render the "
                "GDS/OASIS file unwritable. If you're using a @cell function, "
                "ensure that the function has a different name than the one being "
                "called."
            )
        kdb_copy.name = new_name

    c = self.__class__(kcl=self.kcl, kdb_cell=kdb_copy)
    c.ports = self.ports.copy()

    if self.pins:
        port_mapping = {id(p): i for i, p in enumerate(c.ports)}
        c._base.pins = [
            BasePin(
                name=p.name,
                kcl=self.kcl,
                ports=[c.base.ports[port_mapping[id(port)]] for port in p.ports],
                pin_type=p.pin_type,
                info=p.info,
            )
            for p in self._base.pins
        ]

    c._base.settings = self.settings.model_copy()
    c._base.info = self.info.model_copy()
    c._base.vinsts = self._base.vinsts.copy()

    return c

each_inst

each_inst() -> Iterator[Instance]

Iterates over all child instances (which may actually be instance arrays).

Source code in kfactory/kcell.py
1467
1468
1469
1470
1471
def each_inst(self) -> Iterator[Instance]:
    """Iterates over all child instances (which may actually be instance arrays)."""
    yield from (
        Instance(self.kcl, inst) for inst in self._base.kdb_cell.each_inst()
    )

each_overlapping_inst

each_overlapping_inst(b: Box | DBox) -> Iterator[Instance]

Gets the instances overlapping the given rectangle.

Source code in kfactory/kcell.py
1473
1474
1475
1476
1477
1478
def each_overlapping_inst(self, b: kdb.Box | kdb.DBox) -> Iterator[Instance]:
    """Gets the instances overlapping the given rectangle."""
    yield from (
        Instance(self.kcl, inst)
        for inst in self._base.kdb_cell.each_overlapping_inst(b)
    )

each_touching_inst

each_touching_inst(b: Box | DBox) -> Iterator[Instance]

Gets the instances overlapping the given rectangle.

Source code in kfactory/kcell.py
1480
1481
1482
1483
1484
1485
def each_touching_inst(self, b: kdb.Box | kdb.DBox) -> Iterator[Instance]:
    """Gets the instances overlapping the given rectangle."""
    yield from (
        Instance(self.kcl, inst)
        for inst in self._base.kdb_cell.each_touching_inst(b)
    )

flatten

flatten(merge: bool = True) -> None

Flatten the cell.

Parameters:

Name Type Description Default
merge bool

Merge the shapes on all layers.

True
Source code in kfactory/kcell.py
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
def flatten(self, merge: bool = True) -> None:
    """Flatten the cell.

    Args:
        merge: Merge the shapes on all layers.
    """
    if self.locked:
        raise LockedError(self)
    for vinst in self._base.vinsts:
        vinst.insert_into_flat(self)
    self._base.vinsts = VInstances()
    self._base.kdb_cell.flatten(False)

    if merge:
        for layer in self.kcl.layout.layer_indexes():
            reg = kdb.Region(self.shapes(layer))
            reg = reg.merge()
            texts = kdb.Texts(self.shapes(layer))
            self.kdb_cell.clear(layer)
            self.shapes(layer).insert(reg)
            self.shapes(layer).insert(texts)

get_meta_data

get_meta_data(
    meta_format: Literal["v1", "v2", "v3"] | None = None,
) -> None

Read metadata from the KLayout Layout object.

Source code in kfactory/kcell.py
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
def get_meta_data(
    self,
    meta_format: Literal["v1", "v2", "v3"] | None = None,
) -> None:
    """Read metadata from the KLayout Layout object."""
    if meta_format is None:
        meta_format = config.meta_format
    port_dict: dict[str, Any] = {}
    pin_dict: dict[str, Any] = {}
    ports: dict[str, BasePort] = {}
    settings: dict[str, MetaData] = {}
    settings_units: dict[str, str] = {}
    from .layout import kcls

    match meta_format:
        case "v3":
            self.ports.clear()
            meta_iter = (
                kcls[self.library().name()][
                    self.library_cell_index()
                ].each_meta_info()
                if self.is_library_cell()
                else self.each_meta_info()
            )
            for meta in meta_iter:
                if meta.name.startswith("kfactory:ports"):
                    i = meta.name.removeprefix("kfactory:ports:")
                    port_dict[i] = meta.value
                elif meta.name.startswith("kfactory:pins"):
                    i = meta.name.removeprefix("kfactory:pins:")
                    pin_dict[i] = meta.value
                elif meta.name.startswith("kfactory:info"):
                    self._base.info = Info(**meta.value)
                elif meta.name.startswith("kfactory:settings_units"):
                    self._base.settings_units = KCellSettingsUnits(**meta.value)
                elif meta.name.startswith("kfactory:settings"):
                    self._base.settings = KCellSettings(**meta.value)
                elif meta.name == "kfactory:function_name":
                    self._base.function_name = meta.value
                elif meta.name == "kfactory:basename":
                    self._base.basename = meta.value

            if not self.is_library_cell():
                for index in sorted(port_dict.keys()):
                    v = port_dict[index]
                    trans_: kdb.Trans | None = v.get("trans")
                    if trans_ is not None:
                        ports[index] = self.create_port(
                            name=v.get("name"),
                            trans=trans_,
                            cross_section=self.kcl.get_symmetrical_cross_section(
                                v["cross_section"]
                            ),
                            port_type=v["port_type"],
                            info=v["info"],
                        )
                    else:
                        ports[index] = self.create_port(
                            name=v.get("name"),
                            dcplx_trans=v["dcplx_trans"],
                            cross_section=self.kcl.get_symmetrical_cross_section(
                                v["cross_section"]
                            ),
                            port_type=v["port_type"],
                            info=v["info"],
                        )
                for index in sorted(pin_dict.keys()):
                    v = pin_dict[index]
                    self.create_pin(
                        name=v.get("name"),
                        ports=[ports[port_index] for port_index in v["ports"]],  # type: ignore[misc]
                        pin_type=v["pin_type"],
                        info=v["info"],
                    )
            else:
                lib_name = self.library().name()
                for index in sorted(port_dict.keys()):
                    v = port_dict[index]
                    trans_ = v.get("trans")
                    lib_kcl = kcls[lib_name]
                    cs = self.kcl.get_symmetrical_cross_section(
                        lib_kcl.get_symmetrical_cross_section(
                            v["cross_section"]
                        ).to_dtype(lib_kcl)
                    )

                    if trans_ is not None:
                        ports[index] = self.create_port(
                            name=v.get("name"),
                            trans=trans_.to_dtype(lib_kcl.dbu).to_itype(
                                self.kcl.dbu
                            ),
                            cross_section=cs,
                            port_type=v["port_type"],
                        )
                    else:
                        ports[index] = self.create_port(
                            name=v.get("name"),
                            dcplx_trans=v["dcplx_trans"],
                            cross_section=cs,
                            port_type=v["port_type"],
                        )
                for index in sorted(pin_dict):
                    v = pin_dict[index]
                    self.create_pin(
                        name=v.get("name"),
                        ports=[ports[str(port_index)] for port_index in v["ports"]],  # type: ignore[misc]
                        pin_type=v["pin_type"],
                        info=v["info"],
                    )

        case "v2":
            for meta in self.each_meta_info():
                if meta.name.startswith("kfactory:ports"):
                    i, type_ = meta.name.removeprefix("kfactory:ports:").split(
                        ":", 1
                    )
                    if i not in port_dict:
                        port_dict[i] = {}
                    if not type_.startswith("info"):
                        port_dict[i][type_] = meta.value
                    else:
                        if "info" not in port_dict[i]:
                            port_dict[i]["info"] = {}
                        port_dict[i]["info"][type_.removeprefix("info:")] = (
                            meta.value
                        )
                elif meta.name.startswith("kfactory:info"):
                    setattr(
                        self.info,
                        meta.name.removeprefix("kfactory:info:"),
                        meta.value,
                    )
                elif meta.name.startswith("kfactory:settings_units"):
                    settings_units[
                        meta.name.removeprefix("kfactory:settings_units:")
                    ] = meta.value
                elif meta.name.startswith("kfactory:settings"):
                    settings[meta.name.removeprefix("kfactory:settings:")] = (
                        meta.value
                    )

                elif meta.name == "kfactory:function_name":
                    self.function_name = meta.value

                elif meta.name == "kfactory:basename":
                    self.basename = meta.value

            self.settings = KCellSettings(**settings)
            self.settings_units = KCellSettingsUnits(**settings_units)

            self.ports.clear()
            for index in sorted(port_dict.keys()):
                d = port_dict[index]
                name = d.get("name", None)
                port_type = d["port_type"]
                layer_info = d["layer"]
                width = d["width"]
                trans = d.get("trans", None)
                dcplx_trans = d.get("dcplx_trans", None)
                port = Port(
                    name=name,
                    width=width,
                    layer_info=layer_info,
                    trans=kdb.Trans.R0,
                    kcl=self.kcl,
                    port_type=port_type,
                    info=d.get("info", {}),
                )
                if trans:
                    port.trans = trans
                elif dcplx_trans:
                    port.dcplx_trans = dcplx_trans

                self.add_port(port=port, keep_mirror=True)
        case "v1":
            for meta in self.each_meta_info():
                if meta.name.startswith("kfactory:ports"):
                    i, type_ = meta.name.removeprefix("kfactory:ports:").split(
                        ":", 1
                    )
                    if i not in port_dict:
                        port_dict[i] = {}
                    if not type_.startswith("info"):
                        port_dict[i][type_] = meta.value
                    else:
                        if "info" not in port_dict[i]:
                            port_dict[i]["info"] = {}
                        port_dict[i]["info"][type_.removeprefix("info:")] = (
                            meta.value
                        )
                elif meta.name.startswith("kfactory:info"):
                    setattr(
                        self.info,
                        meta.name.removeprefix("kfactory:info:"),
                        meta.value,
                    )
                elif meta.name.startswith("kfactory:settings_units"):
                    settings_units[
                        meta.name.removeprefix("kfactory:settings_units:")
                    ] = meta.value
                elif meta.name.startswith("kfactory:settings"):
                    settings[meta.name.removeprefix("kfactory:settings:")] = (
                        meta.value
                    )

                elif meta.name == "kfactory:function_name":
                    self.function_name = meta.value

                elif meta.name == "kfactory:basename":
                    self.basename = meta.value

            self.settings = KCellSettings(**settings)
            self.settings_units = KCellSettingsUnits(**settings_units)

            self.ports.clear()
            for index in sorted(port_dict.keys()):
                d = port_dict[index]
                name = d.get("name", None)
                port_type = d["port_type"]
                layer = d["layer"]
                width = d["width"]
                trans = d.get("trans", None)
                dcplx_trans = d.get("dcplx_trans", None)
                port = Port(
                    name=name,
                    width=width,
                    layer_info=layer,
                    trans=kdb.Trans.R0,
                    kcl=self.kcl,
                    port_type=port_type,
                    info=d.get("info", {}),
                )
                if trans:
                    port.trans = kdb.Trans.from_s(trans)
                elif dcplx_trans:
                    port.dcplx_trans = kdb.DCplxTrans.from_s(dcplx_trans)

                self.add_port(port=port, keep_mirror=True)

icreate_inst

icreate_inst(
    cell: ProtoTKCell[Any] | int,
    trans: Trans | Vector | ICplxTrans | None = None,
    *,
    a: Vector | None = None,
    b: Vector | None = None,
    na: int = 1,
    nb: int = 1,
    libcell_as_static: bool = False,
    static_name_separator: str = "__",
) -> Instance

Add an instance of another KCell.

Parameters:

Name Type Description Default
cell ProtoTKCell[Any] | int

The cell to be added

required
trans Trans | Vector | ICplxTrans | None

The integer transformation applied to the reference

None
a Vector | None

Vector for the array. Needs to be in positive X-direction. Usually this is only a Vector in x-direction. Some foundries won't allow other Vectors.

None
b Vector | None

Vector for the array. Needs to be in positive Y-direction. Usually this is only a Vector in x-direction. Some foundries won't allow other Vectors.

None
na int

Number of elements in direction of a

1
nb int

Number of elements in direction of b

1
libcell_as_static bool

If the cell is a Library cell (different KCLayout object), convert it to a static cell. This can cause name collisions that are automatically resolved by appending $1[..n] on the newly created cell.

False
static_name_separator str

Stringt to separate the KCLayout name from the cell name when converting library cells (other KCLayout object than the one of this KCell) to static cells (copy them into this KCell's KCLayout).

'__'

Returns:

Type Description
Instance

The created instance

Source code in kfactory/kcell.py
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
def icreate_inst(
    self,
    cell: ProtoTKCell[Any] | int,
    trans: kdb.Trans | kdb.Vector | kdb.ICplxTrans | None = None,
    *,
    a: kdb.Vector | None = None,
    b: kdb.Vector | None = None,
    na: int = 1,
    nb: int = 1,
    libcell_as_static: bool = False,
    static_name_separator: str = "__",
) -> Instance:
    """Add an instance of another KCell.

    Args:
        cell: The cell to be added
        trans: The integer transformation applied to the reference
        a: Vector for the array.
            Needs to be in positive X-direction. Usually this is only a
            Vector in x-direction. Some foundries won't allow other Vectors.
        b: Vector for the array.
            Needs to be in positive Y-direction. Usually this is only a
            Vector in x-direction. Some foundries won't allow other Vectors.
        na: Number of elements in direction of `a`
        nb: Number of elements in direction of `b`
        libcell_as_static: If the cell is a Library cell
            (different KCLayout object), convert it to a static cell. This can cause
            name collisions that are automatically resolved by appending $1[..n] on
            the newly created cell.
        static_name_separator: Stringt to separate the KCLayout name from the cell
            name when converting library cells (other KCLayout object than the one
            of this KCell) to static cells (copy them into this KCell's KCLayout).

    Returns:
        The created instance
    """
    if trans is None:
        trans = kdb.Trans()
    if isinstance(cell, int):
        ci = cell
    else:
        ci = self._get_ci(cell, libcell_as_static, static_name_separator)

    if a is None:
        inst = self._base.kdb_cell.insert(kdb.CellInstArray(ci, trans))
    else:
        if b is None:
            b = kdb.Vector()
        inst = self._base.kdb_cell.insert(
            kdb.CellInstArray(ci, trans, a, b, na, nb)
        )
    return Instance(kcl=self.kcl, instance=inst)

insert

insert(
    inst: Instance | CellInstArray | DCellInstArray,
) -> Instance
insert(
    inst: CellInstArray | DCellInstArray, property_id: int
) -> Instance
insert(
    inst: Instance | CellInstArray | DCellInstArray,
    property_id: int | None = None,
) -> Instance

Inserts a cell instance given by another reference.

Source code in kfactory/kcell.py
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
def insert(
    self,
    inst: Instance | kdb.CellInstArray | kdb.DCellInstArray,
    property_id: int | None = None,
) -> Instance:
    """Inserts a cell instance given by another reference."""
    if self.locked:
        raise LockedError(self)
    if isinstance(inst, Instance):
        return Instance(self.kcl, self._base.kdb_cell.insert(inst.instance))
    if not property_id:
        return Instance(self.kcl, self._base.kdb_cell.insert(inst))
    assert isinstance(inst, kdb.CellInstArray | kdb.DCellInstArray)
    return Instance(self.kcl, self._base.kdb_cell.insert(inst, property_id))

insert_vinsts

insert_vinsts(recursive: bool = True) -> None

Insert all virtual instances and create Instances of real KCells.

Source code in kfactory/kcell.py
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
def insert_vinsts(self, recursive: bool = True) -> None:
    """Insert all virtual instances and create Instances of real KCells."""
    if not self._base.kdb_cell._destroyed():
        for vi in self._base.vinsts:
            vi.insert_into(self)
        self._base.vinsts.clear()
        called_cell_indexes = self._base.kdb_cell.called_cells()
        for c in sorted(
            {
                self.kcl[ci]
                for ci in called_cell_indexes
                if not self.kcl[ci].kdb_cell._destroyed()
            }
            & self.kcl.tkcells.keys(),
            key=lambda c: c.hierarchy_levels(),
        ):
            for vi in c._base.vinsts:
                vi.insert_into(c)
            c._base.vinsts.clear()

l2n

l2n(
    port_types: Iterable[str] = ("optical",),
) -> kdb.LayoutToNetlist

Generate a LayoutToNetlist object from the port types.

Parameters:

Name Type Description Default
port_types Iterable[str]

The port types to consider for the netlist extraction.

('optical',)

Returns: LayoutToNetlist extracted from instance and cell port positions.

Source code in kfactory/kcell.py
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
def l2n(self, port_types: Iterable[str] = ("optical",)) -> kdb.LayoutToNetlist:
    """Generate a LayoutToNetlist object from the port types.

    Args:
        port_types: The port types to consider for the netlist extraction.
    Returns:
        LayoutToNetlist extracted from instance and cell port positions.
    """
    logger.warning(
        "l2n is deprecated and will be removed in 2.0. Please use `l2n_ports`"
        " instead."
    )
    return self.l2n_ports(port_types=port_types)

l2n_elec

l2n_elec(
    mark_port_types: Iterable[str] = (
        "electrical",
        "RF",
        "DC",
    ),
    connectivity: Sequence[
        tuple[LayerInfo]
        | tuple[LayerInfo, LayerInfo]
        | tuple[LayerInfo, LayerInfo, LayerInfo],
    ]
    | None = None,
    port_mapping: dict[str, dict[str | None, str]]
    | None = None,
) -> kdb.LayoutToNetlist

Generate a LayoutToNetlist object from the port types.

Uses electrical connectivity for extraction.

Parameters:

Name Type Description Default
mark_port_types Iterable[str]

The port types to consider for the netlist extraction.

('electrical', 'RF', 'DC')
connectivity Sequence[tuple[LayerInfo] | tuple[LayerInfo, LayerInfo] | tuple[LayerInfo, LayerInfo, LayerInfo],] | None

Define connectivity between layers. These can be single layers (just consider this layer as metal), two layers (two metals which touch each other), or three layers (two metals with a via)

None
port_mapping dict[str, dict[str | None, str]] | None

Remap ports of cells to others. This allows to define equivalent ports in the lvs. E.g. {"cell_A": {"o3": "o1", "o2"}} will remap "o2" and "o3" to "o1". Making the three ports the same one for LVS.

None

Returns: LayoutToNetlist extracted from electrical connectivity.

Source code in kfactory/kcell.py
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
def l2n_elec(
    self,
    mark_port_types: Iterable[str] = ("electrical", "RF", "DC"),
    connectivity: Sequence[
        tuple[kdb.LayerInfo]
        | tuple[kdb.LayerInfo, kdb.LayerInfo]
        | tuple[kdb.LayerInfo, kdb.LayerInfo, kdb.LayerInfo],
    ]
    | None = None,
    port_mapping: dict[str, dict[str | None, str]] | None = None,
) -> kdb.LayoutToNetlist:
    """Generate a LayoutToNetlist object from the port types.

    Uses electrical connectivity for extraction.

    Args:
        mark_port_types: The port types to consider for the netlist extraction.
        connectivity: Define connectivity between layers. These can be single
            layers (just consider this layer as metal), two layers (two metals
            which touch each other), or three layers (two metals with a via)
        port_mapping: Remap ports of cells to others. This allows to define
            equivalent ports in the lvs. E.g. `{"cell_A": {"o3": "o1", "o2"}}`
            will remap "o2" and "o3" to "o1". Making the three ports the same
            one for LVS.
    Returns:
        LayoutToNetlist extracted from electrical connectivity.
    """
    connectivity = connectivity or self.kcl.connectivity
    ly_elec = self.kcl.layout.dup()

    port_mapping = port_mapping or {}
    c_elec: kdb.Cell = ly_elec.cell(self.name)

    for ci in [c_elec.cell_index(), *c_elec.called_cells()]:
        c_ = self.kcl[ci]
        c = ly_elec.cell(c_.name)
        assert c_.name == c.name
        c.locked = False
        mapping = port_mapping.get(
            c_.name,
            port_mapping.get(c_.factory_name, {}) if c_.has_factory_name() else {},
        )
        for port in c_.ports:
            port_name = mapping.get(port.name, port.name)
            if (
                port_name == port.name
                and port.port_type in mark_port_types
                and port.name is not None
            ):
                c.shapes(port.layer_info).insert(
                    kdb.Text(string=port.name, trans=port.trans)
                )

    l2n: kdb.LayoutToNetlist = kdb.LayoutToNetlist(
        kdb.RecursiveShapeIterator(
            ly_elec,
            ly_elec.cell(self.name),
            [],
        )
    )

    connectivity = connectivity or self.kcl.connectivity

    layers: dict[int, kdb.Region] = {}

    layer_infos = {
        ly_elec.get_info(ly_elec.layer(info))
        for layer_set in connectivity
        for info in layer_set
    }
    for info in layer_infos:
        l_ = l2n.make_layer(ly_elec.layer(info), info.name)
        layers[ly_elec.layer(info)] = l_
        l2n.connect(l_)
    for conn in connectivity:
        old_layer = layers[ly_elec.layer(conn[0])]

        for layer in conn[1:]:
            li = layers[ly_elec.layer(layer)]
            l2n.connect(old_layer, li)
            old_layer = li
    l2n.extract_netlist()
    l2n.check_extraction_errors()

    return l2n

l2n_ports

l2n_ports(
    port_types: Iterable[str] = ("optical",),
    exclude_purposes: list[str] | None = None,
    ignore_unnamed: bool = False,
    allow_width_mismatch: bool = False,
) -> kdb.LayoutToNetlist

Generate a LayoutToNetlist object from the port types.

Uses kfactory ports as a basis for extraction.

Parameters:

Name Type Description Default
port_types Iterable[str]

The port types to consider for the netlist extraction.

('optical',)
exclude_purposes list[str] | None

List of purposes, if an instance has that purpose, it will be ignored.

None
ignore_unnamed bool

Ignore any instance without .name set.

False

Returns: LayoutToNetlist extracted from instance and cell port positions.

Source code in kfactory/kcell.py
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
def l2n_ports(
    self,
    port_types: Iterable[str] = ("optical",),
    exclude_purposes: list[str] | None = None,
    ignore_unnamed: bool = False,
    allow_width_mismatch: bool = False,
) -> kdb.LayoutToNetlist:
    """Generate a LayoutToNetlist object from the port types.

    Uses kfactory ports as a basis for extraction.

    Args:
        port_types: The port types to consider for the netlist extraction.
        exclude_purposes: List of purposes, if an instance has that purpose, it will
            be ignored.
        ignore_unnamed: Ignore any instance without `.name` set.
    Returns:
        LayoutToNetlist extracted from instance and cell port positions.
    """
    l2n = kdb.LayoutToNetlist(self.name, self.kcl.dbu)
    l2n.extract_netlist()
    il = l2n.internal_layout()
    il.assign(self.kcl.layout)

    called_kcells = [self.kcl[ci] for ci in self.called_cells()]
    called_kcells.sort(key=lambda c: c.hierarchy_levels())

    for c in called_kcells:
        c.circuit(
            l2n,
            port_types=port_types,
            exclude_purposes=exclude_purposes,
            ignore_unnamed=ignore_unnamed,
            allow_width_mismatch=allow_width_mismatch,
        )
    self.circuit(
        l2n,
        port_types=port_types,
        exclude_purposes=exclude_purposes,
        ignore_unnamed=ignore_unnamed,
        allow_width_mismatch=allow_width_mismatch,
    )
    return l2n

plot

plot(
    lyrdb: Path | str | None = None,
    display_type: Literal["image", "widget"] | None = None,
) -> None

Display cell.

Parameters:

Name Type Description Default
lyrdb Path | str | None

Path to the lyrdb file.

None
display_type Literal['image', 'widget'] | None

Type of display. Options are "widget" or "image".

None
Source code in kfactory/kcell.py
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
def plot(
    self,
    lyrdb: Path | str | None = None,
    display_type: Literal["image", "widget"] | None = None,
) -> None:
    """Display cell.

    Args:
        lyrdb: Path to the lyrdb file.
        display_type: Type of display. Options are "widget" or "image".

    """
    from .widgets.interactive import display_kcell

    display_kcell(self, lyrdb=lyrdb, display_type=display_type)

read

read(
    filename: str | Path,
    options: LoadLayoutOptions | None = None,
    register_cells: bool = False,
    test_merge: bool = True,
    update_kcl_meta_data: Literal[
        "overwrite", "skip", "drop"
    ] = "drop",
    meta_format: Literal["v1", "v2", "v3"] | None = None,
) -> list[int]

Read a GDS file into the existing KCell.

Any existing meta info (KCell.info and KCell.settings) will be overwritten if a KCell already exists. Instead of overwriting the cells, they can also be loaded into new cells by using the corresponding cell_conflict_resolution.

Layout meta infos are ignored from the loaded layout.

Parameters:

Name Type Description Default
filename str | Path

Path of the GDS file.

required
options LoadLayoutOptions | None

KLayout options to load from the GDS. Can determine how merge conflicts are handled for example. See https://www.klayout.de/doc-qt5/code/class_LoadLayoutOptions.html

None
register_cells bool

If True create KCells for all cells in the GDS.

False
test_merge bool

Check the layouts first whether they are compatible (no differences).

True
update_kcl_meta_data Literal['overwrite', 'skip', 'drop']

How to treat loaded KCLayout info. overwrite: overwrite existing info entries skip: keep existing info values drop: don't add any new info

'drop'
meta_format Literal['v1', 'v2', 'v3'] | None

How to read KCell metainfo from the gds. v1 had stored port transformations as strings, never versions have them stored and loaded in their native KLayout formats.

None
Source code in kfactory/kcell.py
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
def read(
    self,
    filename: str | Path,
    options: kdb.LoadLayoutOptions | None = None,
    register_cells: bool = False,
    test_merge: bool = True,
    update_kcl_meta_data: Literal["overwrite", "skip", "drop"] = "drop",
    meta_format: Literal["v1", "v2", "v3"] | None = None,
) -> list[int]:
    """Read a GDS file into the existing KCell.

    Any existing meta info (KCell.info and KCell.settings) will be overwritten if
    a KCell already exists. Instead of overwriting the cells, they can also be
    loaded into new cells by using the corresponding cell_conflict_resolution.

    Layout meta infos are ignored from the loaded layout.

    Args:
        filename: Path of the GDS file.
        options: KLayout options to load from the GDS. Can determine how merge
            conflicts are handled for example. See
            https://www.klayout.de/doc-qt5/code/class_LoadLayoutOptions.html
        register_cells: If `True` create KCells for all cells in the GDS.
        test_merge: Check the layouts first whether they are compatible
            (no differences).
        update_kcl_meta_data: How to treat loaded KCLayout info.
            overwrite: overwrite existing info entries
            skip: keep existing info values
            drop: don't add any new info
        meta_format: How to read KCell metainfo from the gds. `v1` had stored port
            transformations as strings, never versions have them stored and loaded
            in their native KLayout formats.
    """
    # see: wait for KLayout update https://github.com/KLayout/klayout/issues/1609
    logger.critical(
        "KLayout <=0.28.15 (last update 2024-02-02) cannot read LayoutMetaInfo on"
        " 'Cell.read'. kfactory uses these extensively for ports, info, and "
        "settings. Therefore proceed at your own risk."
    )
    if meta_format is None:
        meta_format = config.meta_format
    if options is None:
        options = load_layout_options()
    fn = str(Path(filename).expanduser().resolve())
    if test_merge and (
        options.cell_conflict_resolution
        != kdb.LoadLayoutOptions.CellConflictResolution.RenameCell
    ):
        self.kcl.set_meta_data()
        for kcell in self.kcl.kcells.values():
            kcell.set_meta_data()
        layout_b = kdb.Layout()
        layout_b.read(fn, options)
        layout_a = self.kcl.layout.dup()
        layout_a.delete_cell(layout_a.cell(self.name).cell_index())
        diff = MergeDiff(
            layout_a=layout_a,
            layout_b=layout_b,
            name_a=self.name,
            name_b=Path(filename).stem,
        )
        diff.compare()
        if diff.dbu_differs:
            raise MergeError("Layouts' DBU differ. Check the log for more info.")
        if diff.diff_xor.cells() > 0 or diff.layout_meta_diff:
            diff_kcl = KCLayout(self.name + "_XOR")
            diff_kcl.layout.assign(diff.diff_xor)
            show(diff_kcl)

            err_msg = (
                f"Layout {self.name} cannot merge with layout "
                f"{Path(filename).stem} safely. See the error messages "
                f"or check with KLayout."
            )

            if diff.layout_meta_diff:
                yaml = ruamel.yaml.YAML(typ=["rt", "string"])
                err_msg += (
                    "\nLayout Meta Diff:\n```\n"
                    + yaml.dumps(dict(diff.layout_meta_diff))
                    + "\n```"
                )
            if diff.cells_meta_diff:
                yaml = ruamel.yaml.YAML(typ=["rt", "string"])
                err_msg += (
                    "\nLayout Meta Diff:\n```\n"
                    + yaml.dumps(dict(diff.cells_meta_diff))
                    + "\n```"
                )

            raise MergeError(err_msg)

    cell_ids = self._base.kdb_cell.read(fn, options)
    info, settings = self.kcl.get_meta_data()

    match update_kcl_meta_data:
        case "overwrite":
            for k, v in info.items():
                self.kcl.info[k] = v
        case "skip":
            info_ = self.info.model_dump()

            info.update(info_)
            self.kcl.info = Info(**info)
        case "drop":
            ...
    meta_format = settings.get("meta_format") or meta_format

    if register_cells:
        new_cis = set(cell_ids)

        for c in new_cis:
            kc = self.kcl[c]
            kc.get_meta_data(meta_format=meta_format)
    else:
        cis = self.kcl.tkcells.keys()
        new_cis = set(cell_ids)

        for c in new_cis & cis:
            kc = self.kcl[c]
            kc.get_meta_data(meta_format=meta_format)

    self.get_meta_data(meta_format=meta_format)

    return cell_ids

set_meta_data

set_meta_data() -> None

Set metadata of the Cell.

Currently, ports, settings and info will be set.

Source code in kfactory/kcell.py
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
def set_meta_data(self) -> None:
    """Set metadata of the Cell.

    Currently, ports, settings and info will be set.
    """
    self.clear_meta_info()
    if not self.is_library_cell():
        for i, port in enumerate(self.ports):
            if port.base.trans is not None:
                meta_info: dict[str, MetaData] = {
                    "name": port.name,
                    "cross_section": port.cross_section.name,
                    "trans": port.base.trans,
                    "port_type": port.port_type,
                    "info": port.info.model_dump(),
                }

                self.add_meta_info(
                    kdb.LayoutMetaInfo(f"kfactory:ports:{i}", meta_info, None, True)
                )
            else:
                meta_info = {
                    "name": port.name,
                    "cross_section": port.cross_section.name,
                    "dcplx_trans": port.dcplx_trans,
                    "port_type": port.port_type,
                    "info": port.info.model_dump(),
                }

                self.add_meta_info(
                    kdb.LayoutMetaInfo(f"kfactory:ports:{i}", meta_info, None, True)
                )
        for i, pin in enumerate(self.pins):
            meta_info = {
                "name": pin.name,
                "pin_type": pin.pin_type,
                "info": pin.info.model_dump(),
                "ports": [self.base.ports.index(port.base) for port in pin.ports],
            }
            self.add_meta_info(
                kdb.LayoutMetaInfo(f"kfactory:pins:{i}", meta_info, None, True)
            )
        settings = self.settings.model_dump()
        if settings:
            self.add_meta_info(
                kdb.LayoutMetaInfo("kfactory:settings", settings, None, True)
            )
        info = self.info.model_dump()
        if info:
            self.add_meta_info(
                kdb.LayoutMetaInfo("kfactory:info", info, None, True)
            )
        settings_units = self.settings_units.model_dump()
        if settings_units:
            self.add_meta_info(
                kdb.LayoutMetaInfo(
                    "kfactory:settings_units",
                    settings_units,
                    None,
                    True,
                )
            )

        if self.function_name is not None:
            self.add_meta_info(
                kdb.LayoutMetaInfo(
                    "kfactory:function_name", self.function_name, None, True
                )
            )

        if self.basename is not None:
            self.add_meta_info(
                kdb.LayoutMetaInfo("kfactory:basename", self.basename, None, True)
            )

show

show(
    lyrdb: ReportDatabase | Path | str | None = None,
    l2n: LayoutToNetlist | Path | str | None = None,
    keep_position: bool = True,
    save_options: SaveLayoutOptions | None = None,
    use_libraries: bool = True,
    library_save_options: SaveLayoutOptions | None = None,
    technology: str | None = None,
) -> None

Stream the gds to klive.

Will create a temporary file of the gds and load it in KLayout via klive

Source code in kfactory/kcell.py
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
def show(
    self,
    lyrdb: rdb.ReportDatabase | Path | str | None = None,
    l2n: kdb.LayoutToNetlist | Path | str | None = None,
    keep_position: bool = True,
    save_options: kdb.SaveLayoutOptions | None = None,
    use_libraries: bool = True,
    library_save_options: kdb.SaveLayoutOptions | None = None,
    technology: str | None = None,
) -> None:
    """Stream the gds to klive.

    Will create a temporary file of the gds and load it in KLayout via klive
    """
    if save_options is None:
        save_options = save_layout_options()
    if library_save_options is None:
        library_save_options = save_layout_options()
    show_f: ShowFunction = config.show_function or show

    kwargs: dict[str, Any] = {}
    if technology is not None:
        kwargs["technology"] = technology
    if l2n is not None:
        kwargs["l2n"] = l2n
    if lyrdb is not None:
        kwargs["lyrdb"] = lyrdb

    show_f(
        self,
        keep_position=keep_position,
        save_options=save_options,
        use_libraries=use_libraries,
        library_save_options=library_save_options,
        **kwargs,
    )

to_dtype

to_dtype() -> DKCell

Convert the kcell to a um kcell.

Source code in kfactory/kcell.py
827
828
829
def to_dtype(self) -> DKCell:
    """Convert the kcell to a um kcell."""
    return DKCell(base=self._base)

to_itype

to_itype() -> KCell

Convert the kcell to a dbu kcell.

Source code in kfactory/kcell.py
823
824
825
def to_itype(self) -> KCell:
    """Convert the kcell to a dbu kcell."""
    return KCell(base=self._base)

transform

transform(
    inst: Instance,
    trans: Trans | DTrans | ICplxTrans | DCplxTrans,
    /,
    *,
    transform_ports: bool = True,
) -> Instance
transform(
    trans: Trans | DTrans | ICplxTrans | DCplxTrans,
    /,
    *,
    transform_ports: bool = True,
) -> None
transform(
    inst_or_trans: Instance
    | Trans
    | DTrans
    | ICplxTrans
    | DCplxTrans,
    trans: Trans
    | DTrans
    | ICplxTrans
    | DCplxTrans
    | None = None,
    /,
    *,
    transform_ports: bool = True,
) -> Instance | None

Transforms the instance or cell with the transformation given.

Source code in kfactory/kcell.py
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
def transform(
    self,
    inst_or_trans: kdb.Instance
    | kdb.Trans
    | kdb.DTrans
    | kdb.ICplxTrans
    | kdb.DCplxTrans,
    trans: kdb.Trans | kdb.DTrans | kdb.ICplxTrans | kdb.DCplxTrans | None = None,
    /,
    *,
    transform_ports: bool = True,
) -> Instance | None:
    """Transforms the instance or cell with the transformation given."""
    if trans:
        return Instance(
            self.kcl,
            self._base.kdb_cell.transform(
                inst_or_trans,  # type: ignore[arg-type]
                trans,  # type: ignore[arg-type]
            ),
        )
    self._base.kdb_cell.transform(inst_or_trans)  # type:ignore[arg-type]
    if transform_ports:
        if isinstance(inst_or_trans, kdb.DTrans):
            inst_or_trans = kdb.DCplxTrans(inst_or_trans)
        elif isinstance(inst_or_trans, kdb.ICplxTrans):
            inst_or_trans = kdb.DCplxTrans(inst_or_trans, self.kcl.dbu)

        if isinstance(inst_or_trans, kdb.Trans):
            for port in self.ports:
                port.trans = inst_or_trans * port.trans
        else:
            for port in self.ports:
                port.dcplx_trans = inst_or_trans * port.dcplx_trans  # type: ignore[operator]
    return None

write

write(
    filename: str | Path,
    save_options: SaveLayoutOptions | None = None,
    convert_external_cells: bool = False,
    set_meta_data: bool = True,
    autoformat_from_file_extension: bool = True,
) -> None

Write a KCell to a GDS.

See KCLayout.write for more info.

Source code in kfactory/kcell.py
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
def write(
    self,
    filename: str | Path,
    save_options: kdb.SaveLayoutOptions | None = None,
    convert_external_cells: bool = False,
    set_meta_data: bool = True,
    autoformat_from_file_extension: bool = True,
) -> None:
    """Write a KCell to a GDS.

    See [KCLayout.write][kfactory.kcell.KCLayout.write] for more info.
    """
    if save_options is None:
        save_options = save_layout_options()
    self.insert_vinsts()
    match set_meta_data, convert_external_cells:
        case True, True:
            self.kcl.set_meta_data()
            for kcell in (self.kcl[ci] for ci in self.called_cells()):
                if not kcell._destroyed():
                    if kcell.is_library_cell():
                        kcell.convert_to_static(recursive=True)
                    kcell.set_meta_data()
            if self.is_library_cell():
                self.convert_to_static(recursive=True)
            self.set_meta_data()
        case True, False:
            self.kcl.set_meta_data()
            for kcell in (self.kcl[ci] for ci in self.called_cells()):
                if not kcell._destroyed():
                    kcell.set_meta_data()
            self.set_meta_data()
        case False, True:
            for kcell in (self.kcl[ci] for ci in self.called_cells()):
                if kcell.is_library_cell() and not kcell._destroyed():
                    kcell.convert_to_static(recursive=True)
            if self.is_library_cell():
                self.convert_to_static(recursive=True)
        case _:
            ...

    for kci in set(self._base.kdb_cell.called_cells()) & self.kcl.tkcells.keys():
        kc = self.kcl[kci]
        kc.insert_vinsts()

    filename = str(filename)
    if autoformat_from_file_extension:
        save_options.set_format_from_filename(filename)
    self._base.kdb_cell.write(filename, save_options)

write_bytes

write_bytes(
    save_options: SaveLayoutOptions | None = None,
    convert_external_cells: bool = False,
    set_meta_data: bool = True,
) -> bytes

Write a KCell to a binary format as oasis.

See KCLayout.write for more info.

Source code in kfactory/kcell.py
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
def write_bytes(
    self,
    save_options: kdb.SaveLayoutOptions | None = None,
    convert_external_cells: bool = False,
    set_meta_data: bool = True,
) -> bytes:
    """Write a KCell to a binary format as oasis.

    See [KCLayout.write][kfactory.kcell.KCLayout.write] for more info.
    """
    if save_options is None:
        save_options = save_layout_options()
    self.insert_vinsts()
    match set_meta_data, convert_external_cells:
        case True, True:
            self.kcl.set_meta_data()
            for kcell in (self.kcl[ci] for ci in self.called_cells()):
                if not kcell._destroyed():
                    if kcell.is_library_cell():
                        kcell.convert_to_static(recursive=True)
                    kcell.set_meta_data()
            if self.is_library_cell():
                self.convert_to_static(recursive=True)
            self.set_meta_data()
        case True, False:
            self.kcl.set_meta_data()
            for kcell in (self.kcl[ci] for ci in self.called_cells()):
                if not kcell._destroyed():
                    kcell.set_meta_data()
            self.set_meta_data()
        case False, True:
            for kcell in (self.kcl[ci] for ci in self.called_cells()):
                if kcell.is_library_cell() and not kcell._destroyed():
                    kcell.convert_to_static(recursive=True)
            if self.is_library_cell():
                self.convert_to_static(recursive=True)
        case _:
            ...

    for kci in set(self._base.kdb_cell.called_cells()) & self.kcl.tkcells.keys():
        kc = self.kcl[kci]
        kc.insert_vinsts()

    save_options.format = save_options.format or "OASIS"
    save_options.clear_cells()
    save_options.select_cell(self.cell_index())
    return self.kcl.layout.write_bytes(save_options)

Schema pydantic-model

Bases: Schematic

Deprecated alias for Schematic (will be removed in kfactory 2.0).

Fields:

Validators:

  • _validate_schematic
  • assign_backrefs
Source code in kfactory/schematic.py
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
class Schema(Schematic):
    """Deprecated alias for `Schematic` (will be removed in kfactory 2.0)."""

    def __init__(self, **data: Any) -> None:
        logger.warning(
            "Schema is deprecated, please use Schematic. "
            "It will be removed in kfactory 2.0"
        )
        if "unit" in data:
            raise ValueError(
                "Cannot set the unit direct. It needs to be set by the class init."
            )
        super().__init__(**data)

Schematic pydantic-model

Bases: TSchematic[dbu]

Schematic with a base unit of dbu for placements.

Fields:

Validators:

  • _validate_schematic
  • assign_backrefs
Source code in kfactory/schematic.py
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
class Schematic(TSchematic[dbu]):
    """Schematic with a base unit of dbu for placements."""

    unit: Literal["dbu"] = "dbu"

    def __init__(self, **data: Any) -> None:
        if "unit" in data:
            raise ValueError(
                "Cannot set the unit direct. It needs to be set by the class init."
            )
        super().__init__(unit="dbu", **data)

SymmetricalCrossSection pydantic-model

Bases: BaseModel

CrossSection which is symmetrical to its main_layer/width.

Fields:

  • width (int)
  • enclosure (LayerEnclosure)
  • name (str)
  • radius (int | None)
  • radius_min (int | None)
  • bbox_sections (dict[LayerInfo, int])

Validators:

  • _set_name
  • _validate_enclosure_main_layer
  • _validate_width
Source code in kfactory/cross_section.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class SymmetricalCrossSection(BaseModel, frozen=True, arbitrary_types_allowed=True):
    """CrossSection which is symmetrical to its main_layer/width."""

    width: int
    enclosure: LayerEnclosure
    name: str = ""
    radius: int | None = None
    radius_min: int | None = None
    bbox_sections: dict[kdb.LayerInfo, int]

    def __init__(
        self,
        width: int,
        enclosure: LayerEnclosure,
        name: str | None = None,
        bbox_sections: dict[kdb.LayerInfo, int] | None = None,
        radius: int | None = None,
        radius_min: int | None = None,
    ) -> None:
        """Initialized the CrossSection."""
        super().__init__(
            width=width,
            enclosure=enclosure,
            name=name or f"{enclosure.name}_{width}",
            bbox_sections=bbox_sections or {},
            radius=radius,
            radius_min=radius_min,
        )

    @model_validator(mode="before")
    @classmethod
    def _set_name(cls, data: Any) -> Any:
        data["name"] = data.get("name") or f"{data['enclosure'].name}_{data['width']}"
        return data

    @model_validator(mode="after")
    def _validate_enclosure_main_layer(self) -> Self:
        if self.enclosure.main_layer is None:
            raise ValueError("Enclosures of cross sections must have a main layer.")
        if self.width % 2:
            raise ValueError(
                "Width of symmetrical cross sections must have be a multiple of 2 dbu. "
                "This could cause cross sections and extrusions to become unsymmetrical"
                " otherwise."
            )
        if not self.width:
            raise ValueError("Cross section with width 0 is not allowed.")
        return self

    @model_validator(mode="after")
    def _validate_width(self) -> Self:
        if self.width <= 0:
            raise ValueError("Width must be greater than 0.")
        return self

    @property
    def main_layer(self) -> kdb.LayerInfo:
        """Main Layer of the enclosure and cross section."""
        assert self.enclosure.main_layer is not None
        return self.enclosure.main_layer

    def to_dtype(self, kcl: KCLayout) -> DSymmetricalCrossSection:
        """Convert to a um based CrossSection."""
        return DSymmetricalCrossSection(
            width=kcl.to_um(self.width),
            enclosure=self.enclosure.to_dtype(kcl),
            name=self.name,
        )

    def get_xmax(self) -> int:
        return self.width // 2 + max(
            s.d_max
            for sections in self.enclosure.layer_sections.values()
            for s in sections.sections
        )

    def model_copy(
        self, *, update: Mapping[str, Any] | None = {"name": None}, deep: bool = False
    ) -> SymmetricalCrossSection:
        return super().model_copy(update=update, deep=deep)

    def __eq__(self, o: object) -> bool:
        if isinstance(o, TCrossSection):
            return o == self
        return super().__eq__(o)

main_layer property

main_layer: LayerInfo

Main Layer of the enclosure and cross section.

__init__

__init__(
    width: int,
    enclosure: LayerEnclosure,
    name: str | None = None,
    bbox_sections: dict[LayerInfo, int] | None = None,
    radius: int | None = None,
    radius_min: int | None = None,
) -> None

Initialized the CrossSection.

Source code in kfactory/cross_section.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def __init__(
    self,
    width: int,
    enclosure: LayerEnclosure,
    name: str | None = None,
    bbox_sections: dict[kdb.LayerInfo, int] | None = None,
    radius: int | None = None,
    radius_min: int | None = None,
) -> None:
    """Initialized the CrossSection."""
    super().__init__(
        width=width,
        enclosure=enclosure,
        name=name or f"{enclosure.name}_{width}",
        bbox_sections=bbox_sections or {},
        radius=radius,
        radius_min=radius_min,
    )

to_dtype

to_dtype(kcl: KCLayout) -> DSymmetricalCrossSection

Convert to a um based CrossSection.

Source code in kfactory/cross_section.py
 98
 99
100
101
102
103
104
def to_dtype(self, kcl: KCLayout) -> DSymmetricalCrossSection:
    """Convert to a um based CrossSection."""
    return DSymmetricalCrossSection(
        width=kcl.to_um(self.width),
        enclosure=self.enclosure.to_dtype(kcl),
        name=self.name,
    )

VInstance

Bases: ProtoInstance[float], UMGeometricObject

Source code in kfactory/instance.py
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
class VInstance(ProtoInstance[float], UMGeometricObject):
    _name: str | None
    cell: AnyKCell
    trans: kdb.DCplxTrans
    a: kdb.DVector
    b: kdb.DVector
    na: int = 1
    nb: int = 1

    def __init__(
        self,
        cell: AnyKCell,
        trans: kdb.DCplxTrans | None = None,
        name: str | None = None,
        *,
        a: kdb.DVector = kdb.DVector(0, 0),  # noqa: B008
        b: kdb.DVector = kdb.DVector(0, 0),  # noqa: B008
        na: int = 1,
        nb: int = 1,
    ) -> None:
        self.kcl = cell.kcl
        self._name = name
        self.cell = cell
        self.trans = trans or kdb.DCplxTrans()
        self.a = a
        self.b = b
        self.na = na
        self.nb = nb

    @property
    def name(self) -> str | None:
        return self._name

    @name.setter
    def name(self, value: str | None) -> None:
        self._name = value

    @property
    def dcplx_trans(self) -> kdb.DCplxTrans:
        return self.trans

    @dcplx_trans.setter
    def dcplx_trans(self, val: kdb.DCplxTrans) -> None:
        self.trans = val

    @property
    def cell_name(self) -> str | None:
        return self.cell.name

    def ibbox(self, layer: int | LayerEnum | None = None) -> kdb.Box:
        return self.dbbox(layer).to_itype(self.kcl.dbu)

    def dbbox(self, layer: int | LayerEnum | None = None) -> kdb.DBox:
        cell_bb = self.cell.dbbox(layer)
        na_ = self.na - 1
        nb_ = self.nb - 1
        if na_ or nb_:
            return cell_bb.transformed(self.trans) + cell_bb.transformed(
                self.trans * kdb.DCplxTrans(na_ * self.a + nb_ * self.b)
            )
        return cell_bb.transformed(self.trans)

    def __getitem__(
        self, key: int | str | tuple[int | str | None, int, int] | None
    ) -> DPort:
        """Returns port from instance.

        The key can either be an integer, in which case the nth port is
        returned, or a string in which case the first port with a matching
        name is returned.

        If the instance is an array, the key can also be a tuple in the
        form of `c.ports[key_name, i_a, i_b]`, where `i_a` is the index in
        the `instance.a` direction and `i_b` the `instance.b` direction.

        E.g. `c.ports["a", 3, 5]`, accesses the ports of the instance which is
        3 times in `a` direction (4th index in the array), and 5 times in `b` direction
        (5th index in the array).
        """
        return self.ports[key]

    @functools.cached_property
    def ports(self) -> VInstancePorts:
        from .instance_ports import VInstancePorts

        return VInstancePorts(self)

    @functools.cached_property
    def pins(self) -> VInstancePins:
        from .instance_pins import VInstancePins

        return VInstancePins(self)

    def __repr__(self) -> str:
        """Return a string representation of the instance."""
        port_names = [p.name for p in self.ports]
        return f"{self.cell.name}: ports {port_names}, transformation {self.trans}"

    def insert_into(
        self,
        cell: AnyTKCell,
        trans: kdb.DCplxTrans | None = None,
    ) -> Instance:
        from .kcell import KCell, ProtoTKCell, VKCell

        if trans is None:
            trans = kdb.DCplxTrans()

        if isinstance(self.cell, VKCell):
            trans_ = trans * self.trans
            base_trans = kdb.DCplxTrans(
                kdb.DCplxTrans(
                    kdb.ICplxTrans(trans_, cell.kcl.dbu)
                    .s_trans()
                    .to_dtype(cell.kcl.dbu)
                )
            )
            trans_ = base_trans.inverted() * trans_
            cell_name = self.cell.name
            if cell_name is None:
                raise ValueError(
                    "Cannot insert a non-flattened VInstance into a VKCell when the"
                    f" name is 'None'. VKCell at {self.trans}"
                )
            if trans_ != kdb.DCplxTrans():
                cell_name += f"_{trans_.hash():x}"
            if cell.kcl.layout_cell(cell_name) is None:
                cell_ = KCell(kcl=self.cell.kcl, name=cell_name)  # self.cell.dup()
                for layer, shapes in self.cell.shapes().items():
                    for shape in shapes.transform(trans_):
                        cell_.shapes(layer).insert(shape)
                for inst in self.cell.insts:
                    inst.insert_into(cell=cell_, trans=trans_)
                cell_.name = cell_name
                for port in self.cell.ports:
                    cell_.add_port(port=port.copy(trans_))
                for c_shapes in (
                    cell_.shapes(layer) for layer in cell_.kcl.layer_indexes()
                ):
                    if not c_shapes.is_empty():
                        r = kdb.Region(c_shapes)
                        r.merge()
                        c_shapes.clear()
                        c_shapes.insert(r)
                settings = self.cell.settings.model_copy()
                settings_units = self.cell.settings_units.model_copy()
                cell_.settings = settings
                cell_.info = self.cell.info.model_copy(deep=True)
                cell_.settings_units = settings_units
                cell_.function_name = self.cell.function_name
                cell_.basename = self.cell.basename
                cell_._base.virtual = True
                if trans_ != kdb.DCplxTrans.R0:
                    cell_._base.vtrans = trans_
            else:
                cell_ = cell.kcl[cell_name]
            inst_ = cell.create_inst(
                cell=cell_, na=self.na, nb=self.nb, a=self.a, b=self.b
            )
            inst_.transform(base_trans)
            if self._name:
                inst_.name = self._name
            return Instance(kcl=self.cell.kcl, instance=inst_.instance)

        assert isinstance(self.cell, ProtoTKCell)
        trans_ = trans * self.trans
        base_trans = kdb.DCplxTrans(
            kdb.ICplxTrans(trans_, cell.kcl.dbu).s_trans().to_dtype(cell.kcl.dbu)
        )
        trans_ = base_trans.inverted() * trans_
        cell_name = self.cell.name
        if trans_ != kdb.DCplxTrans():
            cell_name += f"_{trans_.hash():x}"
        else:
            inst_ = cell.create_inst(
                cell=self.cell, na=self.na, nb=self.nb, a=self.a, b=self.b
            )
            if self._name:
                inst_.name = self._name
            inst_.transform(base_trans)
            return Instance(kcl=self.cell.kcl, instance=inst_.instance)
        if cell.kcl.layout_cell(cell_name) is None:
            tkcell = self.cell.dup()
            tkcell.name = cell_name
            tkcell.flatten(True)
            for layer in tkcell.kcl.layer_indexes():
                tkcell.shapes(layer).transform(trans_)
            for _port in tkcell.ports:
                _port.dcplx_trans = trans_ * _port.dcplx_trans
            if trans_ != kdb.DCplxTrans.R0:
                tkcell._base.vtrans = trans_
            settings = self.cell.settings.model_copy()
            settings_units = self.cell.settings_units.model_copy()
            tkcell.settings = settings
            tkcell.info = self.cell.info.model_copy(deep=True)
            tkcell.settings_units = settings_units
            tkcell.function_name = self.cell.function_name
            tkcell.basename = self.cell.basename
            tkcell._base.vtrans = trans_
        else:
            tkcell = cell.kcl[cell_name]
        inst_ = cell.create_inst(
            cell=tkcell, na=self.na, nb=self.nb, a=self.a, b=self.b
        )
        inst_.transform(base_trans)
        if self._name:
            inst_.name = self._name
        return Instance(kcl=self.cell.kcl, instance=inst_.instance)

    @overload
    def insert_into_flat(
        self,
        cell: AnyKCell,
        trans: kdb.DCplxTrans | None = None,
        *,
        levels: None = None,
    ) -> None: ...

    @overload
    def insert_into_flat(
        self,
        cell: AnyKCell,
        *,
        trans: kdb.DCplxTrans | None = None,
        levels: int,
    ) -> None: ...

    def insert_into_flat(
        self,
        cell: AnyKCell,
        trans: kdb.DCplxTrans | None = None,
        *,
        levels: int | None = None,
    ) -> None:
        from .kcell import ProtoTKCell, VKCell

        if trans is None:
            trans = kdb.DCplxTrans()

        if isinstance(self.cell, VKCell):
            for layer, shapes in self.cell.shapes().items():
                for shape in shapes.transform(trans * self.trans):
                    cell.shapes(layer).insert(shape)
            for inst in self.cell.insts:
                if levels is not None:
                    if levels > 0:
                        inst.insert_into_flat(
                            cell, trans=trans * self.trans, levels=levels - 1
                        )
                    else:
                        assert isinstance(cell, ProtoTKCell)
                        inst.insert_into(cell, trans=trans * self.trans)
                else:
                    inst.insert_into_flat(cell, trans=trans * self.trans)

        else:
            assert isinstance(self.cell, ProtoTKCell)
            if levels:
                logger.warning(
                    "Levels are not supported if the inserted Instance is a KCell."
                )
            if isinstance(cell, ProtoTKCell):
                for layer in cell.kcl.layer_indexes():
                    reg = kdb.Region(self.cell.kdb_cell.begin_shapes_rec(layer))
                    reg.transform(kdb.ICplxTrans((trans * self.trans), cell.kcl.dbu))
                    cell.shapes(layer).insert(reg)
            else:
                for layer, shapes in self.cell._shapes.items():
                    for shape in shapes.transform(trans * self.trans):
                        cell.shapes(layer).insert(shape)
                for vinst in self.cell.insts:
                    vinst.insert_into_flat(cell, trans=trans * self.trans)

    @overload
    def connect(
        self,
        port: str | ProtoPort[Any] | None,
        other: ProtoPort[Any],
        *,
        mirror: bool = False,
        allow_width_mismatch: bool | None = None,
        allow_layer_mismatch: bool | None = None,
        allow_type_mismatch: bool | None = None,
        use_mirror: bool | None = None,
        use_angle: bool | None = None,
    ) -> None: ...

    @overload
    def connect(
        self,
        port: str | ProtoPort[Any] | None,
        other: ProtoTInstance[Any],
        other_port_name: str | int | tuple[int | str, int, int] | None,
        *,
        mirror: bool = False,
        allow_width_mismatch: bool | None = None,
        allow_layer_mismatch: bool | None = None,
        allow_type_mismatch: bool | None = None,
        use_mirror: bool | None = None,
        use_angle: bool | None = None,
    ) -> None: ...

    @overload
    def connect(
        self,
        port: str | ProtoPort[Any] | None,
        other: VInstance,
        other_port_name: str | int | None,
        *,
        mirror: bool = False,
        allow_width_mismatch: bool | None = None,
        allow_layer_mismatch: bool | None = None,
        allow_type_mismatch: bool | None = None,
        use_mirror: bool | None = None,
        use_angle: bool | None = None,
    ) -> None: ...

    def connect(
        self,
        port: str | ProtoPort[Any] | None,
        other: ProtoInstance[Any] | ProtoPort[Any],
        other_port_name: str | int | tuple[int | str, int, int] | None = None,
        *,
        mirror: bool = False,
        allow_width_mismatch: bool | None = None,
        allow_layer_mismatch: bool | None = None,
        allow_type_mismatch: bool | None = None,
        use_mirror: bool | None = None,
        use_angle: bool | None = None,
    ) -> None:
        """Align port with name `portname` to a port.

        Function to allow to transform this instance so that a port of this instance is
        connected (same center with 180° turn) to another instance.

        Args:
            port: The name of the port of this instance to be connected, or directly an
                instance port. Can be `None` because port names can be `None`.
            other: The other instance or a port. Skip `other_port_name` if it's a port.
            other_port_name: The name of the other port. Ignored if
                `other` is a port.
            mirror: Instead of applying klayout.db.Trans.R180 as a connection
                transformation, use klayout.db.Trans.M90, which effectively means this
                instance will be mirrored and connected.
            allow_width_mismatch: Skip width check between the ports if set.
            allow_layer_mismatch: Skip layer check between the ports if set.
            allow_type_mismatch: Skip port_type check between the ports if set.
            use_mirror: If False mirror flag does not get applied from the connection.
            use_angle: If False the angle does not get applied from the connection.
        """
        if allow_layer_mismatch is None:
            allow_layer_mismatch = config.allow_layer_mismatch
        if allow_width_mismatch is None:
            allow_width_mismatch = config.allow_width_mismatch
        if allow_type_mismatch is None:
            allow_type_mismatch = config.allow_type_mismatch
        if use_mirror is None:
            use_mirror = config.connect_use_mirror
        if use_angle is None:
            use_angle = config.connect_use_angle
        if isinstance(other, ProtoInstance):
            if other_port_name is None:
                raise ValueError(
                    "portname cannot be None if an Instance Object is given. For"
                    "complex connections (non-90 degree and floating point ports) use"
                    "route_cplx instead"
                )
            op = Port(base=other.ports[other_port_name].base)  # type: ignore[index]
        else:
            op = Port(base=other.base)
        if isinstance(port, ProtoPort):
            p = port.copy(self.trans.inverted()).to_itype()
        else:
            p = self.cell.ports[port].to_itype()

        assert isinstance(p, Port)
        assert isinstance(op, Port)

        if p.width != op.width and not allow_width_mismatch:
            raise PortWidthMismatchError(self, other, p, op)
        if p.layer != op.layer and not allow_layer_mismatch:
            raise PortLayerMismatchError(self.cell.kcl, self, other, p, op)
        if p.port_type != op.port_type and not allow_type_mismatch:
            raise PortTypeMismatchError(self, other, p, op)
        dconn_trans = kdb.DCplxTrans.M90 if mirror else kdb.DCplxTrans.R180
        match (use_mirror, use_angle):
            case True, True:
                trans = op.dcplx_trans * dconn_trans * p.dcplx_trans.inverted()
                self.trans = trans
            case False, True:
                dconn_trans = (
                    kdb.DCplxTrans.M90
                    if mirror ^ self.dcplx_trans.mirror
                    else kdb.DCplxTrans.R180
                )
                opt = op.dcplx_trans
                opt.mirror = False
                dcplx_trans = opt * dconn_trans * p.dcplx_trans.inverted()
                self.trans = dcplx_trans
            case False, False:
                self.trans = kdb.DCplxTrans(op.dcplx_trans.disp - p.dcplx_trans.disp)
            case True, False:
                self.trans = kdb.DCplxTrans(op.dcplx_trans.disp - p.dcplx_trans.disp)
                self.mirror_y(op.dcplx_trans.disp.y)
            case _:
                ...

    def transform(
        self,
        trans: kdb.Trans | kdb.DTrans | kdb.ICplxTrans | kdb.DCplxTrans,
        /,
    ) -> None:
        if isinstance(trans, kdb.Trans):
            trans = trans.to_dtype(self.kcl.dbu)
        self.trans = kdb.DCplxTrans(trans) * self.trans

cell_name property

cell_name: str | None

Name of the cell the instance refers to.

kcl instance-attribute

kcl = kcl

KCLayout object.

name property writable

name: str | None

Name of the instance.

__getitem__

__getitem__(
    key: int
    | str
    | tuple[int | str | None, int, int]
    | None,
) -> DPort

Returns port from instance.

The key can either be an integer, in which case the nth port is returned, or a string in which case the first port with a matching name is returned.

If the instance is an array, the key can also be a tuple in the form of c.ports[key_name, i_a, i_b], where i_a is the index in the instance.a direction and i_b the instance.b direction.

E.g. c.ports["a", 3, 5], accesses the ports of the instance which is 3 times in a direction (4th index in the array), and 5 times in b direction (5th index in the array).

Source code in kfactory/instance.py
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
def __getitem__(
    self, key: int | str | tuple[int | str | None, int, int] | None
) -> DPort:
    """Returns port from instance.

    The key can either be an integer, in which case the nth port is
    returned, or a string in which case the first port with a matching
    name is returned.

    If the instance is an array, the key can also be a tuple in the
    form of `c.ports[key_name, i_a, i_b]`, where `i_a` is the index in
    the `instance.a` direction and `i_b` the `instance.b` direction.

    E.g. `c.ports["a", 3, 5]`, accesses the ports of the instance which is
    3 times in `a` direction (4th index in the array), and 5 times in `b` direction
    (5th index in the array).
    """
    return self.ports[key]

__repr__

__repr__() -> str

Return a string representation of the instance.

Source code in kfactory/instance.py
756
757
758
759
def __repr__(self) -> str:
    """Return a string representation of the instance."""
    port_names = [p.name for p in self.ports]
    return f"{self.cell.name}: ports {port_names}, transformation {self.trans}"

connect

connect(
    port: str | ProtoPort[Any] | None,
    other: ProtoPort[Any],
    *,
    mirror: bool = False,
    allow_width_mismatch: bool | None = None,
    allow_layer_mismatch: bool | None = None,
    allow_type_mismatch: bool | None = None,
    use_mirror: bool | None = None,
    use_angle: bool | None = None,
) -> None
connect(
    port: str | ProtoPort[Any] | None,
    other: ProtoTInstance[Any],
    other_port_name: str
    | int
    | tuple[int | str, int, int]
    | None,
    *,
    mirror: bool = False,
    allow_width_mismatch: bool | None = None,
    allow_layer_mismatch: bool | None = None,
    allow_type_mismatch: bool | None = None,
    use_mirror: bool | None = None,
    use_angle: bool | None = None,
) -> None
connect(
    port: str | ProtoPort[Any] | None,
    other: VInstance,
    other_port_name: str | int | None,
    *,
    mirror: bool = False,
    allow_width_mismatch: bool | None = None,
    allow_layer_mismatch: bool | None = None,
    allow_type_mismatch: bool | None = None,
    use_mirror: bool | None = None,
    use_angle: bool | None = None,
) -> None
connect(
    port: str | ProtoPort[Any] | None,
    other: ProtoInstance[Any] | ProtoPort[Any],
    other_port_name: str
    | int
    | tuple[int | str, int, int]
    | None = None,
    *,
    mirror: bool = False,
    allow_width_mismatch: bool | None = None,
    allow_layer_mismatch: bool | None = None,
    allow_type_mismatch: bool | None = None,
    use_mirror: bool | None = None,
    use_angle: bool | None = None,
) -> None

Align port with name portname to a port.

Function to allow to transform this instance so that a port of this instance is connected (same center with 180° turn) to another instance.

Parameters:

Name Type Description Default
port str | ProtoPort[Any] | None

The name of the port of this instance to be connected, or directly an instance port. Can be None because port names can be None.

required
other ProtoInstance[Any] | ProtoPort[Any]

The other instance or a port. Skip other_port_name if it's a port.

required
other_port_name str | int | tuple[int | str, int, int] | None

The name of the other port. Ignored if other is a port.

None
mirror bool

Instead of applying klayout.db.Trans.R180 as a connection transformation, use klayout.db.Trans.M90, which effectively means this instance will be mirrored and connected.

False
allow_width_mismatch bool | None

Skip width check between the ports if set.

None
allow_layer_mismatch bool | None

Skip layer check between the ports if set.

None
allow_type_mismatch bool | None

Skip port_type check between the ports if set.

None
use_mirror bool | None

If False mirror flag does not get applied from the connection.

None
use_angle bool | None

If False the angle does not get applied from the connection.

None
Source code in kfactory/instance.py
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
def connect(
    self,
    port: str | ProtoPort[Any] | None,
    other: ProtoInstance[Any] | ProtoPort[Any],
    other_port_name: str | int | tuple[int | str, int, int] | None = None,
    *,
    mirror: bool = False,
    allow_width_mismatch: bool | None = None,
    allow_layer_mismatch: bool | None = None,
    allow_type_mismatch: bool | None = None,
    use_mirror: bool | None = None,
    use_angle: bool | None = None,
) -> None:
    """Align port with name `portname` to a port.

    Function to allow to transform this instance so that a port of this instance is
    connected (same center with 180° turn) to another instance.

    Args:
        port: The name of the port of this instance to be connected, or directly an
            instance port. Can be `None` because port names can be `None`.
        other: The other instance or a port. Skip `other_port_name` if it's a port.
        other_port_name: The name of the other port. Ignored if
            `other` is a port.
        mirror: Instead of applying klayout.db.Trans.R180 as a connection
            transformation, use klayout.db.Trans.M90, which effectively means this
            instance will be mirrored and connected.
        allow_width_mismatch: Skip width check between the ports if set.
        allow_layer_mismatch: Skip layer check between the ports if set.
        allow_type_mismatch: Skip port_type check between the ports if set.
        use_mirror: If False mirror flag does not get applied from the connection.
        use_angle: If False the angle does not get applied from the connection.
    """
    if allow_layer_mismatch is None:
        allow_layer_mismatch = config.allow_layer_mismatch
    if allow_width_mismatch is None:
        allow_width_mismatch = config.allow_width_mismatch
    if allow_type_mismatch is None:
        allow_type_mismatch = config.allow_type_mismatch
    if use_mirror is None:
        use_mirror = config.connect_use_mirror
    if use_angle is None:
        use_angle = config.connect_use_angle
    if isinstance(other, ProtoInstance):
        if other_port_name is None:
            raise ValueError(
                "portname cannot be None if an Instance Object is given. For"
                "complex connections (non-90 degree and floating point ports) use"
                "route_cplx instead"
            )
        op = Port(base=other.ports[other_port_name].base)  # type: ignore[index]
    else:
        op = Port(base=other.base)
    if isinstance(port, ProtoPort):
        p = port.copy(self.trans.inverted()).to_itype()
    else:
        p = self.cell.ports[port].to_itype()

    assert isinstance(p, Port)
    assert isinstance(op, Port)

    if p.width != op.width and not allow_width_mismatch:
        raise PortWidthMismatchError(self, other, p, op)
    if p.layer != op.layer and not allow_layer_mismatch:
        raise PortLayerMismatchError(self.cell.kcl, self, other, p, op)
    if p.port_type != op.port_type and not allow_type_mismatch:
        raise PortTypeMismatchError(self, other, p, op)
    dconn_trans = kdb.DCplxTrans.M90 if mirror else kdb.DCplxTrans.R180
    match (use_mirror, use_angle):
        case True, True:
            trans = op.dcplx_trans * dconn_trans * p.dcplx_trans.inverted()
            self.trans = trans
        case False, True:
            dconn_trans = (
                kdb.DCplxTrans.M90
                if mirror ^ self.dcplx_trans.mirror
                else kdb.DCplxTrans.R180
            )
            opt = op.dcplx_trans
            opt.mirror = False
            dcplx_trans = opt * dconn_trans * p.dcplx_trans.inverted()
            self.trans = dcplx_trans
        case False, False:
            self.trans = kdb.DCplxTrans(op.dcplx_trans.disp - p.dcplx_trans.disp)
        case True, False:
            self.trans = kdb.DCplxTrans(op.dcplx_trans.disp - p.dcplx_trans.disp)
            self.mirror_y(op.dcplx_trans.disp.y)
        case _:
            ...

VInstanceGroup

Bases: ProtoInstanceGroup[float, VInstance], UMGeometricObject, DCreatePort

Group of DInstances.

The instance group can be treated similar to a single instance with regards to transformation functions and bounding boxes.

Parameters:

Name Type Description Default
insts Sequence[TInstance_co] | None

List of the vinstances of the group.

None
Source code in kfactory/instance_group.py
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
class VInstanceGroup(
    ProtoInstanceGroup[float, VInstance], UMGeometricObject, DCreatePort
):
    """Group of DInstances.

    The instance group can be treated similar to a single instance
    with regards to transformation functions and bounding boxes.

    Args:
        insts: List of the vinstances of the group.
    """

    @cached_property
    def ports(self) -> DPorts:
        return DPorts(kcl=self.kcl, bases=self._base_ports)

    def add_port(
        self,
        *,
        port: ProtoPort[Any],
        name: str | None = None,
        keep_mirror: bool = False,
    ) -> DPort:
        return self.ports.add_port(port=port, name=name, keep_mirror=keep_mirror)

ports cached property

ports: DPorts

Ports of the instance.

VInstancePorts

Bases: ProtoInstancePorts[float, VInstance]

Ports of an instance.

These act as virtual ports as the centers needs to change if the instance changes etc.

Attributes:

Name Type Description
cell_ports DPorts

A pointer to the KCell.ports of the cell

instance VInstance

A pointer to the Instance related to this. This provides a way to dynamically calculate the ports.

Source code in kfactory/instance_ports.py
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
class VInstancePorts(ProtoInstancePorts[float, VInstance]):
    """Ports of an instance.

    These act as virtual ports as the centers needs to change if the
    instance changes etc.


    Attributes:
        cell_ports: A pointer to the [`KCell.ports`][kfactory.kcell.KCell.ports]
            of the cell
        instance: A pointer to the Instance related to this.
            This provides a way to dynamically calculate the ports.
    """

    instance: VInstance

    def __init__(self, instance: VInstance) -> None:
        """Creates the virtual ports object.

        Args:
            instance: The related instance
        """
        self.instance = instance

    @property
    def cell_ports(self) -> DPorts:
        return DPorts(
            kcl=self.instance.cell.ports.kcl, bases=self.instance.cell.ports.bases
        )

    def __len__(self) -> int:
        """Return Port count."""
        return len(self.cell_ports)

    def __getitem__(
        self, key: int | str | tuple[int | str | None, int, int] | None
    ) -> DPort:
        if isinstance(key, tuple):
            return self.cell_ports[key[0]].copy(
                kdb.DCplxTrans(
                    (self.instance.na - 1) * self.instance.a
                    + (self.instance.nb - 1) * self.instance.b
                )
                * self.instance.trans
            )
        return self.cell_ports[key].copy(self.instance.trans)

    def __iter__(self) -> Iterator[DPort]:
        """Create a copy of the ports to iterate through."""
        yield from (p.copy(self.instance.trans) for p in self.cell_ports)

    def __contains__(self, port: str | ProtoPort[Any]) -> bool:
        """Check if a port is in the instance."""
        if isinstance(port, ProtoPort):
            return port.base in [p.base for p in self.instance.ports]
        return any(_port.name == port for _port in self.instance.ports)

    def filter(
        self,
        angle: int | None = None,
        orientation: float | None = None,
        layer: LayerEnum | int | None = None,
        port_type: str | None = None,
        regex: str | None = None,
    ) -> list[DPort]:
        """Filter ports by name.

        Args:
            angle: Filter by angle. 0, 1, 2, 3.
            orientation: Filter by orientation in degrees.
            layer: Filter by layer.
            port_type: Filter by port type.
            regex: Filter by regex of the name.
        """
        ports = list(self.instance.ports)
        if regex:
            ports = list(filter_regex(ports, regex))
        if layer is not None:
            ports = list(filter_layer(ports, layer))
        if port_type:
            ports = list(filter_port_type(ports, port_type))
        if angle is not None:
            ports = list(filter_direction(ports, angle))
        if orientation is not None:
            ports = list(filter_orientation(ports, orientation))
        return list(ports)

    def copy(self) -> DPorts:
        """Creates a copy in the form of [Ports][kfactory.kcell.Ports]."""
        return DPorts(
            kcl=self.instance.cell.kcl,
            bases=[b.transformed(self.instance.trans) for b in self.cell_ports.bases],
        )

__contains__

__contains__(port: str | ProtoPort[Any]) -> bool

Check if a port is in the instance.

Source code in kfactory/instance_ports.py
431
432
433
434
435
def __contains__(self, port: str | ProtoPort[Any]) -> bool:
    """Check if a port is in the instance."""
    if isinstance(port, ProtoPort):
        return port.base in [p.base for p in self.instance.ports]
    return any(_port.name == port for _port in self.instance.ports)

__init__

__init__(instance: VInstance) -> None

Creates the virtual ports object.

Parameters:

Name Type Description Default
instance VInstance

The related instance

required
Source code in kfactory/instance_ports.py
396
397
398
399
400
401
402
def __init__(self, instance: VInstance) -> None:
    """Creates the virtual ports object.

    Args:
        instance: The related instance
    """
    self.instance = instance

__iter__

__iter__() -> Iterator[DPort]

Create a copy of the ports to iterate through.

Source code in kfactory/instance_ports.py
427
428
429
def __iter__(self) -> Iterator[DPort]:
    """Create a copy of the ports to iterate through."""
    yield from (p.copy(self.instance.trans) for p in self.cell_ports)

__len__

__len__() -> int

Return Port count.

Source code in kfactory/instance_ports.py
410
411
412
def __len__(self) -> int:
    """Return Port count."""
    return len(self.cell_ports)

copy

copy() -> DPorts

Creates a copy in the form of Ports.

Source code in kfactory/instance_ports.py
467
468
469
470
471
472
def copy(self) -> DPorts:
    """Creates a copy in the form of [Ports][kfactory.kcell.Ports]."""
    return DPorts(
        kcl=self.instance.cell.kcl,
        bases=[b.transformed(self.instance.trans) for b in self.cell_ports.bases],
    )

filter

filter(
    angle: int | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> list[DPort]

Filter ports by name.

Parameters:

Name Type Description Default
angle int | None

Filter by angle. 0, 1, 2, 3.

None
orientation float | None

Filter by orientation in degrees.

None
layer LayerEnum | int | None

Filter by layer.

None
port_type str | None

Filter by port type.

None
regex str | None

Filter by regex of the name.

None
Source code in kfactory/instance_ports.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
def filter(
    self,
    angle: int | None = None,
    orientation: float | None = None,
    layer: LayerEnum | int | None = None,
    port_type: str | None = None,
    regex: str | None = None,
) -> list[DPort]:
    """Filter ports by name.

    Args:
        angle: Filter by angle. 0, 1, 2, 3.
        orientation: Filter by orientation in degrees.
        layer: Filter by layer.
        port_type: Filter by port type.
        regex: Filter by regex of the name.
    """
    ports = list(self.instance.ports)
    if regex:
        ports = list(filter_regex(ports, regex))
    if layer is not None:
        ports = list(filter_layer(ports, layer))
    if port_type:
        ports = list(filter_port_type(ports, port_type))
    if angle is not None:
        ports = list(filter_direction(ports, angle))
    if orientation is not None:
        ports = list(filter_orientation(ports, orientation))
    return list(ports)

VInstances

Bases: ProtoInstances[float, VInstance]

Holder for VInstances.

Allows retrieval by name or index

Source code in kfactory/instances.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
class VInstances(ProtoInstances[float, VInstance]):
    """Holder for VInstances.

    Allows retrieval by name or index
    """

    _vinsts: list[VInstance]

    def __init__(self, vinsts: list[VInstance] | None = None) -> None:
        self._vinsts = vinsts or []

    def __iter__(self) -> Iterator[VInstance]:
        """Get instance iterator."""
        yield from self._vinsts

    def __len__(self) -> int:
        """Get the number of instances."""
        return len(self._vinsts)

    def __delitem__(self, item: VInstance | int) -> None:
        """Delete an instance by index or instance."""
        if isinstance(item, int):
            del self._vinsts[item]
        else:
            self._vinsts.remove(item)

    def __getitem__(self, key: str | int) -> VInstance:
        """Retrieve instance by index or by name."""
        if isinstance(key, int):
            return self._vinsts[key]
        for inst in self._vinsts:
            if inst.name == key:
                return inst
        raise KeyError(f"No instance found with name: {key}")

    def __contains__(self, key: str | int | VInstance) -> bool:
        if isinstance(key, VInstance):
            return key in self._vinsts
        if isinstance(key, int):
            return key < len(self)
        return any(inst.name == key for inst in self._vinsts)

    def clear(self) -> None:
        """Clear all instances."""
        self._vinsts.clear()

    def append(self, inst: VInstance) -> None:
        """Append a new instance."""
        self._vinsts.append(inst)

    def remove(self, inst: VInstance) -> None:
        """Remove an instance."""
        self._vinsts.remove(inst)

    def copy(self) -> VInstances:
        """Copy the instances."""
        return VInstances(self._vinsts)

__delitem__

__delitem__(item: VInstance | int) -> None

Delete an instance by index or instance.

Source code in kfactory/instances.py
187
188
189
190
191
192
def __delitem__(self, item: VInstance | int) -> None:
    """Delete an instance by index or instance."""
    if isinstance(item, int):
        del self._vinsts[item]
    else:
        self._vinsts.remove(item)

__getitem__

__getitem__(key: str | int) -> VInstance

Retrieve instance by index or by name.

Source code in kfactory/instances.py
194
195
196
197
198
199
200
201
def __getitem__(self, key: str | int) -> VInstance:
    """Retrieve instance by index or by name."""
    if isinstance(key, int):
        return self._vinsts[key]
    for inst in self._vinsts:
        if inst.name == key:
            return inst
    raise KeyError(f"No instance found with name: {key}")

__iter__

__iter__() -> Iterator[VInstance]

Get instance iterator.

Source code in kfactory/instances.py
179
180
181
def __iter__(self) -> Iterator[VInstance]:
    """Get instance iterator."""
    yield from self._vinsts

__len__

__len__() -> int

Get the number of instances.

Source code in kfactory/instances.py
183
184
185
def __len__(self) -> int:
    """Get the number of instances."""
    return len(self._vinsts)

append

append(inst: VInstance) -> None

Append a new instance.

Source code in kfactory/instances.py
214
215
216
def append(self, inst: VInstance) -> None:
    """Append a new instance."""
    self._vinsts.append(inst)

clear

clear() -> None

Clear all instances.

Source code in kfactory/instances.py
210
211
212
def clear(self) -> None:
    """Clear all instances."""
    self._vinsts.clear()

copy

copy() -> VInstances

Copy the instances.

Source code in kfactory/instances.py
222
223
224
def copy(self) -> VInstances:
    """Copy the instances."""
    return VInstances(self._vinsts)

remove

remove(inst: VInstance) -> None

Remove an instance.

Source code in kfactory/instances.py
218
219
220
def remove(self, inst: VInstance) -> None:
    """Remove an instance."""
    self._vinsts.remove(inst)

VKCell

Bases: ProtoKCell[float, TVCell], UMGeometricObject, DCreatePort

Emulate [klayout.db.Cell][klayout.db.Cell].

Source code in kfactory/kcell.py
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
class VKCell(ProtoKCell[float, TVCell], UMGeometricObject, DCreatePort):
    """Emulate `[klayout.db.Cell][klayout.db.Cell]`."""

    @overload
    def __init__(self, *, base: TVCell) -> None: ...

    @overload
    def __init__(
        self,
        *,
        name: str | None = None,
        kcl: KCLayout | None = None,
        info: dict[str, Any] | None = None,
        settings: dict[str, Any] | None = None,
    ) -> None: ...

    def __init__(
        self,
        *,
        base: TVCell | None = None,
        name: str | None = None,
        kcl: KCLayout | None = None,
        info: dict[str, Any] | None = None,
        settings: dict[str, Any] | None = None,
    ) -> None:
        from .layout import get_default_kcl

        if base is not None:
            self._base = base
        else:
            kcl_ = kcl or get_default_kcl()
            self._base = TVCell(
                kcl=kcl_,
                info=Info(**(info or {})),
                settings=KCellSettings(**(settings or {})),
                vinsts=VInstances(),
            )
            if name:
                self._base.name = name

    def ibbox(self, layer: int | None = None) -> kdb.Box:
        return self.dbbox(layer).to_itype(self.kcl.dbu)

    def transform(
        self,
        trans: kdb.Trans | kdb.DTrans | kdb.ICplxTrans | kdb.DCplxTrans,
        /,
    ) -> None:
        shapes = self.base.shapes
        for key, vshape in shapes.items():
            shapes[key] = vshape.transform(trans)

    @property
    def ports(self) -> DPorts:
        """Ports associated with the cell."""
        return DPorts(kcl=self.kcl, bases=self._base.ports)

    @ports.setter
    def ports(self, new_ports: Iterable[ProtoPort[Any]]) -> None:
        if self.locked:
            raise LockedError(self)
        self._base.ports = [port.base for port in new_ports]

    @property
    def pins(self) -> DPins:
        """Ports associated with the cell."""
        return DPins(kcl=self.kcl, bases=self._base.pins)

    @pins.setter
    def pins(self, new_pins: Iterable[ProtoPin[Any]]) -> None:
        if self.locked:
            raise LockedError(self)
        self._base.pins = [pin.base for pin in new_pins]

    def dbbox(self, layer: int | LayerEnum | None = None) -> kdb.DBox:
        layers_ = set(self.shapes().keys())

        layers = layers_ if layer is None else {layer} & layers_
        box = kdb.DBox()
        for layer_ in layers:
            layer__ = layer_
            if isinstance(layer__, LayerEnum):
                layer__ = layer__.layout.layer(layer__.layer, layer__.datatype)
            box += self.shapes(layer__).bbox()

        for vinst in self.insts:
            box += vinst.dbbox()

        return box

    def __getitem__(self, key: int | str | None) -> DPort:
        """Returns port from instance."""
        return self.ports[key]

    @property
    def insts(self) -> VInstances:
        return self._base.vinsts

    def dup(self, new_name: str | None = None) -> Self:
        """Copy the full cell.

        Removes lock if the original cell was locked.

        Returns:
            cell: Exact copy of the current cell.
                The name will have `$1` as duplicate names are not allowed
        """
        c = self.__class__(
            kcl=self.kcl, name=new_name or self.name + "$1" if self.name else None
        )
        c.ports = DPorts(kcl=self.kcl, ports=self.ports.copy())

        c.settings = self.settings.model_copy()
        c.settings_units = self.settings_units.model_copy()
        c.info = self._base.info.model_copy()
        for layer, shapes in self.shapes().items():
            for shape in shapes:
                c.shapes(layer).insert(shape)

        return c

    def show(
        self,
        lyrdb: rdb.ReportDatabase | Path | str | None = None,
        l2n: kdb.LayoutToNetlist | Path | str | None = None,
        keep_position: bool = True,
        save_options: kdb.SaveLayoutOptions | None = None,
        use_libraries: bool = True,
        library_save_options: kdb.SaveLayoutOptions | None = None,
        technology: str | None = None,
    ) -> None:
        """Stream the gds to klive.

        Will create a temporary file of the gds and load it in KLayout via klive
        """
        if save_options is None:
            save_options = save_layout_options()
        if library_save_options is None:
            library_save_options = save_layout_options()
        c = self.kcl.kcell()
        if self.name is not None:
            c.name = self.name
        VInstance(self).insert_into_flat(c, levels=0)
        c.add_ports(self.ports)
        c.info = self.info.model_copy()
        c.base.settings = self.settings.model_copy()

        kwargs: dict[str, Any] = {}
        if technology is not None:
            kwargs["technology"] = technology
        if l2n is not None:
            kwargs["l2n"] = l2n
        if lyrdb is not None:
            kwargs["lyrdb"] = lyrdb

        c.show(keep_position=keep_position, **kwargs)

    def plot(self) -> None:
        """Display cell.

        Usage: Pass the vkcell variable as an argument in the cell at the end
        """
        from .widgets.interactive import display_kcell

        c = self.kcl.kcell()
        if self.name is not None:
            c.name = self.name
        VInstance(self).insert_into_flat(c, levels=0)

        display_kcell(c)
        c.delete()

    def _ipython_display_(self) -> None:
        """Display a cell in a Jupyter Cell.

        Usage: Pass the kcell variable as an argument in the cell at the end
        """
        self.plot()

    def __repr__(self) -> str:
        """Return a string representation of the Cell."""
        port_names = [p.name for p in self.ports]
        pin_names = [pin.name for pin in self.pins]
        return (
            f"{self.name}: ports {port_names}, pins {pin_names}, {len(self.insts)} "
            "instances"
        )

    def add_port(
        self,
        *,
        port: ProtoPort[Any],
        name: str | None = None,
        keep_mirror: bool = False,
    ) -> DPort:
        """Proxy for [Ports.create_port][kfactory.kcell.Ports.create_port]."""
        if self.locked:
            raise LockedError(self)
        return self.ports.add_port(
            port=port,
            name=name,
            keep_mirror=keep_mirror,
        )

    def create_inst(
        self, cell: AnyKCell, trans: kdb.DCplxTrans | None = None
    ) -> VInstance:
        if self.locked:
            raise LockedError(self)
        inst = VInstance(cell=cell, trans=trans or kdb.DCplxTrans())
        self.insts.append(inst)
        return inst

    def auto_rename_ports(self, rename_func: Callable[..., None] | None = None) -> None:
        """Rename the ports with the schema angle -> "NSWE" and sort by x and y.

        Args:
            rename_func: Function that takes Iterable[Port] and renames them.
                This can of course contain a filter and only rename some of the ports
        """
        if self.locked:
            raise LockedError(self)
        if rename_func is None:
            self.kcl.rename_function(self.ports)
        else:
            rename_func(self.ports)

    def __lshift__(self, cell: AnyKCell) -> VInstance:
        return self.create_inst(cell=cell)

    @overload
    def shapes(self, layer: None = ...) -> dict[int, VShapes]: ...

    @overload
    def shapes(self, layer: int | kdb.LayerInfo) -> VShapes: ...

    def shapes(
        self, layer: int | kdb.LayerInfo | None = None
    ) -> VShapes | dict[int, VShapes]:
        if layer is None:
            return self._base.shapes
        if isinstance(layer, kdb.LayerInfo):
            layer = self.kcl.layout.layer(layer)
        if layer not in self._base.shapes:
            self._base.shapes[layer] = VShapes(cell=self)
        return self._base.shapes[layer]

    def flatten(self) -> None:
        if self.locked:
            raise LockedError(self)
        for inst in self.insts:
            inst.insert_into_flat(self, inst.trans)

    def draw_ports(self) -> None:
        """Draw all the ports on their respective layer."""
        polys: dict[float, kdb.DPolygon] = {}

        for port in self.ports:
            w = port.width

            if w in polys:
                poly = polys[w]
            else:
                if w < 2:  # noqa: PLR2004
                    poly = kdb.DPolygon(
                        [
                            kdb.DPoint(0, -w / 2),
                            kdb.DPoint(0, w / 2),
                            kdb.DPoint(w / 2, 0),
                        ]
                    )
                else:
                    poly = kdb.DPolygon(
                        [
                            kdb.DPoint(0, -w / 2),
                            kdb.DPoint(0, w / 2),
                            kdb.DPoint(w / 2, 0),
                            kdb.DPoint(w * 19 / 20, 0),
                            kdb.DPoint(w / 20, w * 9 / 20),
                            kdb.DPoint(w / 20, -w * 9 / 20),
                            kdb.DPoint(w * 19 / 20, 0),
                            kdb.DPoint(w / 2, 0),
                        ]
                    )
                polys[w] = poly
            self.shapes(port.layer).insert(poly.transformed(port.dcplx_trans))
            self.shapes(port.layer).insert(kdb.Text(port.name or "", port.trans))

    def write(
        self,
        filename: str | Path,
        save_options: kdb.SaveLayoutOptions | None = None,
        convert_external_cells: bool = False,
        set_meta_data: bool = True,
        autoformat_from_file_extension: bool = True,
    ) -> None:
        """Write a KCell to a GDS.

        See [KCLayout.write][kfactory.kcell.KCLayout.write] for more info.
        """
        if save_options is None:
            save_options = save_layout_options()
        c = self.kcl.kcell()
        if self.name is not None:
            c.name = self.name
        c.settings = self.settings
        c.settings_units = self.settings_units
        c.info = self.info
        VInstance(self).insert_into_flat(c, levels=1)

        c.write(
            filename=filename,
            save_options=save_options,
            convert_external_cells=convert_external_cells,
            set_meta_data=set_meta_data,
            autoformat_from_file_extension=autoformat_from_file_extension,
        )

    def l2n(
        self, port_types: Iterable[str] = ("optical",)
    ) -> tuple[KCell, kdb.LayoutToNetlist]:
        """Generate a LayoutToNetlist object from the port types.

        Args:
            port_types: The port types to consider for the netlist extraction.
        """
        c = self.kcl.kcell()
        if self.name is not None:
            c.name = self.name
        c.settings = self.settings
        c.settings_units = self.settings_units
        c.info = self.info
        VInstance(self).insert_into(c)
        return c, c.l2n()

    def connectivity_check(
        self,
        port_types: list[str] | None = None,
        layers: list[int] | None = None,
        db: rdb.ReportDatabase | None = None,
        recursive: bool = True,
        add_cell_ports: bool = False,
        check_layer_connectivity: bool = True,
    ) -> tuple[KCell, rdb.ReportDatabase]:
        if layers is None:
            layers = []
        if port_types is None:
            port_types = []
        c = self.kcl.kcell()
        if self.name is not None:
            c.name = self.name
        c.settings = self.settings
        c.settings_units = self.settings_units
        c.info = self.info
        VInstance(self).insert_into_flat(c, levels=0)
        return c, c.connectivity_check(
            port_types=port_types,
            layers=layers,
            db=db,
            recursive=recursive,
            add_cell_ports=add_cell_ports,
            check_layer_connectivity=check_layer_connectivity,
        )

pins property writable

pins: DPins

Ports associated with the cell.

ports property writable

ports: DPorts

Ports associated with the cell.

__getitem__

__getitem__(key: int | str | None) -> DPort

Returns port from instance.

Source code in kfactory/kcell.py
3533
3534
3535
def __getitem__(self, key: int | str | None) -> DPort:
    """Returns port from instance."""
    return self.ports[key]

__repr__

__repr__() -> str

Return a string representation of the Cell.

Source code in kfactory/kcell.py
3622
3623
3624
3625
3626
3627
3628
3629
def __repr__(self) -> str:
    """Return a string representation of the Cell."""
    port_names = [p.name for p in self.ports]
    pin_names = [pin.name for pin in self.pins]
    return (
        f"{self.name}: ports {port_names}, pins {pin_names}, {len(self.insts)} "
        "instances"
    )

add_port

add_port(
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> DPort

Proxy for Ports.create_port.

Source code in kfactory/kcell.py
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
def add_port(
    self,
    *,
    port: ProtoPort[Any],
    name: str | None = None,
    keep_mirror: bool = False,
) -> DPort:
    """Proxy for [Ports.create_port][kfactory.kcell.Ports.create_port]."""
    if self.locked:
        raise LockedError(self)
    return self.ports.add_port(
        port=port,
        name=name,
        keep_mirror=keep_mirror,
    )

auto_rename_ports

auto_rename_ports(
    rename_func: Callable[..., None] | None = None,
) -> None

Rename the ports with the schema angle -> "NSWE" and sort by x and y.

Parameters:

Name Type Description Default
rename_func Callable[..., None] | None

Function that takes Iterable[Port] and renames them. This can of course contain a filter and only rename some of the ports

None
Source code in kfactory/kcell.py
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
def auto_rename_ports(self, rename_func: Callable[..., None] | None = None) -> None:
    """Rename the ports with the schema angle -> "NSWE" and sort by x and y.

    Args:
        rename_func: Function that takes Iterable[Port] and renames them.
            This can of course contain a filter and only rename some of the ports
    """
    if self.locked:
        raise LockedError(self)
    if rename_func is None:
        self.kcl.rename_function(self.ports)
    else:
        rename_func(self.ports)

draw_ports

draw_ports() -> None

Draw all the ports on their respective layer.

Source code in kfactory/kcell.py
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
def draw_ports(self) -> None:
    """Draw all the ports on their respective layer."""
    polys: dict[float, kdb.DPolygon] = {}

    for port in self.ports:
        w = port.width

        if w in polys:
            poly = polys[w]
        else:
            if w < 2:  # noqa: PLR2004
                poly = kdb.DPolygon(
                    [
                        kdb.DPoint(0, -w / 2),
                        kdb.DPoint(0, w / 2),
                        kdb.DPoint(w / 2, 0),
                    ]
                )
            else:
                poly = kdb.DPolygon(
                    [
                        kdb.DPoint(0, -w / 2),
                        kdb.DPoint(0, w / 2),
                        kdb.DPoint(w / 2, 0),
                        kdb.DPoint(w * 19 / 20, 0),
                        kdb.DPoint(w / 20, w * 9 / 20),
                        kdb.DPoint(w / 20, -w * 9 / 20),
                        kdb.DPoint(w * 19 / 20, 0),
                        kdb.DPoint(w / 2, 0),
                    ]
                )
            polys[w] = poly
        self.shapes(port.layer).insert(poly.transformed(port.dcplx_trans))
        self.shapes(port.layer).insert(kdb.Text(port.name or "", port.trans))

dup

dup(new_name: str | None = None) -> Self

Copy the full cell.

Removes lock if the original cell was locked.

Returns:

Name Type Description
cell Self

Exact copy of the current cell. The name will have $1 as duplicate names are not allowed

Source code in kfactory/kcell.py
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
def dup(self, new_name: str | None = None) -> Self:
    """Copy the full cell.

    Removes lock if the original cell was locked.

    Returns:
        cell: Exact copy of the current cell.
            The name will have `$1` as duplicate names are not allowed
    """
    c = self.__class__(
        kcl=self.kcl, name=new_name or self.name + "$1" if self.name else None
    )
    c.ports = DPorts(kcl=self.kcl, ports=self.ports.copy())

    c.settings = self.settings.model_copy()
    c.settings_units = self.settings_units.model_copy()
    c.info = self._base.info.model_copy()
    for layer, shapes in self.shapes().items():
        for shape in shapes:
            c.shapes(layer).insert(shape)

    return c

l2n

l2n(
    port_types: Iterable[str] = ("optical",),
) -> tuple[KCell, kdb.LayoutToNetlist]

Generate a LayoutToNetlist object from the port types.

Parameters:

Name Type Description Default
port_types Iterable[str]

The port types to consider for the netlist extraction.

('optical',)
Source code in kfactory/kcell.py
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
def l2n(
    self, port_types: Iterable[str] = ("optical",)
) -> tuple[KCell, kdb.LayoutToNetlist]:
    """Generate a LayoutToNetlist object from the port types.

    Args:
        port_types: The port types to consider for the netlist extraction.
    """
    c = self.kcl.kcell()
    if self.name is not None:
        c.name = self.name
    c.settings = self.settings
    c.settings_units = self.settings_units
    c.info = self.info
    VInstance(self).insert_into(c)
    return c, c.l2n()

plot

plot() -> None

Display cell.

Usage: Pass the vkcell variable as an argument in the cell at the end

Source code in kfactory/kcell.py
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
def plot(self) -> None:
    """Display cell.

    Usage: Pass the vkcell variable as an argument in the cell at the end
    """
    from .widgets.interactive import display_kcell

    c = self.kcl.kcell()
    if self.name is not None:
        c.name = self.name
    VInstance(self).insert_into_flat(c, levels=0)

    display_kcell(c)
    c.delete()

show

show(
    lyrdb: ReportDatabase | Path | str | None = None,
    l2n: LayoutToNetlist | Path | str | None = None,
    keep_position: bool = True,
    save_options: SaveLayoutOptions | None = None,
    use_libraries: bool = True,
    library_save_options: SaveLayoutOptions | None = None,
    technology: str | None = None,
) -> None

Stream the gds to klive.

Will create a temporary file of the gds and load it in KLayout via klive

Source code in kfactory/kcell.py
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
def show(
    self,
    lyrdb: rdb.ReportDatabase | Path | str | None = None,
    l2n: kdb.LayoutToNetlist | Path | str | None = None,
    keep_position: bool = True,
    save_options: kdb.SaveLayoutOptions | None = None,
    use_libraries: bool = True,
    library_save_options: kdb.SaveLayoutOptions | None = None,
    technology: str | None = None,
) -> None:
    """Stream the gds to klive.

    Will create a temporary file of the gds and load it in KLayout via klive
    """
    if save_options is None:
        save_options = save_layout_options()
    if library_save_options is None:
        library_save_options = save_layout_options()
    c = self.kcl.kcell()
    if self.name is not None:
        c.name = self.name
    VInstance(self).insert_into_flat(c, levels=0)
    c.add_ports(self.ports)
    c.info = self.info.model_copy()
    c.base.settings = self.settings.model_copy()

    kwargs: dict[str, Any] = {}
    if technology is not None:
        kwargs["technology"] = technology
    if l2n is not None:
        kwargs["l2n"] = l2n
    if lyrdb is not None:
        kwargs["lyrdb"] = lyrdb

    c.show(keep_position=keep_position, **kwargs)

write

write(
    filename: str | Path,
    save_options: SaveLayoutOptions | None = None,
    convert_external_cells: bool = False,
    set_meta_data: bool = True,
    autoformat_from_file_extension: bool = True,
) -> None

Write a KCell to a GDS.

See KCLayout.write for more info.

Source code in kfactory/kcell.py
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
def write(
    self,
    filename: str | Path,
    save_options: kdb.SaveLayoutOptions | None = None,
    convert_external_cells: bool = False,
    set_meta_data: bool = True,
    autoformat_from_file_extension: bool = True,
) -> None:
    """Write a KCell to a GDS.

    See [KCLayout.write][kfactory.kcell.KCLayout.write] for more info.
    """
    if save_options is None:
        save_options = save_layout_options()
    c = self.kcl.kcell()
    if self.name is not None:
        c.name = self.name
    c.settings = self.settings
    c.settings_units = self.settings_units
    c.info = self.info
    VInstance(self).insert_into_flat(c, levels=1)

    c.write(
        filename=filename,
        save_options=save_options,
        convert_external_cells=convert_external_cells,
        set_meta_data=set_meta_data,
        autoformat_from_file_extension=autoformat_from_file_extension,
    )

VShapes

Emulate [klayout.db.Shapes][klayout.db.Shapes].

Source code in kfactory/shapes.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class VShapes:
    """Emulate `[klayout.db.Shapes][klayout.db.Shapes]`."""

    cell: VKCell
    _shapes: list[ShapeLike]
    _bbox: kdb.DBox

    def __init__(
        self, cell: VKCell, _shapes: Sequence[ShapeLike] | None = None
    ) -> None:
        """Initialize the shapes."""
        self.cell = cell
        self._shapes = list(_shapes) if _shapes is not None else []
        self._bbox = kdb.DBox()

    def insert(self, shape: ShapeLike) -> None:
        """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s insert'`."""
        if (
            isinstance(shape, kdb.Shape)
            and shape.cell.layout().dbu != self.cell.kcl.dbu
        ):
            raise ValueError
        if isinstance(shape, kdb.DBox):
            shape = kdb.DPolygon(shape)
        elif isinstance(shape, kdb.Box):
            shape = self.cell.kcl.to_um(shape)
        self._shapes.append(shape)
        b = shape.bbox()
        if isinstance(b, kdb.Box):
            self._bbox += self.cell.kcl.to_um(b)
        else:
            self._bbox += b

    def bbox(self) -> kdb.DBox:
        """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s bbox'`."""
        return self._bbox.dup()

    def __iter__(self) -> Iterator[ShapeLike]:
        """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s __iter__'`."""
        yield from self._shapes

    def each(self) -> Iterator[ShapeLike]:
        """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s each'`."""
        yield from self._shapes

    def transform(
        self,
        trans: kdb.Trans | kdb.DTrans | kdb.ICplxTrans | kdb.DCplxTrans,
        /,
    ) -> VShapes:
        """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s transform'`."""
        new_shapes: list[DShapeLike] = []
        if isinstance(trans, kdb.Trans):
            trans = trans.to_dtype(self.cell.kcl.dbu)
        elif isinstance(trans, kdb.ICplxTrans):
            trans = trans.to_itrans(self.cell.kcl.dbu)

        for shape in self._shapes:
            if isinstance(shape, DShapeLike):
                new_shapes.append(shape.transformed(trans))
            elif isinstance(shape, IShapeLike):
                if isinstance(shape, kdb.Region):
                    new_shapes.extend(
                        poly.to_dtype(self.cell.kcl.dbu) for poly in shape.each()
                    )
                else:
                    new_shapes.append(
                        shape.to_dtype(self.cell.kcl.dbu).transformed(trans)
                    )
            else:
                new_shapes.append(shape.dpolygon.transform(trans))

        return VShapes(cell=self.cell, _shapes=new_shapes)

    def size(self) -> int:
        """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s size'`."""
        return len(self._shapes)

__init__

__init__(
    cell: VKCell, _shapes: Sequence[ShapeLike] | None = None
) -> None

Initialize the shapes.

Source code in kfactory/shapes.py
24
25
26
27
28
29
30
def __init__(
    self, cell: VKCell, _shapes: Sequence[ShapeLike] | None = None
) -> None:
    """Initialize the shapes."""
    self.cell = cell
    self._shapes = list(_shapes) if _shapes is not None else []
    self._bbox = kdb.DBox()

__iter__

__iter__() -> Iterator[ShapeLike]

Emulate [klayout.db.Shapes][klayout.db.Shapes]'s __iter__'.

Source code in kfactory/shapes.py
54
55
56
def __iter__(self) -> Iterator[ShapeLike]:
    """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s __iter__'`."""
    yield from self._shapes

bbox

bbox() -> kdb.DBox

Emulate [klayout.db.Shapes][klayout.db.Shapes]'s bbox'.

Source code in kfactory/shapes.py
50
51
52
def bbox(self) -> kdb.DBox:
    """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s bbox'`."""
    return self._bbox.dup()

each

each() -> Iterator[ShapeLike]

Emulate [klayout.db.Shapes][klayout.db.Shapes]'s each'.

Source code in kfactory/shapes.py
58
59
60
def each(self) -> Iterator[ShapeLike]:
    """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s each'`."""
    yield from self._shapes

insert

insert(shape: ShapeLike) -> None

Emulate [klayout.db.Shapes][klayout.db.Shapes]'s insert'.

Source code in kfactory/shapes.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def insert(self, shape: ShapeLike) -> None:
    """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s insert'`."""
    if (
        isinstance(shape, kdb.Shape)
        and shape.cell.layout().dbu != self.cell.kcl.dbu
    ):
        raise ValueError
    if isinstance(shape, kdb.DBox):
        shape = kdb.DPolygon(shape)
    elif isinstance(shape, kdb.Box):
        shape = self.cell.kcl.to_um(shape)
    self._shapes.append(shape)
    b = shape.bbox()
    if isinstance(b, kdb.Box):
        self._bbox += self.cell.kcl.to_um(b)
    else:
        self._bbox += b

size

size() -> int

Emulate [klayout.db.Shapes][klayout.db.Shapes]'s size'.

Source code in kfactory/shapes.py
91
92
93
def size(self) -> int:
    """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s size'`."""
    return len(self._shapes)

transform

transform(
    trans: Trans | DTrans | ICplxTrans | DCplxTrans,
) -> VShapes

Emulate [klayout.db.Shapes][klayout.db.Shapes]'s transform'.

Source code in kfactory/shapes.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def transform(
    self,
    trans: kdb.Trans | kdb.DTrans | kdb.ICplxTrans | kdb.DCplxTrans,
    /,
) -> VShapes:
    """Emulate `[klayout.db.Shapes][klayout.db.Shapes]'s transform'`."""
    new_shapes: list[DShapeLike] = []
    if isinstance(trans, kdb.Trans):
        trans = trans.to_dtype(self.cell.kcl.dbu)
    elif isinstance(trans, kdb.ICplxTrans):
        trans = trans.to_itrans(self.cell.kcl.dbu)

    for shape in self._shapes:
        if isinstance(shape, DShapeLike):
            new_shapes.append(shape.transformed(trans))
        elif isinstance(shape, IShapeLike):
            if isinstance(shape, kdb.Region):
                new_shapes.extend(
                    poly.to_dtype(self.cell.kcl.dbu) for poly in shape.each()
                )
            else:
                new_shapes.append(
                    shape.to_dtype(self.cell.kcl.dbu).transformed(trans)
                )
        else:
            new_shapes.append(shape.dpolygon.transform(trans))

    return VShapes(cell=self.cell, _shapes=new_shapes)

dpolygon_from_array

dpolygon_from_array(
    array: Iterable[tuple[float, float]],
) -> kdb.DPolygon

Create a DPolygon from a 2D array-like structure. (um version).

Array-like: [[x1,y1],[x2,y2],...]

Source code in kfactory/utilities.py
75
76
77
78
79
80
def dpolygon_from_array(array: Iterable[tuple[float, float]]) -> kdb.DPolygon:
    """Create a DPolygon from a 2D array-like structure. (um version).

    Array-like: `[[x1,y1],[x2,y2],...]`
    """
    return kdb.DPolygon([kdb.DPoint(x, y) for (x, y) in array])

flexgrid

flexgrid(
    target: DKCell,
    kcells: Sequence[DKCell | None]
    | Sequence[Sequence[DKCell | None]],
    spacing: float | tuple[float, float],
    target_trans: DCplxTrans | None = None,
    shape: tuple[int, int] | None = None,
    align_x: Literal[
        "origin", "xmin", "xmax", "center"
    ] = "center",
    align_y: Literal[
        "origin", "ymin", "ymax", "center"
    ] = "center",
    rotation: float = 0,
    mirror: bool = False,
) -> DInstanceGroup

Create a grid of instances.

A grid uses the bounding box of the biggest width per column and biggest height per row of any bounding boxes inserted into the grid. To this bounding box a spacing is applied in x and y.

                     spacing[0] or spacing
                    ◄─►
 ┌──────────────────┐ ┌──┬─────┬──┐ ┌──────────┐ ┌──────────┐▲
 │                  │ │  │     │  │ │          │ │          ││
 │   ┌────┐         │ │  │     │  │ │          │ │   ┌──────┼│
 │   │    │         │ │  │     │  │ │          │ │   │      ││
 │   │    │         │ │  │ big │  │ │  ┌───────┤ │   │      ││
 │   │ ▼  │         │ │  │ comp│  │ │  │       │ │   │      ││
 │   │    │         │ │  │ y   │  │ │  │       │ │   │  ▼   ││
 │   │    │         │ │  │     │  │ │  │  ▼    │ │   │      ││ y[1]
 │   └────┘         │ │  │ ▼   │  │ │  │       │ │   │      ││
 │                  │ │  │     │  │ │  │       │ │   │      ││
 │                  │ │  │     │  │ │  │       │ │   └──────┼│
 │                  │ │  │     │  │ │  │       │ │          ││
 │                  │ │  │     │  │ │  └───────┤ │          ││
 │                  │ │  │     │  │ │          │ │          ││
▲└──────────────────┘ └──┴─────┴──┘ └──────────┘ └──────────┘▼
│spacing[1] or spacing
▼┌──────────────────┐ ┌───────────┐ ┌────┬─────┐ ┌───┬──────┐▲
 ├──────────────────┤ ├───────────┤ │    │     │ │   │      ││
 │                  │ │           │ │    │     │ │   │      ││
 │              ▼   │ │           │ │    │     │ │   │      ││
 │   big comp x     │ │           │ │    │     │ │   │      ││
 │                  │ │    ▼      │ │  ▼ │     │ │ ▼ │      ││ y[0]
 ├──────────────────┤ │           │ │    │     │ │   │      ││
 │                  │ │           │ │    │     │ │   │      ││
 │                  │ │           │ │    │     │ │   │      ││
 │                  │ │           │ │    │     │ │   │      ││
 └──────────────────┴─┴───────────┴─┴────┼─────┴─┴───┼──────┘►
 ►──────────────────► ►───────────► ►──────────► ►──────────►
         x[0]               x[1]        x[2]         x[3]

Parameters:

Name Type Description Default
target DKCell

Target DKCell.

required
kcells Sequence[DKCell | None] | Sequence[Sequence[DKCell | None]]

Sequence or sequence of sequence of DKCells to add to the grid

required
spacing float | tuple[float, float]

Value or tuple of value (different x/y) for spacing of the grid.

required
target_trans DCplxTrans | None

Apply a transformation to the whole grid before placing it.

None
shape tuple[int, int] | None

Respace the input of kcells into an array and fill as many positions (first x then y).

None
align_x Literal['origin', 'xmin', 'xmax', 'center']

Align all the instance on the x-coordinate.

'center'
align_y Literal['origin', 'ymin', 'ymax', 'center']

Align all the instance on the y-coordinate.

'center'
rotation float

Apply a rotation to each kcells instance before adding it to the grid.

0
mirror bool

Mirror the instances before placing them in the grid

False
Source code in kfactory/grid.py
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
def flexgrid(
    target: DKCell,
    kcells: Sequence[DKCell | None] | Sequence[Sequence[DKCell | None]],
    spacing: float | tuple[float, float],
    target_trans: kdb.DCplxTrans | None = None,
    shape: tuple[int, int] | None = None,
    align_x: Literal["origin", "xmin", "xmax", "center"] = "center",
    align_y: Literal["origin", "ymin", "ymax", "center"] = "center",
    rotation: float = 0,
    mirror: bool = False,
) -> DInstanceGroup:
    """Create a grid of instances.

    A grid uses the bounding box of the biggest width per column and biggest height per
    row of any bounding boxes inserted into the grid.
    To this bounding box a spacing is applied in x and y.

    ```
                         spacing[0] or spacing
                        ◄─►
     ┌──────────────────┐ ┌──┬─────┬──┐ ┌──────────┐ ┌──────────┐▲
     │                  │ │  │     │  │ │          │ │          ││
     │   ┌────┐         │ │  │     │  │ │          │ │   ┌──────┼│
     │   │    │         │ │  │     │  │ │          │ │   │      ││
     │   │    │         │ │  │ big │  │ │  ┌───────┤ │   │      ││
     │   │ ▼  │         │ │  │ comp│  │ │  │       │ │   │      ││
     │   │    │         │ │  │ y   │  │ │  │       │ │   │  ▼   ││
     │   │    │         │ │  │     │  │ │  │  ▼    │ │   │      ││ y[1]
     │   └────┘         │ │  │ ▼   │  │ │  │       │ │   │      ││
     │                  │ │  │     │  │ │  │       │ │   │      ││
     │                  │ │  │     │  │ │  │       │ │   └──────┼│
     │                  │ │  │     │  │ │  │       │ │          ││
     │                  │ │  │     │  │ │  └───────┤ │          ││
     │                  │ │  │     │  │ │          │ │          ││
    ▲└──────────────────┘ └──┴─────┴──┘ └──────────┘ └──────────┘▼
    │spacing[1] or spacing
    ▼┌──────────────────┐ ┌───────────┐ ┌────┬─────┐ ┌───┬──────┐▲
     ├──────────────────┤ ├───────────┤ │    │     │ │   │      ││
     │                  │ │           │ │    │     │ │   │      ││
     │              ▼   │ │           │ │    │     │ │   │      ││
     │   big comp x     │ │           │ │    │     │ │   │      ││
     │                  │ │    ▼      │ │  ▼ │     │ │ ▼ │      ││ y[0]
     ├──────────────────┤ │           │ │    │     │ │   │      ││
     │                  │ │           │ │    │     │ │   │      ││
     │                  │ │           │ │    │     │ │   │      ││
     │                  │ │           │ │    │     │ │   │      ││
     └──────────────────┴─┴───────────┴─┴────┼─────┴─┴───┼──────┘►
     ►──────────────────► ►───────────► ►──────────► ►──────────►
             x[0]               x[1]        x[2]         x[3]
    ```

    Args:
        target: Target DKCell.
        kcells: Sequence or sequence of sequence of DKCells to add to the grid
        spacing: Value or tuple of value (different x/y) for spacing of the grid.
        target_trans: Apply a transformation to the whole grid before placing it.
        shape: Respace the input of kcells into an array and fill as many positions
            (first x then y).
        align_x: Align all the instance on the x-coordinate.
        align_y: Align all the instance on the y-coordinate.
        rotation: Apply a rotation to each kcells instance before adding it to the grid.
        mirror: Mirror the instances before placing them in the grid

    """
    if isinstance(spacing, tuple):
        spacing_x, spacing_y = spacing
    else:
        spacing_x = spacing
        spacing_y = spacing

    if target_trans is None:
        target_trans = kdb.DCplxTrans()

    insts: list[list[DInstance | None]]
    kcell_array: Sequence[Sequence[DKCell]]

    if shape is None:
        if isinstance(kcells[0], DKCell):
            kcell_array = cast("Sequence[list[DKCell]]", [list(kcells)])
        else:
            kcell_array = cast("Sequence[Sequence[DKCell]]", kcells)

        x0: float = 0
        y0: float = 0

        insts = [
            [
                None
                if kcell is None
                else target.create_inst(
                    kcell, kdb.DCplxTrans(1, rotation, mirror, 0, 0)
                )
                for kcell in array
            ]
            for array in kcell_array
        ]
        bboxes = [
            [None if inst is None else inst.dbbox() for inst in array]
            for array in insts
        ]
        xmin: dict[int, float] = {}
        ymin: dict[int, float] = {}
        ymax: dict[int, float] = {}
        xmax: dict[int, float] = {}
        for i_y, (array, box_array) in enumerate(zip(insts, bboxes, strict=False)):
            for i_x, (inst, bbox) in enumerate(zip(array, box_array, strict=False)):
                if inst is not None and bbox is not None:
                    match align_x:
                        case "xmin":
                            x = -bbox.left
                        case "xmax":
                            x = -bbox.right
                        case "center":
                            x = -bbox.center().x
                        case _:
                            x = 0
                    match align_y:
                        case "ymin":
                            y = -bbox.bottom
                        case "ymax":
                            y = -bbox.top
                        case "center":
                            y = -bbox.center().y
                        case _:
                            y = 0
                    at = kdb.DCplxTrans(x, y)
                    inst.dcplx_trans = at * inst.dcplx_trans
                    bbox_ = inst.dbbox()
                    xmin[i_x] = min(xmin.get(i_x) or bbox_.left, bbox_.left - spacing_x)
                    xmax[i_x] = max(xmax.get(i_x) or bbox_.right, bbox_.right)
                    ymin[i_y] = min(
                        ymin.get(i_y) or bbox_.bottom, bbox_.bottom - spacing_y
                    )
                    ymax[i_y] = max(ymax.get(i_y) or bbox_.top, bbox_.top)

        for i_y, (array, bbox_array) in enumerate(zip(insts, bboxes, strict=False)):
            y0 -= ymin.get(i_y, 0)
            for i_x, (bbox, inst) in enumerate(zip(bbox_array, array, strict=False)):
                x0 -= xmin.get(i_x, 0)
                if inst is not None and bbox is not None:
                    at = kdb.DCplxTrans(x0, y0)
                    inst.transform(target_trans * at)
                x0 += xmax.get(i_x, 0)
            y0 += ymax.get(i_y, 0)
            x0 = 0
        return DInstanceGroup(
            [inst for array in insts for inst in array if inst is not None]
        )
    _kcells: Sequence[DKCell | None]
    if isinstance(kcells[0], DKCell):
        _kcells = cast("Sequence[DKCell | None]", kcells)
    else:
        _kcells = [
            kcell
            for array in cast("Sequence[Sequence[DKCell | None]]", kcells)
            for kcell in array
        ]

    if len(_kcells) > shape[0] * shape[1]:
        raise ValueError(
            f"Shape container size {shape[0] * shape[1]=} must be bigger "
            f"than the number of kcells {len(_kcells)}"
        )

    x0 = 0
    y0 = 0

    _insts = [
        None
        if kcell is None
        else target.create_inst(kcell, kdb.DCplxTrans(1, rotation, mirror, 0, 0))
        for kcell in _kcells
    ]

    xmin = {}
    ymin = {}
    ymax = {}
    xmax = {}
    for i, inst in enumerate(_insts):
        i_x = i % shape[1]
        i_y = i // shape[1]

        if inst is not None:
            bbox = inst.dbbox()
            match align_x:
                case "xmin":
                    x = -bbox.left
                case "xmax":
                    x = -bbox.right
                case "center":
                    x = -bbox.center().x
                case _:
                    x = 0
            match align_y:
                case "ymin":
                    y = -bbox.bottom
                case "ymax":
                    y = -bbox.top
                case "center":
                    y = -bbox.center().y
                case _:
                    y = 0
            at = kdb.DCplxTrans(x, y)
            inst.dcplx_trans = at * inst.dcplx_trans
            bbox = inst.dbbox()
            xmin[i_x] = min(xmin.get(i_x) or bbox.left, bbox.left - spacing_x)
            xmax[i_x] = max(xmax.get(i_x) or bbox.right, bbox.right)
            ymin[i_y] = min(ymin.get(i_y) or bbox.bottom, bbox.bottom - spacing_y)
            ymax[i_y] = max(ymax.get(i_y) or bbox.top, bbox.top)

    insts = [[None] * shape[1] for _ in range(shape[0])]
    for i, inst in enumerate(_insts):
        i_x = i % shape[1]
        i_y = i // shape[1]
        if i_x == 0:
            y0 -= ymin.get(i_y, 0)
            x0 = 0
        else:
            x0 -= xmin.get(i_x, 0)

        if inst is not None:
            at = kdb.DCplxTrans(x0, y0)
            inst.transform(target_trans * at)
            insts[i_y][i_x] = inst
        if i_x == shape[1] - 1:
            y0 += ymax.get(i_y, 0)
            x0 = 0
        else:
            x0 += xmax.get(i_x, 0)
    return DInstanceGroup(
        [inst for array in insts for inst in array if inst is not None]
    )

flexgrid_dbu

flexgrid_dbu(
    target: KCell,
    kcells: Sequence[KCell | None]
    | Sequence[Sequence[KCell | None]],
    spacing: int | tuple[int, int],
    target_trans: Trans | None = None,
    shape: tuple[int, int] | None = None,
    align_x: Literal[
        "origin", "xmin", "xmax", "center"
    ] = "center",
    align_y: Literal[
        "origin", "ymin", "ymax", "center"
    ] = "center",
    rotation: Literal[0, 1, 2, 3] = 0,
    mirror: bool = False,
) -> InstanceGroup

Create a grid of instances.

A grid uses the bounding box of the biggest width per column and biggest height per row of any bounding boxes inserted into the grid. To this bounding box a spacing is applied in x and y.

                     spacing[0] or spacing
                    ◄─►
 ┌──────────────────┐ ┌──┬─────┬──┐ ┌──────────┐ ┌──────────┐▲
 │                  │ │  │     │  │ │          │ │          ││
 │   ┌────┐         │ │  │     │  │ │          │ │   ┌──────┼│
 │   │    │         │ │  │     │  │ │          │ │   │      ││
 │   │    │         │ │  │ big │  │ │  ┌───────┤ │   │      ││
 │   │ ▼  │         │ │  │ comp│  │ │  │       │ │   │      ││
 │   │    │         │ │  │ y   │  │ │  │       │ │   │  ▼   ││
 │   │    │         │ │  │     │  │ │  │  ▼    │ │   │      ││ y[1]
 │   └────┘         │ │  │ ▼   │  │ │  │       │ │   │      ││
 │                  │ │  │     │  │ │  │       │ │   │      ││
 │                  │ │  │     │  │ │  │       │ │   └──────┼│
 │                  │ │  │     │  │ │  │       │ │          ││
 │                  │ │  │     │  │ │  └───────┤ │          ││
 │                  │ │  │     │  │ │          │ │          ││
▲└──────────────────┘ └──┴─────┴──┘ └──────────┘ └──────────┘▼
│spacing[1] or spacing
▼┌──────────────────┐ ┌───────────┐ ┌────┬─────┐ ┌───┬──────┐▲
 ├──────────────────┤ ├───────────┤ │    │     │ │   │      ││
 │                  │ │           │ │    │     │ │   │      ││
 │              ▼   │ │           │ │    │     │ │   │      ││
 │   big comp x     │ │           │ │    │     │ │   │      ││
 │                  │ │    ▼      │ │  ▼ │     │ │ ▼ │      ││ y[0]
 ├──────────────────┤ │           │ │    │     │ │   │      ││
 │                  │ │           │ │    │     │ │   │      ││
 │                  │ │           │ │    │     │ │   │      ││
 │                  │ │           │ │    │     │ │   │      ││
 └──────────────────┴─┴───────────┴─┴────┼─────┴─┴───┼──────┘►
 ►──────────────────► ►───────────► ►──────────► ►──────────►
         x[0]               x[1]        x[2]         x[3]

Parameters:

Name Type Description Default
target KCell

Target KCell.

required
kcells Sequence[KCell | None] | Sequence[Sequence[KCell | None]]

Sequence or sequence of sequence of KCells to add to the grid

required
spacing int | tuple[int, int]

Value or tuple of value (different x/y) for spacing of the grid. [dbu]

required
target_trans Trans | None

Apply a transformation to the whole grid before placing it.

None
shape tuple[int, int] | None

Respace the input of kcells into an array and fill as many positions (first x then y).

None
align_x Literal['origin', 'xmin', 'xmax', 'center']

Align all the instance on the x-coordinate.

'center'
align_y Literal['origin', 'ymin', 'ymax', 'center']

Align all the instance on the y-coordinate.

'center'
rotation Literal[0, 1, 2, 3]

Apply a rotation to each kcells instance before adding it to the grid.

0
mirror bool

Mirror the instances before placing them in the grid

False
Source code in kfactory/grid.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
def flexgrid_dbu(
    target: KCell,
    kcells: Sequence[KCell | None] | Sequence[Sequence[KCell | None]],
    spacing: int | tuple[int, int],
    target_trans: kdb.Trans | None = None,
    shape: tuple[int, int] | None = None,
    align_x: Literal["origin", "xmin", "xmax", "center"] = "center",
    align_y: Literal["origin", "ymin", "ymax", "center"] = "center",
    rotation: Literal[0, 1, 2, 3] = 0,
    mirror: bool = False,
) -> InstanceGroup:
    """Create a grid of instances.

    A grid uses the bounding box of the biggest width per column and biggest height per
    row of any bounding boxes inserted into the grid.
    To this bounding box a spacing is applied in x and y.

    ```
                         spacing[0] or spacing
                        ◄─►
     ┌──────────────────┐ ┌──┬─────┬──┐ ┌──────────┐ ┌──────────┐▲
     │                  │ │  │     │  │ │          │ │          ││
     │   ┌────┐         │ │  │     │  │ │          │ │   ┌──────┼│
     │   │    │         │ │  │     │  │ │          │ │   │      ││
     │   │    │         │ │  │ big │  │ │  ┌───────┤ │   │      ││
     │   │ ▼  │         │ │  │ comp│  │ │  │       │ │   │      ││
     │   │    │         │ │  │ y   │  │ │  │       │ │   │  ▼   ││
     │   │    │         │ │  │     │  │ │  │  ▼    │ │   │      ││ y[1]
     │   └────┘         │ │  │ ▼   │  │ │  │       │ │   │      ││
     │                  │ │  │     │  │ │  │       │ │   │      ││
     │                  │ │  │     │  │ │  │       │ │   └──────┼│
     │                  │ │  │     │  │ │  │       │ │          ││
     │                  │ │  │     │  │ │  └───────┤ │          ││
     │                  │ │  │     │  │ │          │ │          ││
    ▲└──────────────────┘ └──┴─────┴──┘ └──────────┘ └──────────┘▼
    │spacing[1] or spacing
    ▼┌──────────────────┐ ┌───────────┐ ┌────┬─────┐ ┌───┬──────┐▲
     ├──────────────────┤ ├───────────┤ │    │     │ │   │      ││
     │                  │ │           │ │    │     │ │   │      ││
     │              ▼   │ │           │ │    │     │ │   │      ││
     │   big comp x     │ │           │ │    │     │ │   │      ││
     │                  │ │    ▼      │ │  ▼ │     │ │ ▼ │      ││ y[0]
     ├──────────────────┤ │           │ │    │     │ │   │      ││
     │                  │ │           │ │    │     │ │   │      ││
     │                  │ │           │ │    │     │ │   │      ││
     │                  │ │           │ │    │     │ │   │      ││
     └──────────────────┴─┴───────────┴─┴────┼─────┴─┴───┼──────┘►
     ►──────────────────► ►───────────► ►──────────► ►──────────►
             x[0]               x[1]        x[2]         x[3]
    ```

    Args:
        target: Target KCell.
        kcells: Sequence or sequence of sequence of KCells to add to the grid
        spacing: Value or tuple of value (different x/y) for spacing of the grid. [dbu]
        target_trans: Apply a transformation to the whole grid before placing it.
        shape: Respace the input of kcells into an array and fill as many positions
            (first x then y).
        align_x: Align all the instance on the x-coordinate.
        align_y: Align all the instance on the y-coordinate.
        rotation: Apply a rotation to each kcells instance before adding it to the grid.
        mirror: Mirror the instances before placing them in the grid

    """
    if isinstance(spacing, tuple):
        spacing_x, spacing_y = spacing
    else:
        spacing_x = spacing
        spacing_y = spacing

    if target_trans is None:
        target_trans = kdb.Trans()

    insts: list[list[Instance | None]]
    kcell_array: Sequence[Sequence[KCell]]

    if shape is None:
        if isinstance(kcells[0], KCell):
            kcell_array = cast("Sequence[list[KCell]]", [list(kcells)])
        else:
            kcell_array = cast("Sequence[Sequence[KCell]]", kcells)

        x0 = 0
        y0 = 0

        insts = [
            [
                None
                if kcell is None
                else target.create_inst(kcell, kdb.Trans(rotation, mirror, 0, 0))
                for kcell in array
            ]
            for array in kcell_array
        ]
        bboxes = [
            [None if inst is None else inst.bbox() for inst in array] for array in insts
        ]
        xmin: dict[int, int] = {}
        ymin: dict[int, int] = {}
        ymax: dict[int, int] = {}
        xmax: dict[int, int] = {}
        for i_y, (array, box_array) in enumerate(zip(insts, bboxes, strict=False)):
            for i_x, (inst, bbox) in enumerate(zip(array, box_array, strict=False)):
                if inst is not None and bbox is not None:
                    match align_x:
                        case "xmin":
                            x = -bbox.left
                        case "xmax":
                            x = -bbox.right
                        case "center":
                            x = -bbox.center().x
                        case _:
                            x = 0
                    match align_y:
                        case "ymin":
                            y = -bbox.bottom
                        case "ymax":
                            y = -bbox.top
                        case "center":
                            y = -bbox.center().y
                        case _:
                            y = 0
                    at = kdb.Trans(x, y)
                    inst.trans = at * inst.trans
                    bbox_ = inst.bbox()
                    xmin[i_x] = min(xmin.get(i_x) or bbox_.left, bbox_.left - spacing_x)
                    xmax[i_x] = max(xmax.get(i_x) or bbox_.right, bbox_.right)
                    ymin[i_y] = min(
                        ymin.get(i_y) or bbox_.bottom, bbox_.bottom - spacing_y
                    )
                    ymax[i_y] = max(ymax.get(i_y) or bbox_.top, bbox_.top)

        for i_y, (array, bbox_array) in enumerate(zip(insts, bboxes, strict=False)):
            y0 -= ymin.get(i_y, 0)
            for i_x, (bbox, inst) in enumerate(zip(bbox_array, array, strict=False)):
                x0 -= xmin.get(i_x, 0)
                if inst is not None and bbox is not None:
                    at = kdb.Trans(x0, y0)
                    inst.transform(target_trans * at)
                x0 += xmax.get(i_x, 0)
            y0 += ymax.get(i_y, 0)
            x0 = 0
        return InstanceGroup(
            [inst for array in insts for inst in array if inst is not None]
        )
    _kcells: Sequence[KCell | None]
    if isinstance(kcells[0], KCell):
        _kcells = cast("Sequence[KCell | None]", kcells)
    else:
        _kcells = [
            kcell
            for array in cast("Sequence[Sequence[KCell | None]]", kcells)
            for kcell in array
        ]

    if len(_kcells) > shape[0] * shape[1]:
        raise ValueError(
            f"Shape container size {shape[0] * shape[1]=} must be bigger "
            f"than the number of kcells {len(_kcells)}"
        )

    x0 = 0
    y0 = 0

    _insts = [
        None
        if kcell is None
        else target.create_inst(kcell, kdb.Trans(rotation, mirror, 0, 0))
        for kcell in _kcells
    ]

    xmin = {}
    ymin = {}
    ymax = {}
    xmax = {}
    for i, inst in enumerate(_insts):
        i_x = i % shape[1]
        i_y = i // shape[1]

        if inst is not None:
            bbox = inst.bbox()
            match align_x:
                case "xmin":
                    x = -bbox.left
                case "xmax":
                    x = -bbox.right
                case "center":
                    x = -bbox.center().x
                case _:
                    x = 0
            match align_y:
                case "ymin":
                    y = -bbox.bottom
                case "ymax":
                    y = -bbox.top
                case "center":
                    y = -bbox.center().y
                case _:
                    y = 0
            at = kdb.Trans(x, y)
            inst.trans = at * inst.trans
            bbox = inst.bbox()
            xmin[i_x] = min(xmin.get(i_x) or bbox.left, bbox.left - spacing_x)
            xmax[i_x] = max(xmax.get(i_x) or bbox.right, bbox.right)
            ymin[i_y] = min(ymin.get(i_y) or bbox.bottom, bbox.bottom - spacing_y)
            ymax[i_y] = max(ymax.get(i_y) or bbox.top, bbox.top)

    insts = []
    for _ in range(shape[0]):
        insts.append([None] * shape[1])

    for i, inst in enumerate(_insts):
        i_x = i % shape[1]
        i_y = i // shape[1]
        if i_x == 0:
            y0 -= ymin.get(i_y, 0)
            x0 = 0
        else:
            x0 -= xmin.get(i_x, 0)

        if inst is not None:
            at = kdb.Trans(x0, y0)
            inst.transform(target_trans * at)
            insts[i_y][i_x] = inst
        if i_x == shape[1] - 1:
            y0 += ymax.get(i_y, 0)
            x0 = 0
        else:
            x0 += xmax.get(i_x, 0)
    return InstanceGroup(
        [inst for array in insts for inst in array if inst is not None]
    )

get_schematic

get_schematic(
    c: KCell,
    exclude_port_types: Sequence[str] | None = (
        "placement",
        "pad",
        "bump",
    ),
) -> TSchematic[int]
get_schematic(
    c: DKCell,
    exclude_port_types: Sequence[str] | None = (
        "placement",
        "pad",
        "bump",
    ),
) -> TSchematic[float]
get_schematic(
    c: KCell | DKCell,
    exclude_port_types: Sequence[str] | None = (
        "placement",
        "pad",
        "bump",
    ),
) -> TSchematic[int] | TSchematic[float]

NOT FUNCTIONAL YET.

Create a minimal TSchematic from an existing cell.

Currently extracts named instances only. Port extraction is not yet implemented.

Parameters:

Name Type Description Default
c KCell | DKCell

Source cell.

required
exclude_port_types Sequence[str] | None

Port types to ignore (reserved for future use).

('placement', 'pad', 'bump')

Returns:

Type Description
TSchematic[int] | TSchematic[float]

A Schematic if c is KCell, otherwise a DSchematic.

Source code in kfactory/schematic.py
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
def get_schematic(
    c: KCell | DKCell,
    exclude_port_types: Sequence[str] | None = ("placement", "pad", "bump"),
) -> TSchematic[int] | TSchematic[float]:
    """NOT FUNCTIONAL YET.

    Create a minimal `TSchematic` from an existing cell.

    Currently extracts named instances only. Port extraction is not yet implemented.

    Args:
        c: Source cell.
        exclude_port_types: Port types to ignore (reserved for future use).

    Returns:
        A `Schematic` if `c` is `KCell`, otherwise a `DSchematic`.
    """

    if isinstance(c, KCell):
        schematic: TSchematic[int] | TSchematic[float] = Schematic(name=c.name)
    else:
        schematic = DSchematic(name=c.name)

    for inst in c.insts:
        name = inst.property(PROPID.NAME)
        if name is not None:
            schematic.create_inst(name, inst.cell.factory_name or inst.cell.name)

    return schematic

grid_dbu

grid_dbu(
    target: KCell,
    kcells: Sequence[KCell | None]
    | Sequence[Sequence[KCell | None]],
    spacing: int | tuple[int, int],
    target_trans: Trans | None = None,
    shape: tuple[int, int] | None = None,
    align_x: Literal[
        "origin", "xmin", "xmax", "center"
    ] = "center",
    align_y: Literal[
        "origin", "ymin", "ymax", "center"
    ] = "center",
    rotation: Literal[0, 1, 2, 3] = 0,
    mirror: bool = False,
) -> InstanceGroup

Create a grid of instances.

A grid uses the bounding box of the biggest width and biggest height of any bounding boxes inserted into the grid. to this bounding box a spacing is applied in x and y.

                      spacing[0] or spacing
                     ◄─►
  ┌──────────────────┐ ┌────┬─────┬─────┐ ┌────────────────┐ ┌──────────────────┐ ▲
  │                  │ │    │     │     │ │                │ │                  │ │
  │   ┌────┐         │ │    │     │     │ │                │ │      ┌──────┐    │ │
  │   │    │         │ │    │     │     │ │                │ │      │      │    │ │
  │   │    │         │ │    │ big │     │ │    ┌───────┐   │ │      │      │    │ │
  │   │    │         │ │    │ comp│     │ │    │       │   │ │      │      │    │ │
  │   │    │         │ │    │ y   │     │ │    │       │   │ │      │      │    │ │
  │   │    │         │ │    │     │     │ │    │       │   │ │      │     max bbox y
  │   └────┘         │ │    │     │     │ │    │       │   │ │      │      │    │ │
  │                  │ │    │     │     │ │    │       │   │ │      │      │    │ │
  │                  │ │    │     │     │ │    │       │   │ │      └──────┘    │ │
  │                  │ │    │     │     │ │    │       │   │ │                  │ │
  │                  │ │    │     │     │ │    └───────┘   │ │                  │ │
  │                  │ │    │     │     │ │                │ │                  │ │
 ▲└──────────────────┘ └────┴─────┴─────┘ └────────────────┘ └──────────────────┘ ▼
 │spacing[1] or spacing
 ▼┌──────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌──────────────────┐
  │                  │ │                │ │                │ │                  │
  │                  │ │                │ │ ┌────┐         │ │  ┌───┐           │
  ├──────────────────┤ │ ┌───────────┐  │ │ │    │         │ │  │   │           │
  │                  │ │ │           │  │ │ │    │         │ │  │   │           │
  │                  │ │ │           │  │ │ │    │         │ │  │   │           │
  │   big comp x     │ │ │           │  │ │ │    │         │ │  │   │           │
  │                  │ │ │           │  │ │ │    │         │ │  │   │           │
  ├──────────────────┤ │ │           │  │ │ │    │         │ │  │   │           │
  │                  │ │ │           │  │ │ │    │         │ │  │   │           │
  │                  │ │ │           │  │ │ │    │         │ │  │   │           │
  │                  │ │ │           │  │ │ │    │         │ │  │   │           │
  │                  │ │ └───────────┘  │ │ └────┘         │ │  └───┘           │
  │                  │ │                │ │                │ │                  │
  └──────────────────┘ └────────────────┘ └────────────────┘ └──────────────────┘

                                                             ◄──────────────────►
                                                                max bbox x

Parameters:

Name Type Description Default
target KCell

Target KCell.

required
kcells Sequence[KCell | None] | Sequence[Sequence[KCell | None]]

Sequence or sequence of sequence of KCells to add to the grid

required
spacing int | tuple[int, int]

Value or tuple of value (different x/y) for spacing of the grid. [dbu]

required
target_trans Trans | None

Apply a transformation to the whole grid before placing it.

None
shape tuple[int, int] | None

Respace the input of kcells into an array and fill as many positions (first x then y).

None
align_x Literal['origin', 'xmin', 'xmax', 'center']

Align all the instance on the x-coordinate.

'center'
align_y Literal['origin', 'ymin', 'ymax', 'center']

Align all the instance on the y-coordinate.

'center'
rotation Literal[0, 1, 2, 3]

Apply a rotation to each kcells instance before adding it to the grid.

0
mirror bool

Mirror the instances before placing them in the grid

False
Source code in kfactory/grid.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def grid_dbu(
    target: KCell,
    kcells: Sequence[KCell | None] | Sequence[Sequence[KCell | None]],
    spacing: int | tuple[int, int],
    target_trans: kdb.Trans | None = None,
    shape: tuple[int, int] | None = None,
    align_x: Literal["origin", "xmin", "xmax", "center"] = "center",
    align_y: Literal["origin", "ymin", "ymax", "center"] = "center",
    rotation: Literal[0, 1, 2, 3] = 0,
    mirror: bool = False,
) -> InstanceGroup:
    """Create a grid of instances.

    A grid uses the bounding box of the biggest width and biggest height of any bounding
    boxes inserted into the grid.
    to this bounding box a spacing is applied in x and y.

    ```
                          spacing[0] or spacing
                         ◄─►
      ┌──────────────────┐ ┌────┬─────┬─────┐ ┌────────────────┐ ┌──────────────────┐ ▲
      │                  │ │    │     │     │ │                │ │                  │ │
      │   ┌────┐         │ │    │     │     │ │                │ │      ┌──────┐    │ │
      │   │    │         │ │    │     │     │ │                │ │      │      │    │ │
      │   │    │         │ │    │ big │     │ │    ┌───────┐   │ │      │      │    │ │
      │   │    │         │ │    │ comp│     │ │    │       │   │ │      │      │    │ │
      │   │    │         │ │    │ y   │     │ │    │       │   │ │      │      │    │ │
      │   │    │         │ │    │     │     │ │    │       │   │ │      │     max bbox y
      │   └────┘         │ │    │     │     │ │    │       │   │ │      │      │    │ │
      │                  │ │    │     │     │ │    │       │   │ │      │      │    │ │
      │                  │ │    │     │     │ │    │       │   │ │      └──────┘    │ │
      │                  │ │    │     │     │ │    │       │   │ │                  │ │
      │                  │ │    │     │     │ │    └───────┘   │ │                  │ │
      │                  │ │    │     │     │ │                │ │                  │ │
     ▲└──────────────────┘ └────┴─────┴─────┘ └────────────────┘ └──────────────────┘ ▼
     │spacing[1] or spacing
     ▼┌──────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌──────────────────┐
      │                  │ │                │ │                │ │                  │
      │                  │ │                │ │ ┌────┐         │ │  ┌───┐           │
      ├──────────────────┤ │ ┌───────────┐  │ │ │    │         │ │  │   │           │
      │                  │ │ │           │  │ │ │    │         │ │  │   │           │
      │                  │ │ │           │  │ │ │    │         │ │  │   │           │
      │   big comp x     │ │ │           │  │ │ │    │         │ │  │   │           │
      │                  │ │ │           │  │ │ │    │         │ │  │   │           │
      ├──────────────────┤ │ │           │  │ │ │    │         │ │  │   │           │
      │                  │ │ │           │  │ │ │    │         │ │  │   │           │
      │                  │ │ │           │  │ │ │    │         │ │  │   │           │
      │                  │ │ │           │  │ │ │    │         │ │  │   │           │
      │                  │ │ └───────────┘  │ │ └────┘         │ │  └───┘           │
      │                  │ │                │ │                │ │                  │
      └──────────────────┘ └────────────────┘ └────────────────┘ └──────────────────┘

                                                                 ◄──────────────────►
                                                                    max bbox x
    ```

    Args:
        target: Target KCell.
        kcells: Sequence or sequence of sequence of KCells to add to the grid
        spacing: Value or tuple of value (different x/y) for spacing of the grid. [dbu]
        target_trans: Apply a transformation to the whole grid before placing it.
        shape: Respace the input of kcells into an array and fill as many positions
            (first x then y).
        align_x: Align all the instance on the x-coordinate.
        align_y: Align all the instance on the y-coordinate.
        rotation: Apply a rotation to each kcells instance before adding it to the grid.
        mirror: Mirror the instances before placing them in the grid

    """
    if isinstance(spacing, tuple):
        spacing_x, spacing_y = spacing
    else:
        spacing_x = spacing
        spacing_y = spacing

    if target_trans is None:
        target_trans = kdb.Trans()

    insts: list[list[Instance | None]]
    kcell_array: Sequence[Sequence[KCell]]

    if shape is None:
        if isinstance(kcells[0], KCell):  # noqa: SIM108
            kcell_array = [list(kcells)]  # type:ignore[arg-type]
        else:
            kcell_array = kcells  # type: ignore[assignment]

        x0 = 0
        y0 = 0

        insts = [
            [
                target.create_inst(kcell, kdb.Trans(rotation, mirror, 0, 0))
                for kcell in array
            ]
            for array in kcell_array
        ]
        bboxes = [
            [None if inst is None else inst.bbox() for inst in array] for array in insts
        ]
        w = max(
            max(0 if bbox is None else bbox.width() + spacing_x for bbox in box_array)
            for box_array in bboxes
        )
        h = max(
            max(0 if bbox is None else bbox.height() + spacing_y for bbox in box_array)
            for box_array in bboxes
        )
        for array, bbox_array in zip(insts, bboxes, strict=False):
            y0 += h - h // 2
            for bbox, inst in zip(bbox_array, array, strict=False):
                x0 += w - w // 2
                if bbox is not None and inst is not None:
                    match align_x:
                        case "xmin":
                            x = -bbox.left
                        case "xmax":
                            x = -bbox.right
                        case "center":
                            x = -bbox.center().x
                        case _:
                            x = 0
                    match align_y:
                        case "ymin":
                            y = -bbox.bottom
                        case "ymax":
                            y = -bbox.top
                        case "center":
                            y = -bbox.center().y
                        case _:
                            y = 0
                    at = kdb.Trans(x0 + x, y0 + y)

                    inst.transform(target_trans * at)
                x0 += w // 2
            y0 += h // 2
            x0 = 0
        return InstanceGroup(
            [inst for array in insts for inst in array if inst is not None]
        )
    _kcells: Sequence[KCell | None]
    if isinstance(kcells[0], KCell):
        _kcells = cast("Sequence[KCell | None]", kcells)
    else:
        _kcells = [
            kcell
            for array in cast("Sequence[Sequence[KCell | None]]", kcells)
            for kcell in array
        ]

    if len(_kcells) > shape[0] * shape[1]:
        raise ValueError(
            f"Shape container size {shape[0] * shape[1]=!r} must be bigger "
            f"than the number of kcells {len(_kcells)}"
        )

    x0 = 0
    y0 = 0

    _insts = [
        None
        if kcell is None
        else target.create_inst(kcell, kdb.Trans(rotation, mirror, 0, 0))
        for kcell in _kcells
    ]

    insts = []
    for _ in range(shape[0]):
        insts.append([None] * shape[1])

    shape_bboxes = [None if inst is None else inst.bbox() for inst in _insts]
    shape_bboxes_heights = [0 if box is None else box.height() for box in shape_bboxes]
    shape_bboxes_widths = [0 if box is None else box.width() for box in shape_bboxes]
    w = max(shape_bboxes_widths) + spacing_x
    h = max(shape_bboxes_heights) + spacing_y
    for i, (inst, bbox) in enumerate(zip(_insts, shape_bboxes, strict=False)):
        i_x = i % shape[1]
        i_y = i // shape[1]
        insts[i_y][i_x] = inst
        if i_x == 0:
            y0 += h - h // 2
            x0 = 0
        else:
            x0 += w - w // 2

        if bbox is not None and inst is not None:
            match align_x:
                case "xmin":
                    x = -bbox.left
                case "xmax":
                    x = -bbox.right
                case "center":
                    x = -bbox.center().x
                case _:
                    x = 0
            match align_y:
                case "ymin":
                    y = -bbox.bottom
                case "ymax":
                    y = -bbox.top
                case "center":
                    y = -bbox.center().y
                case _:
                    y = 0
            at = kdb.Trans(x0 + x, y0 + y)

            inst.transform(target_trans * at)
        if i_x == shape[1] - 1:
            y0 += h // 2
            x0 = 0
        else:
            x0 += w // 2
    return InstanceGroup(
        [inst for array in insts for inst in array if inst is not None]
    )

polygon_from_array

polygon_from_array(
    array: Iterable[tuple[int, int]],
) -> kdb.Polygon

Create a DPolygon from a 2D array-like structure. (dbu version).

Array-like: [[x1,y1],[x2,y2],...]

Source code in kfactory/utilities.py
67
68
69
70
71
72
def polygon_from_array(array: Iterable[tuple[int, int]]) -> kdb.Polygon:
    """Create a DPolygon from a 2D array-like structure. (dbu version).

    Array-like: `[[x1,y1],[x2,y2],...]`
    """
    return kdb.Polygon([kdb.Point(int(x), int(y)) for (x, y) in array])

pprint_ports

pprint_ports(
    ports: Iterable[ProtoPort[Any]],
    unit: Literal["dbu", "um"] | None = None,
) -> Table

Print ports as a table.

Parameters:

Name Type Description Default
ports Iterable[ProtoPort[Any]]

The ports which should be printed.

required
unit Literal['dbu', 'um'] | None

Define the print type of the ports. If None, any port which can be represented accurately by a dbu representation will be printed in dbu otherwise in um. 'dbu'/'um' will force the printing to enforce one or the other representation

None
Source code in kfactory/utilities.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def pprint_ports(
    ports: Iterable[ProtoPort[Any]], unit: Literal["dbu", "um"] | None = None
) -> Table:
    """Print ports as a table.

    Args:
        ports: The ports which should be printed.
        unit: Define the print type of the ports. If None, any port
            which can be represented accurately by a dbu representation
            will be printed in dbu otherwise in um. 'dbu'/'um' will force
            the printing to enforce one or the other representation
    """
    table = Table(show_lines=True)

    table.add_column("Name")
    table.add_column("Width")
    table.add_column("Layer")
    table.add_column("X")
    table.add_column("Y")
    table.add_column("Angle")
    table.add_column("Mirror")
    table.add_column("Info")

    match unit:
        case None:
            for port in ports:
                if port.base.trans is not None:
                    table.add_row(
                        str(port.name) + " [dbu]",
                        f"{port.width:_}",
                        port.kcl.get_info(port.layer).to_s(),
                        f"{port.x:_}",
                        f"{port.y:_}",
                        str(port.angle),
                        str(port.mirror),
                        JSON.from_data(port.info.model_dump()),
                    )
                else:
                    t = port.dcplx_trans
                    dx = t.disp.x
                    dy = t.disp.y
                    dwidth = port.kcl.to_um(port.cross_section.width)
                    angle = t.angle
                    mirror = t.mirror
                    table.add_row(
                        str(port.name) + " [um]",
                        f"{dwidth:_}",
                        port.kcl.get_info(port.layer).to_s(),
                        f"{dx:_}",
                        f"{dy:_}",
                        str(angle),
                        str(mirror),
                        JSON.from_data(port.info.model_dump()),
                    )
        case "um":
            for port in ports:
                dport = port.to_dtype()
                t = dport.dcplx_trans
                dx = t.disp.x
                dy = t.disp.y
                dwidth = dport.cross_section.width
                angle = t.angle
                mirror = t.mirror
                table.add_row(
                    str(dport.name) + " [um]",
                    f"{dwidth:_}",
                    dport.kcl.get_info(dport.layer).to_s(),
                    f"{dx:_}",
                    f"{dy:_}",
                    str(angle),
                    str(mirror),
                    JSON.from_data(dport.info.model_dump()),
                )
        case "dbu":
            for port in ports:
                iport = port.to_itype()
                table.add_row(
                    str(iport.name) + " [dbu]",
                    f"{iport.width:_}",
                    iport.kcl.get_info(iport.layer).to_s(),
                    f"{iport.x:_}",
                    f"{iport.y:_}",
                    str(iport.angle),
                    str(iport.mirror),
                    JSON.from_data(iport.info.model_dump()),
                )

    return table

read_schematic

read_schematic(
    file: Path | str, unit: Literal["dbu"] = "dbu"
) -> Schematic
read_schematic(
    file: Path | str, unit: Literal["um"]
) -> DSchematic
read_schematic(
    file: Path | str, unit: Literal["dbu", "um"] = "dbu"
) -> Schematic | DSchematic

Read a schematic from a YAML file.

Parameters:

Name Type Description Default
file Path | str

Path to a YAML file.

required
unit Literal['dbu', 'um']

Target coordinate unit; controls which subclass is constructed.

'dbu'

Returns:

Type Description
Schematic | DSchematic

Schematic when unit="dbu", else DSchematic.

Raises:

Type Description
ValueError

If file does not exist or is not a file.

Source code in kfactory/schematic.py
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
def read_schematic(
    file: Path | str, unit: Literal["dbu", "um"] = "dbu"
) -> Schematic | DSchematic:
    """Read a schematic from a YAML file.

    Args:
        file: Path to a YAML file.
        unit: Target coordinate unit; controls which subclass is constructed.

    Returns:
        `Schematic` when `unit="dbu"`, else `DSchematic`.

    Raises:
        ValueError: If `file` does not exist or is not a file.
    """

    file = Path(file).resolve()
    if not file.is_file():
        raise ValueError(f"{file=} is either not a file or does not exist.")
    with file.open(mode="rt") as f:
        yaml_dict = yaml.load(f)
        if unit == "dbu":
            return Schematic.model_validate(yaml_dict, strict=True)
        return DSchematic.model_validate(yaml_dict)

save_layout_options

save_layout_options(
    **attributes: Any,
) -> kdb.SaveLayoutOptions

Default options for saving GDS/OAS.

Parameters:

Name Type Description Default
attributes Any

Set attributes of the layout save option object. E.g. to save the gds without metadata pass write_context_info=False

{}
Source code in kfactory/utilities.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def save_layout_options(**attributes: Any) -> kdb.SaveLayoutOptions:
    """Default options for saving GDS/OAS.

    Args:
        attributes: Set attributes of the layout save option object. E.g. to save the
            gds without metadata pass `write_context_info=False`
    """
    save = kdb.SaveLayoutOptions()
    save.gds2_write_cell_properties = True
    save.gds2_write_file_properties = True
    save.gds2_write_timestamps = False
    save.write_context_info = config.write_context_info
    save.gds2_max_cellname_length = config.max_cellname_length

    for k, v in attributes.items():
        setattr(save, k, v)

    return save

show

show(
    layout: KCLayout | AnyKCell | Path | str,
    lyrdb: ReportDatabase | Path | str | None = None,
    l2n: LayoutToNetlist | Path | str | None = None,
    technology: str | None = None,
    keep_position: bool = True,
    save_options: SaveLayoutOptions | None = None,
    use_libraries: bool = True,
    library_save_options: SaveLayoutOptions | None = None,
    set_technology: bool = True,
    file_format: Literal["oas", "gds"] = "oas",
) -> None

Show GDS in klayout.

Parameters:

Name Type Description Default
layout KCLayout | AnyKCell | Path | str

The object to show. This can be a KCell, KCLayout, Path, or string.

required
lyrdb ReportDatabase | Path | str | None

A KLayout report database (.lyrdb/.rdb) file or object to show with the layout.

None
l2n LayoutToNetlist | Path | str | None

A KLayout LayoutToNetlist object or file (.l2n) to show with the layout.

None
keep_position bool

Keep the current KLayout position if a view is already open.

True
save_options SaveLayoutOptions | None

Custom options for saving the gds/oas.

None
use_libraries bool

Save other KCLayouts as libraries on write.

True
library_save_options SaveLayoutOptions | None

Specific saving options for Cells which are in a library and not the main KCLayout.

None
Source code in kfactory/kcell.py
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
def show(
    layout: KCLayout | AnyKCell | Path | str,
    lyrdb: rdb.ReportDatabase | Path | str | None = None,
    l2n: kdb.LayoutToNetlist | Path | str | None = None,
    technology: str | None = None,
    keep_position: bool = True,
    save_options: kdb.SaveLayoutOptions | None = None,
    use_libraries: bool = True,
    library_save_options: kdb.SaveLayoutOptions | None = None,
    set_technology: bool = True,
    file_format: Literal["oas", "gds"] = "oas",
) -> None:
    """Show GDS in klayout.

    Args:
        layout: The object to show. This can be a KCell, KCLayout, Path, or string.
        lyrdb: A KLayout report database (.lyrdb/.rdb) file or object to show with the
            layout.
        l2n: A KLayout LayoutToNetlist object or file (.l2n) to show with the layout.
        keep_position: Keep the current KLayout position if a view is already open.
        save_options: Custom options for saving the gds/oas.
        use_libraries: Save other KCLayouts as libraries on write.
        library_save_options: Specific saving options for Cells which are in a library
            and not the main KCLayout.
    """
    from .layout import KCLayout, kcls

    delete = False
    delete_lyrdb = False
    delete_l2n = False

    if save_options is None:
        save_options = save_layout_options()
    if library_save_options is None:
        library_save_options = save_layout_options()

    # Find the file that calls stack
    try:
        stk = inspect.getouterframes(inspect.currentframe())
        frame = stk[2]
        frame_filename_stem = Path(frame.filename).stem
        if frame_filename_stem.startswith("<ipython-input"):  # IPython Case
            name = "ipython"
        elif frame.function != "<module>":
            name = clean_name(frame_filename_stem + "_" + frame.function)
        else:
            name = clean_name(frame_filename_stem)
    except Exception:
        try:
            from __main__ import __file__ as mf

            name = clean_name(mf)
        except ImportError:
            name = "shell"

    kcl_paths: list[dict[str, str]] = []

    if isinstance(layout, KCLayout):
        gitpath = config.project_dir
        if gitpath:
            root = Path(gitpath) / "build/mask"
            root.mkdir(parents=True, exist_ok=True)
            tf = root / Path(name).with_suffix(f".{file_format}")
            tf.parent.mkdir(parents=True, exist_ok=True)
            layout.write(str(tf), save_options)
            file = tf
            delete = False
        else:
            try:
                from __main__ import __file__ as mf
            except ImportError:
                mf = "shell"
            tf = Path(gettempdir()) / f"{name}.{file_format}"
            tf.parent.mkdir(parents=True, exist_ok=True)
            layout.write(tf, save_options)
            file = tf
            delete = True
        if use_libraries:
            dir_ = tf.parent
            kcls_ = list(kcls.values())
            kcls_.remove(layout)
            for _kcl in kcls_:
                if save_options.gds2_max_cellname_length:
                    p = (
                        (dir_ / _kcl.name[: save_options.gds2_max_cellname_length])
                        .with_suffix(f".{file_format}")
                        .resolve()
                    )
                else:
                    p = (dir_ / _kcl.name).with_suffix(f".{file_format}").resolve()
                _kcl.write(p, library_save_options)
                kcl_paths.append({"name": _kcl.name, "file": str(p)})
        if technology is None and layout.technology_file is not None:
            technology = layout.technology.name

    elif isinstance(layout, ProtoKCell):
        gitpath = config.project_dir
        if gitpath:
            root = Path(gitpath) / "build" / file_format
            root.mkdir(parents=True, exist_ok=True)
            tf = root / Path(name).with_suffix(f".{file_format}")
            tf.parent.mkdir(parents=True, exist_ok=True)
            layout.write(str(tf), save_options)
            file = tf
            delete = False
        else:
            try:
                from __main__ import __file__ as mf
            except ImportError:
                mf = "shell"
            tf = Path(gettempdir()) / f"{name}.{file_format}"
            tf.parent.mkdir(parents=True, exist_ok=True)
            layout.write(tf, save_options)
            file = tf
            delete = True
        if use_libraries:
            dir_ = tf.parent
            kcls_ = list(kcls.values())
            kcls_.remove(layout.kcl)
            for _kcl in kcls_:
                p = (dir_ / _kcl.name).with_suffix(f".{file_format}").resolve()
                _kcl.write(p, library_save_options)
                kcl_paths.append({"name": _kcl.name, "file": str(p)})
        if technology is None and layout.kcl.technology_file is not None:
            technology = layout.kcl.technology.name

    elif isinstance(layout, str | Path):
        file = Path(layout).expanduser().resolve()
    else:
        raise NotImplementedError(
            f"Unknown type {type(layout)} for streaming to KLayout"
        )
    if not file.is_file():
        raise ValueError(f"{file} is not a File")
    logger.debug("klive file: {}", file)
    data_dict = {
        "gds": str(file),
        "keep_position": keep_position,
        "libraries": kcl_paths,
    }

    if lyrdb is not None:
        if isinstance(lyrdb, rdb.ReportDatabase):
            gitpath = config.project_dir
            if gitpath:
                root = Path(gitpath) / "build/mask"
                root.mkdir(parents=True, exist_ok=True)
                tf = root / Path(name).with_suffix(".lyrdb")
                tf.parent.mkdir(parents=True, exist_ok=True)
                lyrdb.save(str(tf))
                lyrdbfile = tf
                delete_lyrdb = False
            else:
                try:
                    from __main__ import __file__ as mf
                except ImportError:
                    mf = "shell"
                tf = Path(gettempdir()) / (name + ".lyrdb")
                tf.parent.mkdir(parents=True, exist_ok=True)
                lyrdb.save(str(tf))
                lyrdbfile = tf
                delete_lyrdb = True
        elif isinstance(lyrdb, str | Path):
            lyrdbfile = Path(lyrdb).expanduser().resolve()
        else:
            raise NotImplementedError(
                f"Unknown type {type(lyrdb)} for streaming to KLayout"
            )
        if not lyrdbfile.is_file():
            raise ValueError(f"{lyrdbfile} is not a File")
        data_dict["lyrdb"] = str(lyrdbfile)

    if l2n is not None:
        if isinstance(l2n, kdb.LayoutToNetlist):
            gitpath = config.project_dir
            if gitpath:
                root = Path(gitpath) / "build/mask"
                root.mkdir(parents=True, exist_ok=True)
                tf = root / Path(name).with_suffix(".l2n")
                tf.parent.mkdir(parents=True, exist_ok=True)
                l2n.write(str(tf))
                l2nfile = tf
                delete_l2n = False
            else:
                try:
                    from __main__ import __file__ as mf
                except ImportError:
                    mf = "shell"
                tf = Path(gettempdir()) / (name + ".l2n")
                tf.parent.mkdir(parents=True, exist_ok=True)
                l2n.write(str(tf))
                l2nfile = tf
                delete_l2n = True
        elif isinstance(l2n, str | Path):
            l2nfile = Path(l2n).expanduser().resolve()
        else:
            raise NotImplementedError(
                f"Unknown type {type(l2n)} for streaming to KLayout"
            )
        if not l2nfile.is_file():
            raise ValueError(f"{lyrdbfile} is not a File")
        data_dict["l2n"] = str(l2nfile)

    if set_technology and technology is not None:
        data_dict["technology"] = technology

    data = json.dumps(data_dict)
    try:
        conn = socket.create_connection(("127.0.0.1", 8082), timeout=0.5)
        data += "\n"
        enc_data = data.encode()
        conn.sendall(enc_data)
        conn.settimeout(5)
    except OSError:
        logger.warning("Could not connect to klive server")
    else:
        msg = ""
        try:
            msg = conn.recv(1024).decode("utf-8")
            try:
                jmsg = json.loads(msg)
                match jmsg["type"]:
                    case "open":
                        info = jmsg.get("info")
                        if info:
                            (
                                logger.info(
                                    "klive v{version}: Opened file '{file}'"
                                    ", Messages: {info}",
                                    version=jmsg["version"],
                                    file=jmsg["file"],
                                    info=info,
                                ),
                            )
                        else:
                            logger.info(
                                "klive v{version}: Opened file '{file}'",
                                version=jmsg["version"],
                                file=jmsg["file"],
                            )
                    case "reload":
                        info = jmsg.get("info")
                        if info:
                            logger.info(
                                "klive v{version}: Reloaded file '{file}'"
                                ", Messages: {info}",
                                version=jmsg["version"],
                                file=jmsg["file"],
                                info=info,
                            )
                        else:
                            logger.info(
                                "klive v{version}: Reloaded file '{file}'",
                                version=jmsg["version"],
                                file=jmsg["file"],
                            )
                # check klive version
                klive_version = Version.parse(jmsg["version"])
                rec_klive_version = Version(major=0, minor=4, patch=1)
                klive_ok = rec_klive_version.compare(klive_version) >= 0

                if not klive_ok:
                    logger.warning(
                        f"klive is out of date. Installed:{jmsg['version']}/"
                        "Recommended:"
                        f"{rec_klive_version}. Please "
                        "update it in KLayout"
                    )
                else:
                    klayout_version = Version.parse(jmsg["klayout_version"])
                    kfactory_version = Version.parse(_klayout_version)

                    min_rec_klayout = Version(major=0, minor=28, patch=13)

                    if kfactory_version.compare(min_rec_klayout) < 0:
                        logger.error(
                            f"KLayout GUI version ({jmsg['klayout_version']}) "
                            "is older than the Python version "
                            f"({_klayout_version}). This may cause issues. Please "
                            "update the GUI to match or exceed the Python version."
                        )
                    elif kfactory_version.compare(klayout_version) < 0:
                        logger.debug(
                            f"KLayout GUI version ({jmsg['klayout_version']}) "
                            "is older than the Python version "
                            f"({_klayout_version}). This may cause issues. Please "
                            "update the GUI to match or exceed the Python version."
                        )

            except json.JSONDecodeError:
                logger.info(f"Message from klive: {msg}")
        except OSError:
            logger.warning("klive didn't send data, closing")
        finally:
            conn.close()

    if delete:
        Path(file).unlink()
    if delete_lyrdb and lyrdb is not None:
        Path(lyrdbfile).unlink()
    if delete_l2n and l2n is not None:
        Path(l2nfile).unlink()