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:
- Placing sub-cells with
cell.at(x, y)andadd_ref - Using
Instanceas the placement model - Arraying cells with
Instance.arrayand observing copies withArrayCopy - Auto-collection in
write_gdsand when you need aLibrary
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 includedA 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_gdsreturns aLibrary, and you can iterate it withlib.cells(). - You called
run_drc,run_checks, orrun_dfmon a cell from an imported or explicitly managed hierarchy. Pass itsLibraryvia thelibrary=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
Cell: cell API referenceInstance: transforms, arrays, portsArrayCopy: read-only per-copy array viewsLibrary: explicit collections of cells- Core concepts: the overall mental model