Skip to content

Utilities

utilities

as_png_data

as_png_data(
    c: ProtoTKCell[Any],
    layer_properties: str | Path | None = None,
    resolution: tuple[int, int] = (800, 600),
    synchronous: bool = True,
    markers: list[tuple[DShapeLike, MarkerConfig]]
    | None = None,
) -> bytes

Render a cell to PNG bytes via a headless lay.LayoutView.

Parameters:

Name Type Description Default
c ProtoTKCell[Any]

cell to render.

required
layer_properties str | Path | None

optional .lyp to apply.

None
resolution tuple[int, int]

(width, height) in pixels.

(800, 600)
synchronous bool

True to render synchronously (default), False to return whatever the view currently has.

True
markers list[tuple[DShapeLike, MarkerConfig]] | None

optional list of (shape, config) pairs. Each shape becomes a lay.Marker overlay on the view; config is the same MarkerConfig dict that kfactory.show accepts (color, line_width, halo, …). When markers are supplied, the view zooms to the union of all marker bounding boxes expanded by 10 % instead of fitting the full cell.

None
Source code in kfactory/utilities.py
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
def as_png_data(
    c: ProtoTKCell[Any],
    layer_properties: str | Path | None = None,
    resolution: tuple[int, int] = (800, 600),
    synchronous: bool = True,
    markers: list[tuple[DShapeLike, MarkerConfig]] | None = None,
) -> bytes:
    """Render a cell to PNG bytes via a headless ``lay.LayoutView``.

    Args:
        c: cell to render.
        layer_properties: optional ``.lyp`` to apply.
        resolution: ``(width, height)`` in pixels.
        synchronous: ``True`` to render synchronously (default), ``False`` to
            return whatever the view currently has.
        markers: optional list of ``(shape, config)`` pairs. Each shape becomes
            a ``lay.Marker`` overlay on the view; ``config`` is the same
            ``MarkerConfig`` dict that `kfactory.show` accepts (``color``,
            ``line_width``, ``halo``, …). When markers are supplied, the view
            zooms to the union of all marker bounding boxes expanded by 10 %
            instead of fitting the full cell.
    """
    layout_view = lay.LayoutView()
    layout_view.show_layout(c.kcl.layout.dup(), False)
    if layer_properties is not None:
        layer_properties = Path(layer_properties)
        if layer_properties.exists() and layer_properties.is_file():
            layout_view.load_layer_props(str(layer_properties))
    elif c.kcl.technology_file is not None:
        layout_view.active_cellview().technology = c.kcl.technology.name
    layout_view.active_cellview().cell = c.kdb_cell
    layout_view.max_hier()
    layout_view.resize(*resolution)
    layout_view.add_missing_layers()

    # Keep marker references alive — klayout drops markers whose Python
    # handle has been garbage-collected before the screenshot is taken.
    marker_refs: list[lay.Marker] = []
    if markers:
        bbox = kdb.DBox()
        for shape, cfg in markers:
            m = lay.Marker(layout_view)
            if isinstance(shape, kdb.DPolygon | kdb.DSimplePolygon):
                m.set_polygon(
                    shape if isinstance(shape, kdb.DPolygon) else kdb.DPolygon(shape)
                )
            elif isinstance(shape, kdb.DBox):
                m.set_box(shape)
            elif isinstance(shape, kdb.DEdge):
                m.set_edge(shape)
            elif isinstance(shape, kdb.DPath):
                m.set_path(shape)
            elif isinstance(shape, kdb.DText):
                m.set_text(shape)
            else:
                continue
            if (color := cfg.get("color")) is not None:
                m.color = color
            if (frame_color := cfg.get("frame_color")) is not None:
                m.frame_color = frame_color
            if (line_width := cfg.get("line_width")) is not None:
                m.line_width = line_width
            if (line_style := cfg.get("line_style")) is not None:
                m.line_style = line_style
            if (halo := cfg.get("halo")) is not None:
                m.halo = halo
            if (vertex_size := cfg.get("vertex_size")) is not None:
                m.vertex_size = vertex_size
            if (dither_pattern := cfg.get("dither_pattern")) is not None:
                m.dither_pattern = dither_pattern
            if (dismissable := cfg.get("dismissable")) is not None:
                m.dismissable = dismissable
            bbox += shape.bbox()
            marker_refs.append(m)

        if not bbox.empty():
            pad_x = bbox.width() * 0.1 or 1.0
            pad_y = bbox.height() * 0.1 or 1.0
            layout_view.zoom_box(bbox.enlarged(pad_x, pad_y))
        else:
            layout_view.zoom_fit()
    else:
        layout_view.zoom_fit()

    if synchronous:
        return layout_view.get_pixels_with_options(
            width=resolution[0], height=resolution[1]
        ).to_png_data()
    return layout_view.get_screenshot_pixels().to_png_data()

check_cell_ports

check_cell_ports(
    p1: ProtoPort[Any], p2: ProtoPort[Any]
) -> int

Check if two ports are the same.

Returns:

Name Type Description
int int

A bitwise representation of the differences between the two ports.

Source code in kfactory/utilities.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def check_cell_ports(p1: ProtoPort[Any], p2: ProtoPort[Any]) -> int:
    """Check if two ports are the same.

    Returns:
        int: A bitwise representation of the differences between the two ports.
    """
    from .port import Port

    p1_ = Port(base=p1.base)
    p2_ = Port(base=p2.base)
    check_int = 0
    if p1_.width != p2_.width:
        check_int += 1
    if p1_.angle != p2_.angle:
        check_int += 2
    if p1_.port_type != p2_.port_type:
        check_int += 4
    return check_int

check_inst_ports

check_inst_ports(p1: Port, p2: Port) -> int

Check if two ports are the same.

Returns:

Name Type Description
int int

A bitwise representation of the differences between the two ports.

Source code in kfactory/utilities.py
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def check_inst_ports(p1: Port, p2: Port) -> int:
    """Check if two ports are the same.

    Returns:
        int: A bitwise representation of the differences between the two ports.
    """
    check_int = 0
    if p1.width != p2.width:
        check_int += 1
    if p1.angle != ((p2.angle + 2) % 4):
        check_int += 2
    if p1.port_type != p2.port_type:
        check_int += 4
    return check_int

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
76
77
78
79
80
81
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])

ensure_build_directory

ensure_build_directory(
    subdirectory: str = "mask",
    create_gitignore: bool = True,
) -> Path | None

Ensure build directory exists with proper gitignore.

This function consolidates all build directory creation logic, including git repository detection and .gitignore creation.

Parameters:

Name Type Description Default
subdirectory str

Subdirectory under build/ (e.g., 'mask', 'session/kcls').

'mask'
create_gitignore bool

Whether to create .gitignore in build directory.

True

Returns:

Type Description
Path | None

Path to the build subdirectory or None if not in a git repo.

Source code in kfactory/utilities.py
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
def ensure_build_directory(
    subdirectory: str = "mask", create_gitignore: bool = True
) -> Path | None:
    """Ensure build directory exists with proper gitignore.

    This function consolidates all build directory creation logic, including
    git repository detection and .gitignore creation.

    Args:
        subdirectory: Subdirectory under build/ (e.g., 'mask', 'session/kcls').
        create_gitignore: Whether to create .gitignore in build directory.

    Returns:
        Path to the build subdirectory or None if not in a git repo.
    """
    project_dir = config.project_dir
    if not project_dir:
        return None

    build_dir = Path(project_dir) / "build"
    target_dir = build_dir / subdirectory
    target_dir.mkdir(parents=True, exist_ok=True)

    if create_gitignore:
        gitignore_path = build_dir / ".gitignore"
        if not gitignore_path.exists():
            gitignore_path.write_text("*\n")

    return target_dir

get_build_path

get_build_path(
    filename: str,
    subdirectory: str = "mask",
    file_format: str = "gds",
) -> tuple[Path, bool]

Get the appropriate build path for a file.

Determines whether to use a git-tracked build directory or temp directory, and creates necessary directories.

Parameters:

Name Type Description Default
filename str

Base filename (without extension).

required
subdirectory str

Subdirectory under build/ (e.g., 'mask', 'gds', 'oas').

'mask'
file_format str

File extension/format (e.g., 'gds', 'oas', 'lyrdb').

'gds'

Returns:

Type Description
Path

Tuple of (file_path, should_delete):

bool
  • If in git repo: returns build directory path, False
tuple[Path, bool]
  • Otherwise: returns temp directory path, True
Source code in kfactory/utilities.py
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
def get_build_path(
    filename: str, subdirectory: str = "mask", file_format: str = "gds"
) -> tuple[Path, bool]:
    """Get the appropriate build path for a file.

    Determines whether to use a git-tracked build directory or temp directory,
    and creates necessary directories.

    Args:
        filename: Base filename (without extension).
        subdirectory: Subdirectory under build/ (e.g., 'mask', 'gds', 'oas').
        file_format: File extension/format (e.g., 'gds', 'oas', 'lyrdb').

    Returns:
        Tuple of (file_path, should_delete):
        - If in git repo: returns build directory path, False
        - Otherwise: returns temp directory path, True
    """
    from tempfile import gettempdir

    build_dir = ensure_build_directory(subdirectory)

    if build_dir:
        filepath = build_dir / Path(filename).with_suffix(f".{file_format}")
        filepath.parent.mkdir(parents=True, exist_ok=True)
        return filepath, False
    filepath = Path(gettempdir()) / Path(filename).with_suffix(f".{file_format}")
    filepath.parent.mkdir(parents=True, exist_ok=True)
    return filepath, True

get_session_directory

get_session_directory(
    custom_dir: Path | None = None,
) -> Path

Get or create session cache directory.

Parameters:

Name Type Description Default
custom_dir Path | None

Optional custom directory override.

None

Returns:

Type Description
Path

Path to session/kcls directory.

Source code in kfactory/utilities.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
def get_session_directory(custom_dir: Path | None = None) -> Path:
    """Get or create session cache directory.

    Args:
        custom_dir: Optional custom directory override.

    Returns:
        Path to session/kcls directory.
    """
    if custom_dir:
        return custom_dir

    build_dir = ensure_build_directory("session/kcls", create_gitignore=True)
    if build_dir:
        return build_dir
    # Fallback to current directory
    return Path() / "build/session/kcls"

instance_port_name

instance_port_name(
    inst: Instance, port: ProtoPort[Any]
) -> str

Create a name for an instance port.

Parameters:

Name Type Description Default
inst Instance

The instance.

required
port ProtoPort[Any]

The port.

required
Source code in kfactory/utilities.py
120
121
122
123
124
125
126
127
def instance_port_name(inst: Instance, port: ProtoPort[Any]) -> str:
    """Create a name for an instance port.

    Args:
        inst: The instance.
        port: The port.
    """
    return f'{inst.name}["{port.name}"]'

load_layout_options

load_layout_options(
    **attributes: Any,
) -> kdb.LoadLayoutOptions

Default options for loading GDS/OAS.

Parameters:

Name Type Description Default
attributes Any

Set attributes of the layout load option object. E.g. to set the handling of cell name conflicts pass cell_conflict_resolution=kdb.LoadLayoutOptions.CellConflictResolution.OverwriteCell.

{}
Source code in kfactory/utilities.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def load_layout_options(**attributes: Any) -> kdb.LoadLayoutOptions:
    """Default options for loading GDS/OAS.

    Args:
        attributes: Set attributes of the layout load option object. E.g. to set the
            handling of cell name conflicts pass
            `cell_conflict_resolution=kdb.LoadLayoutOptions.CellConflictResolution.OverwriteCell`.
    """
    load = kdb.LoadLayoutOptions()

    load.cell_conflict_resolution = (
        kdb.LoadLayoutOptions.CellConflictResolution.SkipNewCell
    )
    for k, v in attributes.items():
        setattr(load, k, v)

    return load

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
68
69
70
71
72
73
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_pins

pprint_pins(
    pins: Iterable[Pin] | Iterable[DPin],
    unit: Literal["dbu", "um"] | None = None,
) -> Table

Print ports as a table.

Parameters:

Name Type Description Default
pins Iterable[Pin] | Iterable[DPin]

The pins 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
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
def pprint_pins(
    pins: Iterable[Pin] | Iterable[DPin], unit: Literal["dbu", "um"] | None = None
) -> Table:
    """Print ports as a table.

    Args:
        pins: The pins 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("Pin Type")
    table.add_column("Ports")
    table.add_column("Info")

    for pin in pins:
        ports = pprint_ports(pin.ports, unit=unit)
        ports.box = None
        table.add_row(
            str(pin.name),
            str(pin.pin_type),
            ports,
            JSON.from_data(pin.info.model_dump()),
        )

    return table

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
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
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("Type")
    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(),
                        port.port_type,
                        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(),
                        port.port_type,
                        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(),
                    dport.port_type,
                    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(),
                    iport.port_type,
                    f"{iport.x:_}",
                    f"{iport.y:_}",
                    str(iport.angle),
                    str(iport.mirror),
                    JSON.from_data(iport.info.model_dump()),
                )

    return table

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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.write_context_info = config.write_context_info
    save.gds2_write_cell_properties = config.write_cell_properties
    save.gds2_write_file_properties = config.write_file_properties
    save.gds2_write_timestamps = config.write_timestamps
    save.gds2_max_cellname_length = config.max_cellname_length

    for k, v in attributes.items():
        setattr(save, k, v)

    return save

update_default_trans

update_default_trans(
    new_trans: dict[
        str,
        str | int | float | dict[str, str | int | float],
    ],
) -> None

Allows to change the default transformation for reading a yaml file.

Source code in kfactory/utilities.py
61
62
63
64
65
def update_default_trans(
    new_trans: dict[str, str | int | float | dict[str, str | int | float]],
) -> None:
    """Allows to change the default transformation for reading a yaml file."""
    DEFAULT_TRANS.update(new_trans)