Tutorial: Your First Layout
In this tutorial, you'll learn the fundamentals of DeviceLayout.jl by creating a Vernier scale for measuring alignment between two layers.
What You'll Learn
- Creating and manipulating basic geometry entities
- Understanding cells and rendering
- Aligning geometry entities
- Working with references
- Using polygon Boolean operations
- Using CoordinateSystems and semantic metadata
- Exporting geometry to GDSII
Prerequisites
- Getting started: Installing Julia and setting up your development environment
Setup
You'll work interactively in the Julia REPL. If using VS Code, you can use the built-in REPL from the Julia extension with Alt+J Alt+O (or Option+J Option+O on Mac), which will display graphical previews of geometry in a separate tab when a Cell or CoordinateSystem is returned by REPL execution. Otherwise, you can save any Cell to GDSII and view it in KLayout or another program, or save to .svg and open it in a graphics viewer or browser.
Create a directory for this tutorial project and start the Julia REPL with julia. Type ] to enter the REPL mode for the package manager. The name of the current environment appears in parentheses at the left of the prompt. This will be the default environment for your Julia version (e.g., (@v1.12) pkg>). Activate the environment for the current directory with pkg> activate ..
Check to see whether DeviceLayout and FileIO are present in the environment with pkg> status. If you are using a new project directory, this will show that no packages are installed. In that case, run pkg> add DeviceLayout FileIO. Leave the Pkg REPL mode by pressing backspace with an empty prompt.
Finally, load DeviceLayout.jl:
using DeviceLayout
using DeviceLayout.PreferredUnits # for mm, μm, nm, etc
using FileIO # for `save`Step 1: Creating a scale mark
DeviceLayout.jl provides several primitive shapes (GeometryEntity). Let's create a Rectangle for one mark on a Vernier scale:
mark = Rectangle(5μm, 15μm) # lower left corner at (0, 0)Rectangle{Unitful.Quantity{Int64, 𝐋, Unitful.ContextUnits{(μm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((0 μm,0 μm), (5 μm,15 μm))You can enter μ by typing \mu and pressing Tab.
We'd like this to be centered on the Y axis, so we'll move it to the left a bit:
mark = mark - Point(2.5μm, 0μm) # now centered on y axisRectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(μm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((-2.5 μm,0.0 μm), (2.5 μm,15.0 μm))To create actual layout data, shapes need to be associated with layers. To do this, we can create a Cell, a coordinate system corresponding to a GDSII cell, and render! our shape to the cell in our desired layer, represented as GDSMeta(layer, datatype).
mark_cell = Cell("single_mark")
render!(mark_cell, mark, GDSMeta(0, 0)) # layer 0, datatype 0If you're using VS Code the Julia extension's built-in REPL, this should display a rectangle in a new tab. This is not a live preview of your Cell, but a new graphical preview is generated whenever the Cell is returned by REPL execution, as by running julia> mark_cell.
Otherwise, you can view your layout by saving to GDSII and opening it in a standalone viewer like KLayout.
save("single_mark.gds", mark_cell);Note that shapes rendered to a Cell are converted into Polygons:
elements(mark_cell)1-element Vector{Polygon{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}}:
Polygon{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}(Point{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}[(-2500.0 nm,0.0 nm), (2500.0 nm,0.0 nm), (2500.0 nm,15000.0 nm), (-2500.0 nm,15000.0 nm)])Step 2: Creating an array of marks
We can create an array of marks for one of the scale's rulers by rendering several rectangles to a Cell.
mark_pitch = 10μm
ruler_range = -5:5
ruler_cell = Cell("single_ruler")
for i in ruler_range
render!(ruler_cell, mark + i*Point(mark_pitch, 0μm), GDSMeta(0, 0))
end
ruler_cellHowever, arrays of identical structures can be more efficiently represented as an ArrayReference, using aref to create the reference and addref! to place it in a cell:
ruler_cell = Cell("single_ruler") # New empty cell
addref!(ruler_cell, aref(mark_cell, ruler_range * mark_pitch, (0:0)μm))
ruler_cellWe'll extend the center mark for visual clarity when reading the scale. We can use a reference to position an extra mark rectangle above the center mark, this time with sref (for "structure" or "single" reference):
# Use `addref!` with just the referenced cell and where to place its origin
addref!(ruler_cell, sref(mark_cell, Point(zero(mark_pitch), height(mark))))
# Or slightly more concise: addref!(ruler_cell, mark_cell, Point(...))
ruler_cellAlternatively, we could render! another mark rectangle, using Align.above to position it appropriately:
render!(ruler_cell, Align.above(mark, ruler_cell; centered=true), GDSMeta(0, 0))The Align methods are above, below, leftof, rightof, flushtop, flushbottom, flushright, and flushleft. These position a copy of a geometry entity relative to another so that their bounding boxes are aligned as specified, using keyword options for offset from the aligned position and centered positioning in the other dimension.
Step 3: Creating a Vernier scale
The Vernier scale has two rulers in different layers. The distance from one mark to the next is different between rulers, so you can measure the offset of one layer from the other by finding the pair of marks that align. For example, if the difference in pitch is 100nm, and the third pair of marks from the center are aligned, then the layers are offset by 300nm.
Since we want to draw a second ruler as above with a different pitch and layer, we'll create a function to draw a ruler with given parameters:
function ruler!(ruler_cell, mark_rect, mark_pitch, ruler_range, metadata)
mark_cell = Cell(uniquename("mark")) # Avoid creating Cells with duplicate names
render!(mark_cell, mark_rect, metadata)
addref!(ruler_cell, aref(mark_cell, ruler_range * mark_pitch, (0:0)μm))
# Extend center mark (assuming range is symmetric and includes zero!)
render!(ruler_cell, Align.above(mark_rect, ruler_cell; centered=true), metadata)
return ruler_cell
end
ruler_1 = Cell("ruler_1")
ruler_2 = Cell("ruler_2")
ruler!(ruler_1, mark, mark_pitch, ruler_range, GDSMeta()) # default GDSMeta is 0/0
ruler!(ruler_2, mark, mark_pitch - 100nm, ruler_range, GDSMeta(1)) # 1/0Then to create our Vernier scale, we can place references to both of these in another Cell:
vernier = Cell("vernier")
addref!(vernier, ruler_1)
addref!(vernier, ruler_2; xrefl=true) # Reflect ruler 2 across x axis
vernierStep 4: Handle negative layers
What if our process uses negative patterns, where material is removed where you have polygons? With the pattern above, one layer could cover up the other. We'll add an option where the ruler marks are holes in a larger area of a negative layer (based on the bounds of the original ruler), using difference2d to subtract polygons from each other:
function ruler!(ruler_cell, mark_rect, mark_pitch, ruler_range, metadata; negative=false)
mark_cell = Cell(uniquename("mark")) # Avoid creating Cells with duplicate names
if !negative
render!(mark_cell, mark_rect, metadata)
addref!(ruler_cell, aref(mark_cell, ruler_range * mark_pitch, (0:0)μm))
# Extend center mark (assuming range is symmetric and includes zero!)
render!(ruler_cell, Align.above(mark_rect, ruler_cell; centered=true), metadata)
else
# Create temporary cell with geometry to subtract
temp_cell = Cell("temp")
ruler!(temp_cell, mark_rect, mark_pitch, ruler_range, metadata) # positive
# Create cutout
bnds = bounds(temp_cell)
# `offset`: push out all edges by `2*mark_pitch`
cutout = offset(centered(Rectangle(width(bnds), 2*height(bnds))), 2*mark_pitch)
# Create negative pattern
neg_ruler = difference2d(cutout, temp_cell => metadata)
render!(ruler_cell, neg_ruler, metadata)
end
return ruler_cell
end
ruler_1 = Cell("ruler_1")
ruler_2 = Cell("ruler_2")
ruler!(ruler_1, mark, mark_pitch, ruler_range, GDSMeta())
ruler!(ruler_2, mark, mark_pitch - 100nm, ruler_range, GDSMeta(1), negative=true)
vernier = Cell("vernier")
addref!(vernier, ruler_1)
addref!(vernier, ruler_2; xrefl=true) # Reflect ruler 2 across x axis
vernierBesides difference2d, we also have union2d, intersect2d, and xor2d. These only work on polygons. For convenience, they can take any kind of geometry or vector of geometries as arguments, but these will be converted to polygons first. They can also take pairs like temp_cell => metadata above, which selects all polygons in temp_cell in the layer metadata.
Step 5: Using CoordinateSystems
So far, we've done everything with Cells. These correspond to cells in the GDSII format, and can only store Polygons with integer layer/datatype (GDSMeta) and references to other Cells.
It's often better to work with "native" DeviceLayout geometry and metadata, and only render to a Cell when you're ready to save to GDS. This preserves information about the type of geometry entities, like Rectangle or Ellipse rather than Polygon, which can make certain computations easier. Backends other than GDS may also be able to take advantage of that information; for example, the SolidModel backend has exact representations of curves and ellipses, and can use geometry information to control mesh sizing.
Moreover, we can use named layers (SemanticMeta rather than GDSMeta). This helps make geometry "portable" across process technologies. For example, you may have a :metal layer, but you might have a process flow for aluminum where the tool expects layer 1 and an alternative process for niobium where the tool expects layer 2. This mapping can be deferred until you render to a Cell rather than hardcoded into the geometry.
The native equivalent of Cell is CoordinateSystem, which can store any DeviceLayout.GeometryEntity with any kind of metadata, as well as references to any DeviceLayout.GeometryStructure (including Cell, CoordinateSystem, Path, and Component). When we render! to a CoordinateSystem, we simply place the entity as it is, rather than transform it into a backend-specific "primitive" entity. (In fact, place! is a synonym for render! that only works with CoordinateSystems.)
Our ruler! function would already work if we passed a CoordinateSystem as ruler_cell, but the internal Cell containing a single mark would still be holding a polygon. So we just change Cell to CoordinateSystem:
function ruler!(ruler_cs, mark_rect, mark_pitch, ruler_range, metadata; negative=false)
mark_cs = CoordinateSystem(uniquename("mark")) # Avoid duplicate names
if !negative
render!(mark_cs, mark_rect, metadata)
addref!(ruler_cs, aref(mark_cs, ruler_range * mark_pitch, (0:0)μm))
# Extend center mark (assuming range is symmetric and includes zero!)
render!(ruler_cs, Align.above(mark_rect, ruler_cs; centered=true), metadata)
else
# Create temporary cell with geometry to subtract
temp_cs = CoordinateSystem("temp")
ruler!(temp_cs, mark_rect, mark_pitch, ruler_range, metadata) # positive
# Create cutout
bnds = bounds(temp_cs)
# `offset`: push out all edges by `2*mark_pitch`
cutout = offset(centered(Rectangle(width(bnds), 2*height(bnds))), 2*mark_pitch)
# Create negative pattern
neg_ruler = difference2d(cutout, temp_cs => metadata)
render!(ruler_cs, neg_ruler, metadata)
end
return ruler_cs
endruler! (generic function with 1 method)Now we can use this method with SemanticMeta:
METAL_1 = SemanticMeta(:metal_1) # layer names are Julia `Symbol`s
METAL_2 = SemanticMeta(:metal_2)
ruler_1 = CoordinateSystem("ruler_1")
ruler_2 = CoordinateSystem("ruler_2")
ruler!(ruler_1, mark, mark_pitch, ruler_range, METAL_1)
ruler!(ruler_2, mark, mark_pitch - 100nm, ruler_range, METAL_2, negative=true)
vernier = CoordinateSystem("vernier")
addref!(vernier, ruler_1)
addref!(vernier, ruler_2; xrefl=true) # Reflect ruler 2 across x axis
vernierWe can flatten the result, creating a copy with the same geometry without any references, where all geometry entities are elements of the top level coordinate system:
elements(flatten(vernier))13-element Vector{GeometryEntity{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}}:
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((-2500.0 nm,15000.0 nm), (2500.0 nm,30000.0 nm))
ClippedPolygon{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}(Top-level PolyNode with 1 immediate children.)
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((-52500.0 nm,0.0 nm), (-47500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((-42500.0 nm,0.0 nm), (-37500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((-32500.0 nm,0.0 nm), (-27500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((-22500.0 nm,0.0 nm), (-17500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((-12500.0 nm,0.0 nm), (-7500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((-2500.0 nm,0.0 nm), (2500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((7500.0 nm,0.0 nm), (12500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((17500.0 nm,0.0 nm), (22500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((27500.0 nm,0.0 nm), (32500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((37500.0 nm,0.0 nm), (42500.0 nm,15000.0 nm))
Rectangle{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}((47500.0 nm,0.0 nm), (52500.0 nm,15000.0 nm))We find that they're all Rectangles, plus one ClippedPolygon created by difference2d, rather than the Polygons we would get from working directly with Cells.
Before we move on, let's add a second, rotated copy to measure alignment in two dimensions, using addref! with an additional argument for the position of the referenced cell's origin and a rot keyword argument for rotation:
vernier_xy = CoordinateSystem("vernier_xy")
addref!(vernier_xy, vernier)
addref!(vernier_xy, vernier, Point(width(bounds(vernier)), width(bounds(vernier))); rot=90°)
vernier_xyYou can enter ° by typing \degree and pressing Tab.
To export to GDS, we need to provide a way to map named layers to GDSMeta. (If this is not provided, layers will be mapped arbitrarily with a warning, but this is meant as a convenience for quick visualization and should not be relied on.)
layer_record = Dict(:metal_1 => GDSMeta(), :metal_2 => GDSMeta(1))
map_layer(m) = layer_record[layer(m)]
vernier_cell = Cell(vernier_xy; map_meta=map_layer)
save("vernier.gds", vernier_cell);Summary
In this tutorial, you learned about:
- Entities: Basic geometric shapes like
RectangleandPolygon - GDSMeta: Layer information for GDSII output
- Cells: Containers that hold polygons with GDSMeta and references to other cells
- render!: The function that converts entities into cell elements
- References: Arrayed, rotated, mirrored, and translated references to cells
- Alignment: Translating entities to align and center bounding boxes
- Boolean operations:
union2d,difference2d,intersect2d,xor2d - CoordinateSystems: Containers for DeviceLayout-native geometry representation
- SemanticMeta: Named layers that can be mapped to GDSMeta when rendering to a cell
- GDS export: From CoordinateSystems to
.gdsfile via rendering to a cell
Next Steps
Continue to Working with Paths to learn about creating coplanar waveguides and more complex path-based structures.