Tutorial: Building a Component

Components are reusable, parameterized building blocks that encapsulate geometry and connection points. In this tutorial, you'll create your first component, an interdigital capacitor.

What You'll Learn

  • Understanding the component abstraction
  • Defining component parameters with @compdef
  • Implementing geometry with _geometry!
  • Defining hooks for connections
  • Using your component in layouts

Prerequisites

Setup

using DeviceLayout, .PreferredUnits # As usual
using FileIO                        # As usual
using DeviceLayout.SchematicDrivenLayout # For Components

Why Components?

So far, we've created geometry directly. This works for simple layouts, but becomes unwieldy as designs grow. Components solve this by:

  1. Encapsulating geometry: Define once, use many times
  2. Parameterizing designs: Change dimensions without rewriting code
  3. Defining connections: Specify where and how components connect
  4. Enabling automation: Let the schematic system handle placement

Step 1: Define the Component Structure

Let's create a simple capacitor component. First, define the struct:

"""
    MyCapacitor <: Component

A simple interdigitated capacitor with two terminals (positive pattern).

# Parameters
- `name`: Component name
- `finger_length`: Length of each finger
- `finger_width`: Width of each finger
- `finger_gap`: Gap between fingers
- `finger_count`: Number of finger pairs
- `rounding`: Rounding radius for metal corners

# Hooks
- `p0`: Left terminal
- `p1`: Right terminal
"""
@compdef struct MyCapacitor <: Component
    name = "capacitor"
    finger_length = 100μm
    finger_width = 5μm
    finger_gap = 3μm
    finger_count::Int = 4
    rounding = 1μm
end

The @compdef macro creates:

  • A struct with the specified fields
  • Default values for each field
  • A keyword constructor: MyCapacitor(finger_length=200μm, ...)
  • Required methods for component operation

We can now create a component:

cap1 = MyCapacitor(
    finger_length = 150μm,
    finger_count = 6,
    name = "cap1"
    )
Main.MyCapacitor "cap1" with non-default parameters (name = "cap1", finger_length = 150 μm, finger_count = 6)

But we still need to define two methods ourselves: _geometry! and hooks.

Step 2: Implement the Geometry

Define how the component's geometry is generated by creating a new method for SchematicDrivenLayout._geometry!. When we extend an existing function with a new method for our custom type, we need to qualify the function name in the definition with the module it comes from. If we just define function _geometry!(...), then DeviceLayout won't know to call that method internally, so we define function SchematicDrivenLayout._geometry!:

function SchematicDrivenLayout._geometry!(cs::CoordinateSystem, cap::MyCapacitor)
    (; finger_length, finger_width, finger_gap, finger_count, rounding) = cap

    # Calculate dimensions
    pitch = finger_width + finger_gap
    total_height = finger_count * pitch

    # Create the two bus bars
    left_bus = Rectangle(finger_width, total_height)
    right_bus = Translation(finger_length + finger_gap + finger_width, 0nm)(
        Rectangle(finger_width, total_height)
    )

    finger_rect = Align.rightof(Rectangle(finger_length, finger_width), left_bus)
    left_fingers = [
        Align.flushbottom(
            finger_rect, left_bus, offset = (i - 1) * pitch
        ) for i in 1:2:finger_count
        ]
    finger_rect = Align.leftof(Rectangle(finger_length, finger_width), right_bus)
    right_fingers = [
        Align.flushbottom(
            finger_rect, left_bus, offset = (i - 1) * pitch
        ) for i in 2:2:finger_count
        ]

    # Take union before rounding
    all_metal = union2d([left_fingers; right_fingers], [left_bus, right_bus])
    # Apply rounding
    rounded_metal = Rounded(all_metal, rounding) # Creates a StyledEntity
    # Place in the geometry
    place!(cs, rounded_metal, :metal)

    return cs
end

Notice that we take the union before applying rounding. This is necessary to correctly round corners where the original polygons meet. Moreover, Boolean operations like union2d use polygon clipping, so entities are converted to polygons before applying the operation. If we had rounded first, then taking the union would discretize those rounded corners into piecewise linear segments. Not only does that make the union operation more expensive, but it loses information that can be used with backends other than GDS.

Step 3: Inspect the Geometry

Now we can draw the component's geometry:

cs1 = geometry(cap1)
Example block output

geometry(cap1) returns a CoordinateSystem that's been passed through the _geometry! method we defined. Calling geometry again does not re-generate the geometry, and instead returns the same CoordinateSystem:

cs2 = geometry(cap1)
cs2 === cs1 # object identity is the same
true

Step 4: Define the Hooks

Hooks specify where the component connects to other components, along with an inward-pointing direction:

function SchematicDrivenLayout.hooks(cap::MyCapacitor)
    (; finger_length, finger_width, finger_gap, finger_count) = cap

    # Calculate dimensions
    pitch = finger_width + finger_gap
    total_height = finger_count * pitch
    center_y = total_height / 2
    total_width = finger_length + finger_gap + 2*finger_width

    # Left hook: pointing right (+x direction, 0°)
    p0 = PointHook(Point(0nm, center_y), 0°)

    # Right hook: pointing right (-x direction, 180°)
    p1 = PointHook(Point(total_width, center_y), 180°)

    # Return NamedTuple with names p0 and p1
    return (; p0, p1) # (equivalent to (; p0 = p0, p1 = p1))
end

Step 5: Inspect the Hooks

Directly inspect the NamedTuple returned by hooks:

# Get hook information
h = hooks(cap1)
println("Hooks: ", keys(h))
println("p0: $(h.p0.p), $(h.p0.in_direction)")
println("p1: $(h.p1.p), $(h.p1.in_direction)")
Hooks: (:p0, :p1)
p0: (0.0 nm,24000.0 nm), 0.0°
p1: (163.0 μm,24.0 μm), 180.0°

To inspect them visually, add the component to a new coordinate system and place some arrows on its hooks:

annotated_cs = CoordinateSystem("annotated_cap")
addref!(annotated_cs, cap1) # Components are structures, you can create references
# Triangle pointing right with tip at the origin
arrow = Polygon(Point(0,0)μm, Point(-10, 10)μm, Point(-10, -10)μm) # counterclockwise!
# Create a hook to easily compute transformations
# At tip of the triangle, "inward direction" pointing the other way
arrow_hook = PointHook(Point(0, 0)μm, 180°)
for hookname in keys(hooks(cap1))
    # Calculate transformation required to place `arrow_hook` on `hookname`
    # ... inward directions pointing opposite each other
    # ... so arrow is pointing along inward direction of `hookname`
    trans = transformation(hooks(cap1, hookname), arrow_hook)
    place!(annotated_cs, trans(arrow), :annotation)
    # Add a label too
    textstyle = PolyTextSansMono(5μm, SemanticMeta(:text))
    addref!(annotated_cs, polytext("$hookname", textstyle), trans)
end
annotated_cs
Example block output

Step 6: Use Component Parameters

Components provide methods to inspect parameters. A component can also be used as a "template", providing defaults for new instances:

# Get all parameters as a NamedTuple
params = parameters(cap1)
println("Parameters: ", params)

# Get only non-default parameters
println("Non-default: ", SchematicDrivenLayout.non_default_parameters(cap1))

# Create a modified copy using the first as a template
# Also valid: `cap1(name=...)`
cap2 = set_parameters(cap1, name="cap2", finger_gap=5μm, finger_count=8)
Main.MyCapacitor "cap2" with non-default parameters (name = "cap2", finger_length = 150 μm, finger_gap = 5 μm, finger_count = 8)

The new capacitor has the same parameters as the original, except where they were overridden by keyword arguments in the last line.

Step 7: The @component Macro

Above, we have variables named cap1, cap2, where we assigned them the same names as parameters. We can set the name automatically from the variable name using the @component macro:

@component cap3 = MyCapacitor begin
    finger_count = 10
end
Main.MyCapacitor "cap3" with non-default parameters (name = "cap3", finger_count = 10)

We can also use this idiom with a template:

@component cap4 = cap1 begin
    finger_count = 10
end
Main.MyCapacitor "cap4" with non-default parameters (name = "cap4", finger_length = 150 μm, finger_count = 10)

Finally, we can create arrays of components:

@component cap_finger_sweep_[1:10] = cap1 begin
    finger_length = 150μm # All with the same finger length
    finger_count .= 2:2:20 # All with different finger count
end
10-element Vector{Main.MyCapacitor}:
 Main.MyCapacitor "cap_finger_sweep_1" with non-default parameters (name = "cap_finger_sweep_1", finger_length = 150 μm, finger_count = 2)
 Main.MyCapacitor "cap_finger_sweep_2" with non-default parameters (name = "cap_finger_sweep_2", finger_length = 150 μm)
 Main.MyCapacitor "cap_finger_sweep_3" with non-default parameters (name = "cap_finger_sweep_3", finger_length = 150 μm, finger_count = 6)
 Main.MyCapacitor "cap_finger_sweep_4" with non-default parameters (name = "cap_finger_sweep_4", finger_length = 150 μm, finger_count = 8)
 Main.MyCapacitor "cap_finger_sweep_5" with non-default parameters (name = "cap_finger_sweep_5", finger_length = 150 μm, finger_count = 10)
 Main.MyCapacitor "cap_finger_sweep_6" with non-default parameters (name = "cap_finger_sweep_6", finger_length = 150 μm, finger_count = 12)
 Main.MyCapacitor "cap_finger_sweep_7" with non-default parameters (name = "cap_finger_sweep_7", finger_length = 150 μm, finger_count = 14)
 Main.MyCapacitor "cap_finger_sweep_8" with non-default parameters (name = "cap_finger_sweep_8", finger_length = 150 μm, finger_count = 16)
 Main.MyCapacitor "cap_finger_sweep_9" with non-default parameters (name = "cap_finger_sweep_9", finger_length = 150 μm, finger_count = 18)
 Main.MyCapacitor "cap_finger_sweep_10" with non-default parameters (name = "cap_finger_sweep_10", finger_length = 150 μm, finger_count = 20)

Summary

In this tutorial, you learned:

  • Component structure: @compdef creates parameterized structs
  • Geometry: _geometry! defines what to draw, geometry() returns the component's coordinate system
  • Hooks: hooks defines connection points
  • Parameters: parameters() and set_parameters() for inspecting and creating modified copies
  • The @component macro: @component to construct and automatically name new components

Next Steps

Components really shine when used in schematic-driven layout. Continue to Schematic Basics to learn how to connect components in a schematic and have the layout automatically generated.

See Also