Skip to content

Download notebook (.ipynb)

Virtual PCells

A Virtual PCell (VKCell, short for Virtual KCell) is an in-memory PCell whose geometry lives entirely in floating-point micrometres — no KLayout database, no DBU conversion, until you explicitly materialise it.

┌──────────────────────────────────────────────┐
│               VKCell (µm)                    │
│  ┌──────────┐   ┌──────────┐                 │
│  │ VInstance│   │ VShapes  │  DPolygon, DBox │
│  └──────────┘   └──────────┘                 │
│        │             │                       │
│        ▼             ▼                       │
│   insert_into(real_kcell)                    │
└──────────────────────────────────────────────┘
          │
          ▼
   KCell (DBU, KLayout-backed)

When to use VKCell

Use case Recommended cell type
Production GDS, routing into a final layout KCell
All-angle routing backbone computation VKCell
Preview / inspect before committing geometry VKCell
Factory functions that need composition before materialisation VKCell

Key differences vs KCell

Property VKCell KCell
Coordinates float µm integer DBU
Backed by KLayout cell DB No — pure Python Yes
Shape type VShapes (DPolygon, DBox, …) klayout.db.Shapes
@kf.vcell / kcl.vcell decorator Yes No (@kf.cell)
Must call insert_into() to materialise Yes

Setup

from functools import partial

import kfactory as kf
from kfactory.factories.virtual.euler import virtual_bend_euler_factory
from kfactory.factories.virtual.straight import virtual_straight_factory


class LAYER(kf.LayerInfos):
    WG: kf.kdb.LayerInfo = kf.kdb.LayerInfo(1, 0)
    WGCLAD: kf.kdb.LayerInfo = kf.kdb.LayerInfo(2, 0)


L = LAYER()

# Use a dedicated KCLayout so the virtual factories' layer indices are consistent
pdk = kf.KCLayout("virtual_demo", infos=LAYER)

Creating a VKCell directly

kcl.vkcell(name=...) returns an empty VKCell attached to the given layout. Shapes are added via vkcell.shapes(layer_index).insert(...) using µm-based DPolygon / DBox objects.

vc_box = pdk.vkcell(name="simple_box")

layer_idx = pdk.layer(L.WG)

# Insert a rectangle in µm
vc_box.shapes(layer_idx).insert(kf.kdb.DBox(0, -0.25, 10.0, 0.25))

print(f"VKCell name  : {vc_box.name}")
print(f"bbox (µm)    : {vc_box.dbbox(layer_idx)}")
print(f"bbox (DBU)   : {vc_box.ibbox(layer_idx)}")
print(f"shapes count : {vc_box.shapes(layer_idx).size()}")
VKCell name  : simple_box
bbox (µm)    : (0,-0.25;10,0.25)
bbox (DBU)   : (0,-250;10000,250)
shapes count : 1

Notice that ibbox returns the bounding box in DBU (integer units), while dbbox returns it in µm. The two are consistent — multiplied by pdk.dbu (nm/µm).

Ports on a VKCell

Ports are created with create_port using a DCplxTrans (µm-based complex transform) to specify position and orientation.

vc_wg = pdk.vkcell(name="virtual_wg")
wg_layer = pdk.layer(L.WG)

vc_wg.shapes(wg_layer).insert(kf.kdb.DBox(0, -0.25, 20.0, 0.25))

# o1 faces west (angle=180°)
vc_wg.create_port(
    name="o1",
    dcplx_trans=kf.kdb.DCplxTrans(1, 180, False, 0.0, 0.0),
    width=0.5,
    layer=wg_layer,
)
# o2 faces east (angle=0°)
vc_wg.create_port(
    name="o2",
    dcplx_trans=kf.kdb.DCplxTrans(1, 0, False, 20.0, 0.0),
    width=0.5,
    layer=wg_layer,
)

for p in vc_wg.ports:
    print(f"  {p.name}: trans={p.dcplx_trans}  width={p.dwidth:.3f} µm")
  o1: trans=r180 *1 0,0  width=0.500 µm
  o2: trans=r0 *1 20,0  width=0.500 µm

Materialising into a KCell

Call VInstance(vc).insert_into(target) to convert the virtual geometry to a real KCell instance inside target. This is a one-shot operation — you can call it multiple times to place the virtual cell at different locations.

target = kf.KCell("wg_from_virtual", kcl=pdk)
vi = kf.VInstance(vc_wg)
vi.insert_into(target)

print(f"Real cell name  : {target.name}")
print(f"Real cell bbox  : {target.dbbox()} µm")
target.plot()
Real cell name  : wg_from_virtual
Real cell bbox  : (0,-0.25;20,0.25) µm


png

Nesting VKCells

VKCells can contain instances of other VKCells. The whole hierarchy is flattened into real KLayout cells when insert_into is called on the outermost level.

# Build two virtual waveguide arms
arm_a = pdk.vkcell(name="arm_a")
arm_a.shapes(wg_layer).insert(kf.kdb.DBox(0, -0.25, 30.0, 0.25))

arm_b = pdk.vkcell(name="arm_b")
arm_b.shapes(wg_layer).insert(kf.kdb.DBox(0, -0.25, 30.0, 0.25))

# Compose them inside a parent virtual cell
parent_vc = pdk.vkcell(name="virtual_composed")
inst_a = parent_vc.create_inst(cell=arm_a)  # at (0, 0)
inst_b = parent_vc.create_inst(
    cell=arm_b,
    trans=kf.kdb.DCplxTrans(1, 0, False, 0.0, 2.0),  # shift 2 µm up
)

print(f"parent bbox (µm): {parent_vc.dbbox()}")

# Materialise the whole hierarchy at once
c_composed = kf.KCell("composed_from_virtual", kcl=pdk)
kf.VInstance(parent_vc).insert_into(c_composed)
c_composed.plot()
parent bbox (µm): (0,-0.25;30,2.25)


png

@kf.vcell decorator

For reusable virtual component factories use the @pdk.vcell decorator — it works like @pdk.cell but returns a VKCell and caches by parameter hash.

@pdk.vcell
def virtual_straight(
    width: float,
    length: float,
    layer: kf.kdb.LayerInfo,
) -> kf.VKCell:
    c = pdk.vkcell()
    lyr = pdk.layer(layer)
    half_w = width / 2
    c.shapes(lyr).insert(kf.kdb.DBox(0, -half_w, length, half_w))
    c.create_port(
        name="o1",
        dcplx_trans=kf.kdb.DCplxTrans(1, 180, False, 0.0, 0.0),
        width=width,
        layer=lyr,
    )
    c.create_port(
        name="o2",
        dcplx_trans=kf.kdb.DCplxTrans(1, 0, False, length, 0.0),
        width=width,
        layer=lyr,
    )
    return c


s1 = virtual_straight(width=0.5, length=10.0, layer=L.WG)
s2 = virtual_straight(width=0.5, length=10.0, layer=L.WG)  # cached

print(f"s1 is s2 (cached): {s1 is s2}")
print(f"s1 bbox : {s1.dbbox(pdk.layer(L.WG))} µm")
s1 is s2 (cached): True
s1 bbox : (0,-0.25;10,0.25) µm

Virtual factories

kfactory ships built-in virtual factories for straight waveguides and euler bends. These are the building blocks for all-angle routing (see All-Angle Routing).

Factory Source module Parameter units
virtual_straight_factory(kcl) kfactory.factories.virtual.straight µm
virtual_bend_euler_factory(kcl) kfactory.factories.virtual.euler µm
_v_straight_raw = virtual_straight_factory(kcl=pdk)
_v_bend_raw = virtual_bend_euler_factory(kcl=pdk)

# Bind common parameters with functools.partial
v_straight = partial(_v_straight_raw, layer=L.WG)
v_bend = partial(_v_bend_raw, width=0.5, radius=10.0, layer=L.WG)

# Produce virtual components
vs = v_straight(width=0.5, length=15.0)
vb = v_bend(angle=90)

print(f"virtual straight bbox : {vs.dbbox(pdk.layer(L.WG))} µm")
print(f"virtual bend    bbox  : {vb.dbbox(pdk.layer(L.WG))} µm")
print(f"virtual bend    ports : {[p.name for p in vb.ports]}")
virtual straight bbox : (0,-0.25;15,0.25) µm
virtual bend    bbox  : (0,-0.25;18.9509584665,18.7009584665) µm
virtual bend    ports : ['o1', 'o2']

All-angle routing into a VKCell

The most common reason to work with VKCell directly is all-angle routing — computing the route in virtual space, inspecting it, then materialising once satisfied.

vc_route = pdk.vkcell(name="aa_preview")

kf.routing.aa.optical.route(
    vc_route,
    width=0.5,
    backbone=[
        kf.kdb.DPoint(0, 0),
        kf.kdb.DPoint(80, 0),
        kf.kdb.DPoint(80, 60),
        kf.kdb.DPoint(160, 60),
    ],
    straight_factory=v_straight,
    bend_factory=v_bend,
)

print(f"Virtual route bbox (µm): {vc_route.dbbox()}")
print(f"Ports: {[p.name for p in vc_route.ports]}")

# Materialise into a real cell
c_aa = kf.KCell("aa_from_vkcell", kcl=pdk)
kf.VInstance(vc_route).insert_into(c_aa)
c_aa.plot()
Virtual route bbox (µm): (0,-0.25;160,60.25)
Ports: []


png

insert_into_flat

Use insert_into_flat when you want the virtual geometry inlined directly into the target cell (no sub-cell hierarchy created).

c_flat = kf.KCell("aa_flat", kcl=pdk)

vc_flat = pdk.vkcell(name="route_flat_src")
kf.routing.aa.optical.route(
    vc_flat,
    width=0.5,
    backbone=[
        kf.kdb.DPoint(0, 0),
        kf.kdb.DPoint(50, 0),
        kf.kdb.DPoint(50, 40),
        kf.kdb.DPoint(100, 40),
    ],
    straight_factory=v_straight,
    bend_factory=v_bend,
)

kf.VInstance(vc_flat).insert_into_flat(c_flat)

print(f"Instances in c_flat : {list(c_flat.each_inst())}")
print(f"c_flat bbox (µm)    : {c_flat.dbbox()}")
c_flat.plot()
Instances in c_flat : []
c_flat bbox (µm)    : (0,-0.25;100,40.25)


png

With insert_into_flat all geometry lands directly in c_flat — no child cells are created.

Summary

Operation Code
Create VKCell vc = kcl.vkcell(name="...")
Insert shape vc.shapes(layer_idx).insert(kdb.DBox(...))
Add port vc.create_port(name=..., dcplx_trans=..., width=..., layer=...)
Nest VKCells vc.create_inst(cell=child_vc, trans=...)
Reusable factory @pdk.vcell decorator
Materialise (hierarchical) kf.VInstance(vc).insert_into(target)
Materialise (flat) kf.VInstance(vc).insert_into_flat(target)
Virtual straight factory virtual_straight_factory(kcl=pdk)
Virtual euler bend factory virtual_bend_euler_factory(kcl=pdk)

See Also

Topic Where
PCells & caching Components: PCells
Factory functions reference Components: Factories
All-angle routing into VKCell Routing: All-Angle
KCell / DKCell / VKCell basics Core Concepts: KCell