# Frequently Asked Questions

Common questions and "gotcha" moments collected from kfactory users. For executable examples see [Best Practices](<https://gdsfactory.github.io/kfactory/3.5.0/howto/best_practices/index.md>).

---

## Units &amp; coordinates

### Why does my geometry look 1000× too big (or too small)?

kfactory uses two coordinate systems in parallel:

| System | Unit | Used by |
| --- | --- | --- |
| DBU (database units) | nm (1 nm = 1 DBU at default 1 nm/DBU) | KLayout internals, `KCell`, `Port`, `kdb.Trans` |
| µm | micrometres | Human-readable APIs, `DKCell`, `DPort`, `kdb.DTrans` |

The two never mix automatically. Always convert explicitly:

```
width_nm = kf.kcl.to_dbu(0.5)  # 0.5 µm → 500 DBU
width_um = kf.kcl.to_um(500)  # 500 DBU → 0.5 µm
```

The integer `to_dbu` conversion **rounds** — widths that are not a multiple of 2 DBU are silently rounded. Design your grid in multiples of 2 DBU to avoid asymmetry.

---

### Which factory takes DBU and which takes µm?

| Factory | Width / radius units |
| --- | --- |
| `straight_dbu_factory` | **DBU** |
| `taper_factory` | **DBU** |
| `bend_euler_factory` | **µm** |
| `bend_circular_factory` | **µm** |
| `bend_s_euler_factory` | **µm** |
| `bezier_factory` (via `kf.cells.bezier`) | **µm** |

When in doubt, look at the function name: `_dbu_` → DBU, otherwise µm.

---

## Ports

### I get a `TypeError` about a missing keyword argument on `add_port`

`add_port` requires the `port=` keyword — passing a port positionally raises a `TypeError`:

```
# Wrong
c.add_port(instance.ports["o1"])

# Correct
c.add_port(port=instance.ports["o1"])
```

---

### How do I rename a port when exposing it from a sub-cell?

```
c.add_port(port=instance.ports["o1"], name="in")
```

Do **not** use `port.copy("new_name")` — that creates a detached copy not linked to the instance.

---

### What are the port angle integers?

KLayout integer angles: `0` = East (0°), `1` = North (90°), `2` = West (180°), `3` = South (270°).

A port's angle is the **outward** direction — the direction a wire *exits* at that port.

---

## Layers &amp; KCLayout

### My layer indices are wrong when I mix PDK cells with global `kf.kcl` cells

Each `KCLayout` has its own layer-index table. A port or shape created under `kf.kcl` carries layer index 0 for `WG`; a different `KCLayout` may map `WG` to index 5.

**Fix**: always pass `kcl=pdk` when constructing `kf.Port` inside a PDK cell:

```
c.add_port(
    port=kf.Port(
        name="o1",
        trans=kf.kdb.Trans(0, False, 0, 0),
        width=500,
        layer=pdk.find_layer(L.WG),
        kcl=pdk,  # <-- critical
    )
)
```

---

### `KCLayout` isn't seeing my layers even though I set `kcl.infos`

Pass the **class** (not an instance) to `KCLayout.__init__`:

```
# Correct — pass the class
pdk = kf.KCLayout("my_pdk", infos=LAYER)

# Wrong — setting after construction does not fully propagate
pdk = kf.KCLayout("my_pdk")
pdk.infos = LAYER()  # ← incomplete
```

After construction `pdk.infos` holds the instance; the `infos=` constructor argument triggers internal layer registration that the post-hoc assignment skips.

---

### Where do I import `LayerLevel`, `LayerStack`, `layerenum_from_dict`?

These are **not** re-exported from top-level `kf`. Import them from the sub-module:

```
from kfactory.layer import LayerLevel, LayerStack, layerenum_from_dict
```

---

## Caching &amp; `@kf.cell`

### My `@kf.cell` function raises `TypeError: unhashable type`

All arguments passed to a `@kf.cell` function must be hashable (Python can cache them as dict keys). Common offenders:

| Type | Fix |
| --- | --- |
| `list` | Use `tuple` |
| `dict` | Use a frozen dataclass or a named tuple |
| `LayerInfo` object | Use `LayerInfo` directly — it *is* hashable |
| `CrossSection` object | Pass the name string; look it up inside with `kcl.get_icross_section(name)` |

---

### Why does `@pdk.cell` work but `@kf.cell` raises `ValueError: must use same KCLayout`?

When a cell factory creates `KCell(kcl=pdk)` the decorator must match. `@kf.cell` registers cells in `kf.kcl`; `@pdk.cell` registers them in `pdk`. Mix-up causes a layout-ownership mismatch at registration time.

**Rule**: use `@pdk.cell` (or `kf.cell(kcl=pdk)`) whenever your factory body creates cells under a custom `KCLayout`.

---

## Routing

### What is the difference between the nominal radius and the effective radius?

Euler (clothoid) bends are longer than a circular arc of the same nominal radius because the curvature ramps up gradually. Their physical footprint extends further than the radius you passed to the factory.

Use `kf.routing.optical.get_radius(bend_cell.ports)` for the actual footprint radius **in DBU**. Convert to µm before passing it to a µm-based factory or comparing it with the nominal radius:

```
bend90 = kf.factories.euler.bend_euler_factory(kcl=kf.kcl)(
    width=0.5,
    radius=10,
    layer=L.WG,
    angle=90,
)
radius_dbu = kf.routing.optical.get_radius(bend90.ports)
radius_um = bend90.kcl.to_um(radius_dbu)  # > 10 µm for euler bends
```

For circular bends, `radius_um` equals the nominal radius up to DBU rounding. DBU-based routing functions take `radius_dbu` directly.

---

### Path-length matching loops collide — how do I fix it?

Path-length matching inserts S-loops into shorter routes. Each loop needs room:

- Routes must be spaced ≥ 150 µm apart horizontally.
- Routes must already contain at least one bend (purely straight routes have no room for loop insertion).

Increase the horizontal pitch between ports, or add explicit waypoints to force bends before the matching section.

---

### `route_bundle` shows a KLayout error dialog during headless builds

Collision detection calls `kdb.show_error()` which spawns a dialog in GUI mode. Suppress it for CI / notebook execution:

```
kf.routing.optical.route_bundle(
    c,
    start_ports,
    end_ports,
    bend90_cell=bend90,
    straight_factory=sf,
    on_collision=None,  # <-- suppress dialog
)
```

---

### `route_smart` raises a `ValueError` about `BasePort`

`route_smart` expects kfactory's internal `BasePort` (a Pydantic model), **not** a `kf.Port`. Use `route_manhattan` + `place_manhattan` directly for low-level single-route control instead.

---

### All-angle bundle fails with "not enough space"

Each backbone segment between waypoints must be at least 2× the effective bend radius. If your waypoints are too close together the router cannot fit the entry and exit bends. Space backbone waypoints further apart.

---

## Enclosures

### My cladding doesn't follow the taper profile — it's rectangular

Use `apply_minkowski_y` which morphologically expands along the Y axis, following non-rectangular outlines:

```
enc = kf.LayerEnclosure(sections=[(L.WGCLAD, 2_000)])
enc.apply_minkowski_y(c)  # call after shapes are drawn
```

`apply_minkowski_tiled` (the default) works on the merged bounding region and can produce rectangular results for tapered shapes.

---

### `LayerEnclosure` with `dsections=` raises a conversion error

`dsections=` uses µm values and needs a layout context to convert them to DBU. Pass `kcl=` at construction time:

```
enc = kf.LayerEnclosure(
    dsections=[(L.WGCLAD, 0, 2.0)],
    kcl=kf.kcl,  # <-- required for µm→DBU conversion
)
```

---

## Utilities

### `fill_tiled` doesn't appear to do anything

`fill_tiled` modifies the target cell **in-place** and returns `None`. It must be called while the cell is unlocked, i.e., **inside** the `@kf.cell` function body (not after the decorator has finalised the cell).

---

### `packing.pack_kcells` ignores my `spacing` value

`spacing`, `max_width`, and `max_height` are all in **DBU**, not µm. Convert first:

```
kf.packing.pack_kcells(
    cells,
    spacing=kf.kcl.to_dbu(2),  # 2 µm → DBU
    max_width=kf.kcl.to_dbu(500),
)
```

---

### `inst.dmove((x, y))` works but `inst.dmove(kf.kdb.DVector(x, y))` raises `TypeError`

`dmove` unpacks its argument as a 2-tuple internally. Pass a plain Python tuple or a 2-element sequence, not a `kdb.DVector`:

```
inst.dmove((dx, dy))  # correct
inst.dmove(kf.kdb.DVector(dx, dy))  # TypeError
```

---

## Schematics

### `create_inst` raises a JSON serialisation error

Settings passed to `create_inst` must be JSON-serialisable. Do **not** pass `LayerInfo` objects — use `int`, `float`, or `str` instead:

```
# Wrong
sch.create_inst("wg", settings={"layer": L.WG})

# Correct
sch.create_inst("wg", settings={"layer": (1, 0)})
```

---

## Difftest / regression testing

### `difftest()` raises `AssertionError` on the first run

This is expected — on the first run there is no reference GDS file to compare against. The reference is written by that first run. Re-run your test suite a second time; it will pass once the reference exists.

Do **not** call `difftest()` in executable notebook pages — it will always fail in a clean CI environment. Show it as a code comment instead.

## See Also

| Topic | Where |
| --- | --- |
| Common pitfalls with code examples | [How-To: Best Practices](<https://gdsfactory.github.io/kfactory/3.5.0/howto/best_practices/index.md>) |
| Common design patterns | [How-To: Patterns](<https://gdsfactory.github.io/kfactory/3.5.0/howto/patterns/index.md>) |
| Contributing to kfactory | [How-To: Contributing](<https://gdsfactory.github.io/kfactory/3.5.0/howto/contributing/index.md>) |
| DBU vs µm coordinate systems | [Core Concepts: DBU vs µm](<https://gdsfactory.github.io/kfactory/3.5.0/concepts/dbu_vs_um/index.md>) |
