Core Concepts
The mental model behind Rosette. Cells, layers, ports, geometry, and routing.
This page is the single spot where the core ideas of Rosette are introduced. If you have used other layout tools before, most of this will feel familiar. Skim the headings and focus on the naming and quirks. If this is your first layout tool, read top-to-bottom and keep the API Reference open in another tab.
Everything below is about the Python API. Units are micrometers (um) and angles are degrees.
Cells, instances, and libraries
A Cell is the basic unit of a design. It is a named container that holds polygons, paths, text labels, ports, and references to other cells. Cells are how you break a chip up into reusable pieces: a grating coupler is a cell, a ring resonator is a cell, and the whole chip is a cell that references the other two.
from rosette import Cell, Layer, Point, Polygon
from rosette.io import write_gds
inner = Cell("ring")
inner.add_polygon(Polygon.rect(Point(0, 0), 20, 2), Layer(1, 0))
top = Cell("top")
top.add_ref(inner.at(0, 0))
top.add_ref(inner.at(0, 30))
write_gds("output.gds", top)An Instance is a placed cell: a cell
plus a transform (translation, rotation, mirror). You create instances
with cell.at(x, y) and add them to a parent with parent.add_ref(...).
Chained calls apply in order, so cell.at(10, 0).rotate(90) first
translates to (10, 0) and then rotates around the origin, landing
the cell at (0, 10). See the
Instance docs for the full transform
order rules.
For large regular grids, use Instance.array.
One compact GDS array reference replaces thousands of individual refs. When
you need to inspect a specific copy, iteration yields read-only
ArrayCopy views. They expose per-copy ports
and transforms but are not independently placeable.
A Library is a collection of cells that
travel together when reading or writing GDS. In most designs you never
construct a Library directly: write_gds
auto-collects child cells from a top-level Cell built with Instance
references. Imported libraries may have several graph-derived roots;
Library.roots() exposes all of them, while an explicit top selection chooses
the default entry cell for rendering.
When to make a sub-cell
If you are going to place the same shape more than once, make it a cell. Hierarchy keeps your GDS small, your viewer fast, and your intent clear. Merging everything into one giant cell works, but it scales badly.
Layers
A Layer is a (number, datatype) pair that
GDS uses to separate materials: silicon, metal, doping, text, and so on.
The canonical way to construct one is:
from rosette import Layer
Layer(1, 0) # number=1, datatype=0
Layer(1) # datatype defaults to 0Other accepted forms
Anywhere a Layer is expected (e.g. cell.add_polygon), a bare int
like 1 or a tuple like (1, 0) is also accepted and coerced. Prefer
the explicit Layer(...) constructor in design scripts. It reads more
clearly and is the form the rest of these docs use.
For real designs you will want to name your layers. Define them in
rosette.toml under [layers]:
[layers.silicon]
number = 1
datatype = 0
color = "#ff69b4"
description = "Silicon waveguides"
[layers.text]
number = 10
datatype = 0
color = "#607d8b"Then load them in Python with
load_layer_map:
from rosette import Cell, Point, Polygon
from rosette.project import load_layer_map
layers = load_layer_map() # reads rosette.toml
cell = Cell("wg")
cell.add_polygon(
Polygon.rect(Point(0, -0.25), 50, 0.5),
layers.silicon.layer, # Layer(1, 0)
)
print(layers.silicon.color) # "#ff69b4"load_layer_map() returns a LayerMap
whose entries are LayerInfo objects.
Each exposes the Layer, a color, and metadata used by the viewer.
Semantic names everywhere
Everywhere Rosette asks for a layer (add_polygon, DRC config, DFM
config, and so on) you can use the semantic name you defined in
[layers] instead of the raw number/datatype. Keep design scripts
readable, keep foundry numbers in one place.
Ports
A Port is a named connection point on a cell. It has a position, an outward-facing direction (unit vector), and an optional width.
from rosette import Cell, Layer, Point, Polygon, Port, Vector2
cell = Cell("wg_segment")
cell.add_polygon(Polygon.rect(Point(0, -0.25), 100, 0.5), Layer(1, 0))
cell.add_port(Port("in", Point(0, 0), Vector2(-1, 0), width=0.5))
cell.add_port(Port("out", Point(100, 0), Vector2( 1, 0), width=0.5))Ports do three jobs:
- Routing anchors. The
RouteAPI uses ports as start and end points. Seestart_at_portandend_at_port. The route departs and arrives along the port's direction, at the port's width. - Connectivity checks.
uv run rosette check(the design-checks feature) flags ports that are supposed to connect but don't: typos in routing, width mismatches, angle mismatches. - Placed-cell queries.
Instance.porttransforms a cell's local ports into world space, ready to use as routing endpoints withRoute.
Top-level ports are treated as external I/O and are not flagged as unconnected. Use them to mark the chip's inputs and outputs.
Geometry primitives
You rarely touch these directly (Polygon is the one you will use most),
but every other API is built on top of them.
Point(x, y): a 2D point. UsePoint.origin()for(0, 0).Vector2(x, y): a 2D vector, used for port directions and array lattice vectors.Polygon: a closed polygon. Build one withPolygon.rect(origin, width, height),Polygon.rect_centered(center, width, height), or pass a list of points.BBox: an axis-aligned bounding box, returned bycell.bbox()andLibrary.cell_bbox(name).Transform: a 2D affine transform (translate + rotate + scale + mirror). Instance transforms are composed from these under the hood.
Use Cell.add_path() for a constant-width
layout path. Use Route.to(..., width=...) when
a routed segment should transition to a different width.
Routing
A Route connects a sequence of waypoints
with straight segments, inserting bends at corners and interpolating width
changes across segments. It is not an auto-router. You supply the waypoints. A
minimal route between two ports looks like:
from rosette import Cell, Layer, Point, Port, Vector2
from rosette.routing import Route
src = Port("out", Point(0, 0), Vector2( 1, 0), width=0.5)
dst = Port("in", Point(120, 30), Vector2(-1, 0), width=0.5)
route = Route(Layer(1, 0), width=0.5, bend_radius=5.0)
route.start_at_port(src)
route.to(60, src.position.y)
route.to(60, dst.position.y)
route.end_at_port(dst)
top = Cell("top")
top.add_ref(route.to_cell("wg").at(0, 0))The full story (Euler vs. circular bends, width transitions, common pitfalls) lives in the Routing guide.