Cells and hierarchy

Place reusable sub-cells with Instance, inspect array copies, and write compact hierarchy.

A Cell is a named container of geometry. A design with only one giant cell works, but it is slow in the viewer, large on disk, and painful to edit. Breaking things into sub-cells and referring to them keeps the GDS small and the design readable.

This guide covers:

For the mental model behind cells and instances, see Core concepts.

Placing a sub-cell

The usual recipe is: build a sub-cell, place it with .at(x, y), and add it to a parent.

from rosette import Cell, Layer, Point, Polygon
from rosette.io import write_gds

# A sub-cell: a 10 x 0.5 um waveguide stub.
stub = Cell("stub")
stub.add_polygon(Polygon.rect(Point(0, -0.25), 10, 0.5), Layer(1, 0))

# A top cell that places two copies of the stub.
top = Cell("top")
top.add_ref(stub.at(0,   0))
top.add_ref(stub.at(0,  20))

write_gds("output.gds", top)

stub.at(x, y) returns an Instance: a cell plus a transform. add_ref wires it into the parent and silently registers stub as a child of top so write_gds knows to include it.

You can rotate, mirror, and scale instances by chaining:

# Rotate the stub 90 degrees, then place it at (50, 0).
top.add_ref(stub.at(0, 0).rotate(90).translate(50, 0))

Chaining order matters

Each chained call wraps the outside of the accumulated transform, so the first call happens first. cell.at(10, 0).rotate(90) translates to (10, 0) and then rotates around the origin, so the cell ends up at (0, 10), not (10, 0). To rotate then place, do cell.at(0, 0).rotate(90).translate(50, 0).

Port queries on instances

Instance.port(name) returns the port with position and direction transformed into world space. No need to pass the underlying cell twice.

from rosette import Cell, Layer, Point, Polygon, Port, Vector2

# A cell with a single output port.
src = Cell("src")
src.add_polygon(Polygon.rect(Point(0, -0.25), 10, 0.5), Layer(1, 0))
src.add_port(Port("out", Point(10, 0), Vector2(1, 0), width=0.5))

# Place it at (50, 0). The port moves with it.
inst = src.at(50, 0)
out = inst.port("out")        # position=(60, 0), direction=(1, 0)

This is the fundamental pattern for routing between components. See the Routing guide.

Arrays

For regular grids of the same cell, use Instance.array(cols, rows, col_spacing, row_spacing). This emits a single GDS AREF, not cols * rows individual references.

from rosette import Cell, Layer, Point, Polygon

unit = Cell("unit")
unit.add_polygon(Polygon.rect(Point(-2.5, -2.5), 5, 5), Layer(1, 0))

grid = Cell("grid")
# 4 columns x 3 rows with 20 um pitch in both axes.
grid.add_ref(unit.at(0, 0).array(4, 3, 20.0, 20.0))

The viewer selects the whole array as one object, and the GDS stays tiny no matter how many copies you stamp out.

Iterating over array copies

If you need per-copy labels, per-copy routing endpoints, or a netlist, call instance.copies(). Each yielded ArrayCopy is a read-only observational view, not another placeable object. Add the parent Instance once, then use its copies for queries. Iteration does not add extra GDS references, so the hierarchy stays compact.

bank = unit.at(0, 0).array(4, 3, 20.0, 20.0)
grid.add_ref(bank)                   # one AREF

for copy in bank.copies():
    grid.add_text(
        f"{copy.col},{copy.row}",
        copy.position,
        Layer(10, 0),
    )

For non-orthogonal lattices (hex packings, skewed test arrays) use Instance.array_vectors instead and pass two Vector2 displacement vectors.

Auto-collection in write_gds

You rarely need to build a Library explicitly. When you call write_gds(path, top) on a cell that was built with add_ref(instance), Rosette walks the tracked child cells and writes them all out:

write_gds("output.gds", top)   # stub, unit, grid, top are all included

A build summary is printed to stderr by default. Pass quiet=True to suppress it or verbose=True to see port positions.

When do you need an explicit Library?

  • You are writing utilities that operate on collections of cells programmatically.
  • You are reading GDS: read_gds returns a Library, and you can iterate it with lib.cells().
  • You called run_drc, run_checks, or run_dfm on a cell from an imported or explicitly managed hierarchy. Pass its Library via the library= parameter so the checks can resolve referenced cells.

A GDS file can contain multiple independent designs. lib.roots() returns every cell that is not referenced by another cell. lib.top_cell() returns a unique root automatically, but returns None when several roots are possible. Use lib.set_top_cell(name) to choose the entry cell for rendering or other default operations.

One placement model

Instance, returned by cell.at(...), is the public placement abstraction. It tracks the underlying Cell, supports inst.port(name), arrays, and transform chaining, and feeds into write_gds auto-collection. ArrayCopy only observes one copy in an array; it is read-only and cannot be passed to add_ref.

Validated local model

Rosette validates local model state at Python mutation and serialization boundaries. Invalid Python input raises ValueError before changing a cell, instance, reference list, or tracked child set:

  • Polygons require at least three finite vertices. This does not impose a topology policy: repeated vertices, zero-area rings, and self-intersections remain representable. Polygon transforms also reject non-finite results.
  • Ports require nonempty names, finite positions, finite nonzero directions, and optional positive finite widths. Directions are normalized, and names are unique within each cell.
  • Paths require at least two finite points and a finite nonzero width. Negative widths remain absolute under reference magnification.
  • Text positions must be finite and text heights must be positive and finite.
  • Instance and native reference transforms must be finite and invertible; scale factors must be nonzero. Array counts are [1, 32767], and lattice spacings/vectors must be finite. Negative and zero finite spacings are valid.

Rosette JSON uses the versioned rosette-layout schema owned by rosette-io. Schema V1 declares micrometer, Y-up coordinates and preserves explicit top-cell selection. Its wire shape is unchanged. Internally, a typed LayoutDocument keeps editor, route, and DRC annotations in sidecars outside the format-neutral core library. This preserves annotations in JSON without exposing path length, bends, warnings, skips, or waivers as public Cell metadata.

Unversioned core-struct JSON is not accepted. JSON and GDS import reject malformed local model state, while export validates before writing. GDS export also checks format-specific limits such as coordinate ranges and representable reference transforms. Local validation does not require every reference target to exist and does not reject hierarchy cycles; hierarchy operations report those graph-level issues separately.

Putting it together

A 2x2 grid of a sub-cell, with text labels per copy:

from rosette import Cell, Layer, Point, Polygon
from rosette.io import write_gds

# Sub-cell: a 5 x 5 um square.
unit = Cell("unit")
unit.add_polygon(Polygon.rect(Point(-2.5, -2.5), 5, 5), Layer(1, 0))

# Top cell: one AREF + per-copy labels.
top = Cell("top")
arr = unit.at(0, 0).array(2, 2, 20.0, 20.0)
top.add_ref(arr)

for copy in arr.copies():
    top.add_text(
        f"{copy.col},{copy.row}",
        copy.position,
        Layer(10, 0),
        height=1.0,
    )

write_gds("output/grid.gds", top)

See also

On this page