Schematics

Schematic graphs

The schematic graph is a collection of logical information about the presence of components and their connections. Its nodes are ComponentNodes, representing instances of AbstractComponents in the design. The edges are labeled with the names of the components' Hooks that are to be fused together.

Adding and connecting Components

DeviceLayout.SchematicDrivenLayout.add_node!Function
add_node!(g::SchematicGraph, comp::AbstractComponent; base_id=name(comp), kwargs...)

Create and return a new node for comp.

The base_id will be used as the node's id if it is not already in use by the Schematic. If it is, then a uniquename will be generated by appending _n, where n is the number of occurrences of base_id so far (including this one).

Additional keyword arguments will become vertex properties.

source
DeviceLayout.SchematicDrivenLayout.fuse!Function
fuse!(g::SchematicGraph,
    nodehook1::Pair{Int, Symbol},
    nodehook2::Pair{Int, Symbol}; kwargs...)
fuse!(g::SchematicGraph,
    nodehook1::Pair{ComponentNode, <:Union{Symbol, Hook}},
    nodehook2::Pair{<:Union{ComponentNode, AbstractComponent}, <:Union{Symbol, Hook}}; kwargs...)

Usage:

  • fuse!(g, node1=>:hook1, node2=>:hook2)
  • fuse!(g, idx1=>:hook1, idx2=>:hook2)
  • fuse!(g, node1=>:hook1, comp2=>:hook2)
  • fuse!(g, node1=>:hook1, comp2=>:hook2; PLAN_SKIPS_EDGE=>true)

Add an edge (node1, node2) connecting their hooks (:hook1, :hook2) to g.

Returns the second ComponentNode.

If fuse! is passed a bare component comp2, it makes a new node, even if that component is referenced in another node.

If no hook or only one hook is specified, fuse! will try to determine the correct hook using matching_hooks or matching_hook.

You can make an attachment anywhere, not just to an existing named hook, by providing a Hook object with the desired point and orientation rather than a Symbol (example: fuse!(g, node1=>PointHook(1mm, 1mm, 90°), comp2=>:hook2)). This option is provided for convenience in situations that call for ad-hoc or case-by-case relative positioning; one example use case might be placing labels near components. On the other hand, if you find yourself needing a consistent hook that doesn't already exist for MyComponent, then it's generally better to update the component definition so that the hook is available through hooks(::MyComponent). Alternatively, if you want to use an existing hook with an additional offset, consider using the Spacer component.

Cycles: Sometimes we need to avoid adding edges in the graph to avoid cycles that'd force the plan function to throw an error. Solution: Pass a keyword argument plan_skips_edge=true. This allows us to encode all the edges in the graph, while informing the plan function that the edge should be skipped for rendering purposes.

source
DeviceLayout.Paths.route!Method
route!(g::SchematicGraph, rule::RouteRule,
    nodehook1::Pair{ComponentNode,Symbol}, nodehook2::Pair{ComponentNode,Symbol},
    sty, meta;
    waypoints=[], waydirs=[], global_waypoints=false,
    name=uniquename("r_$(component(nodehook1.first).name)_$(component(nodehook2.first).name)"),
    kwargs...)
route!(g::SchematicGraph, rule::RouteRule, node1::ComponentNode, nodehook2::Pair{ComponentNode,Symbol}, sty, meta; kwargs...)
route!(g::SchematicGraph, rule::RouteRule, nodehook1::Pair{ComponentNode,Symbol}, node2::ComponentNode, sty, meta; kwargs...)
route!(g::SchematicGraph, rule::RouteRule, node1::ComponentNode, node2::ComponentNode, sty, meta; kwargs...)

Creates a RouteComponent with given style sty and metadata meta, and fuses it between the specified nodes and hooks in g.

Returns the resulting ComponentNode in g.

Example usage: route!(g, BSplineRouting(), zline_node=>:feedline, z_launcher_node=>:line, Paths.CPW(10μm, 6μm), GDSMeta(1, 2))

If one or both hook symbols are not specified, then matching_hook or matching_hooks will be used to attempt to automatically find the correct hook or hooks.

The route will have start and endpoints at the origin until a method like plan! is called. waypoints and waydirs are in component-local coordinates (unless global_waypoints is true), and rule determines how they will be used.

Additional keyword arguments will become vertex properties for the RouteComponent's node.

name should be unique.

source
DeviceLayout.SchematicDrivenLayout.RouteComponentType
struct RouteComponent{T} <: AbstractComponent{T}
    name::String
    r::Paths.Route{T}
    global_waypoints::Bool
    sty::Paths.Style
    meta::Meta

Wraps a Route in a Component type for use with schematics.

name should be unique. If global_waypoints is false, then the waypoints and waydirs are taken to be relative to the component coordinate system. Otherwise, they will be relative to the schematic global coordinate system.

source
DeviceLayout.Paths.attach!Method
attach!(g::SchematicGraph,
    pathnode::S,
    nodehook2::Pair{T, Symbol},
    position;
    i=lastindex(component(pathnode)),
    location=0) where {S <: ComponentNode, T <: ComponentNode}
Usage: attach!(g, pathnode, node2=>:hook2, position; i=segment_idx, location=0)

Adds an edge to g between pathnode and node2, attaching :hook2 to a specified point along pathnode.

A new HandedPointHook is generated a distance position along the segment specified by i. If location is +1 or -1, the generated hook is offset to the right or left of the segment by the extent defined by the path's style, with the hook's inward direction pointing back to the segment.

source

Routing

A Paths.Route is like a Path, but defined implicitly in terms of its endpoints and rules (like "use only straight sections and 90 degree turns") for getting from one end to another. We can add Routes between components in a schematic using route!, creating flexible connections that are only resolved after floorplanning has determined the positions of the components to be connected.

Terminology note: connected component

There is a graph-theory term "connected component" unrelated to our AbstractComponent, indicating a subgraph whose nodes are all connected by edges and which isn't part of a larger such subgraph. For example, a fully connected graph has one connected component, the entire graph. Sometimes you may even hear these called "components", but below we use "connected component" for the graph-theory sense and "component" for AbstractComponent.

In more detail: Typically, each fuse! operation fully determines the position and rotation of the new node. For each connected component, the first node added to the SchematicGraph is positioned at the origin. A typical workflow could start by creating a node with a "chip template" component containing port hooks and other boilerplate. We then fuse! any CPW launchers to the template. The devices in the middle of the chip are added and fused to one another without yet fixing their position relative to the chip template. Next, one connection is made between this connected component of devices and the connected component containing the template, fixing their relative positions. Since the chip template was added first, it will be centered at the origin.

At this point, further connections still need to be made between various device ports and CPW launchers positioned on the chip template, all of which are now fully constrained. Another constraint like those created by fuse! so far would overconstrain the layout, causing floorplanning with plan to fail. The remaining connections are instead made with route!, which creates a RouteNode with edges to the ComponentNodes at its start and end points. Then, during plan, after the initial floorplanning phase has determined position of all fixed Components, the route node finds a path between its start and end points.

Schematic connections without geometric constraints

You can add edges to the schematic graph that will be ignored during plan using the keyword plan_skips_edge=true in fuse!.

Schematics

DeviceLayout.SchematicDrivenLayout.SchematicType
struct Schematic{S} <: AbstractCoordinateSystem{S}

Spatial representation of a layout in terms of AbstractComponents rather than polygons.

Can be constructed from a SchematicGraph g using plan(g).

source
DeviceLayout.SchematicDrivenLayout.planFunction
plan(g::SchematicGraph, hooks_fn=hooks; strict=:error, log_dir="build", log_level=Logging.Info)
plan(g::SchematicGraph, t::Target; strict=:error, log_dir="build", log_level=Logging.Info)

Constructs a Schematic floorplan from g without rendering Components.

Iterates through the nodes in g depth-first to build a tree (or more than one, if the graph is disconnected) of CoordinateSystems satisfying the translations, reflections, and rotations required by the hooks corresponding to each edge. Each CoordinateSystem holds a StructureReference to the Component in the corresponding node. A new Schematic is created containing references to the roots of these trees (sometimes called "rendering trees") as well as a dictionary mapping each ComponentNode in g to the reference to that node's CoordinateSystem in its rendering tree. (This is the reference stored by its parent.)

plan will ignore edges with the property SchematicDrivenLayout.PLAN_SKIPS_EDGE=>true.

The strict keyword should be :error, :warn, or :no.

The strict=:error keyword option causes plan to throw an error if any errors were logged during planning. This is enabled by default, but can be disabled with strict=:no, in which case any node that was not successfully placed relative to an existing node will simply appear at the origin. Using strict=:no is recommended only for debugging purposes.

The strict=:warn keyword option causes plan to throw an error if any warnings were logged during planning. This is disabled by default. Using strict=:warn is suggested for use in automated pipelines, where warnings may require human review.

Log messages with level of at least log_level will be written to joinpath(log_dir, name(g) * ".log"). If logdir is nothing, then no file will be written. The same log file will be used for build! and render! stages of the schematic workflow.

source
DeviceLayout.SchematicDrivenLayout.build!Function
build!(sch::Schematic, geometry_fn=geometry; strict=:error)
build!(sch::Schematic, t::Target; strict=:error)

Replace the AbstractComponents in sch with their geometry. Usually there's no reason to do this, since render! will still render the geometry but won't modify sch.

Users must run check!(sch) before calling this method; otherwise, it will throw an error.

The strict keyword should be :error, :warn, or :no.

The strict=:error keyword option causes build! to throw an error if any errors were logged while building component geometries. This is enabled by default, but can be disabled with strict=:no, in which case any component which was not successfully built will have an empty geometry. Using strict=:no is recommended only for debugging purposes.

The strict=:warn keyword option causes build! to throw an error if any warnings were logged. This is disabled by default. Using strict=:warn is suggested for use in automated pipelines, where warnings may require human review.

source
DeviceLayout.render!Method
render!(
    cs::AbstractCoordinateSystem,
    sch::Schematic,
    target::LayoutTarget;
    strict=:error,
    kwargs...
)

Render the schematic sch to cs using target's rendering options, without modifying sch.

Users must run check!(sch) before calling this method; otherwise, it will throw an error.

The strict keyword should be :error, :warn, or :no.

The strict=:error keyword option causes render! to throw an error if any errors were logged while building component geometries or while rendering geometries to cs. This is enabled by default, but can be disabled with strict=:no, in which case any component which was not successfully built will have an empty geometry, and any non-fatal rendering errors will be ignored as usual. Using strict=:no is recommended only for debugging purposes.

The strict=:warn keyword option causes render! to throw an error if any warnings were logged. This is disabled by default. Using strict=:warn is suggested for use in automated pipelines, where warnings may require human review.

source

Inspecting and manipulating schematics

Schematics allow you to produce layouts by specifying designs at a high level, in terms of components and their connections. Often, this isn't quite enough to get the final artwork you want. Maybe a wire needs to be routed differently, or a component needs to be modified based on its position.

SchematicDrivenLayout allows you to inspect and modify a Schematic to make these sorts of changes before render! renders it into polygons.

When you construct a SchematicGraph, you don't need to know exactly where a component will end up. You can usually calculate it yourself, but there are some built-in utilities to simplify things.

DeviceLayout.boundsMethod
bounds(sch::Schematic, node::ComponentNode)
bounds(sch::Schematic, node_idx::Int)

The Rectangle bounding the component in node in the global coordinate system of sch.

source
DeviceLayout.centerMethod
center(sch::Schematic, node::ComponentNode)
center(sch::Schematic, node_idx::Int)

The center of the bounds of node's component in the global coordinate system of sch.

source
DeviceLayout.hooksMethod
hooks(sch::Schematic, node::ComponentNode)

The hooks belonging to the component in node in the global coordinate system of sch.

source
DeviceLayout.transformationMethod
transformation(sch::Schematic, node::ComponentNode)

Given a Schematic sch containing ComponentNode node in its SchematicGraph, this function returns a CoordinateTransformations.Transformation object that lets you translate from the coordinate system of the node to the global coordinate system of sch.

Effectively a wrapper around DeviceLayout.transformation(::CoordinateSystem, ::CoordSysRef).

source
DeviceLayout.SchematicDrivenLayout.find_componentsFunction
find_components(f::Function, g::SchematicGraph; depth=-1)

Return the indices of the nodes in sch or g for which f(component(node)) is true.

Keyword depth is the number of layers of the graph to search within, where the subgraph of each CompositeComponent is a layer beyond the layer holding the ComponentNode for that CompositeComponent. To search only top-level components, use depth=1. To search all subgraphs recursively, use a negative depth (the default).

Indices for nodes in subgraphs are integers or Tuples of integer indices, one for each additional layer. For example, if a node containing a CompositeComponent has index 2 in the top layer, then the third subcomponent in the CompositeComponent's graph has index (2,3).

source
find_components(comptype::Type{T}, sch::Schematic; depth=-1) where {T <: AbstractComponent}
find_components(comptype::Type{T}, g::SchematicGraph; depth=-1) where {T <: AbstractComponent}

Return the indices of nodes containing components of type T.

See find_components(::Function, ::SchematicGraph).

source
DeviceLayout.SchematicDrivenLayout.find_nodesFunction
find_nodes(f::Function, g::SchematicGraph; depth=-1)

Return the indices of the nodes in sch or g for which f(node) is true.

Keyword depth is the number of layers of the graph to search within, where the subgraph of each CompositeComponent is a layer beyond the layer holding the ComponentNode for that CompositeComponent. To search only top-level components, use depth=1. To search all subgraphs recursively, use a negative depth (the default).

Indices for nodes in subgraphs are integers or Tuples of integer indices, one for each additional layer. For example, if a node containing a CompositeComponent has index 2 in the top layer, then the third subcomponent in the CompositeComponent's graph has index (2,3).

See also find_components, which checks f(component(node)).

source

If you have a ComponentNode, you can replace its component using an arbitrary function that takes the original component as an argument:

You can also replace it (or all components of that type) using a function that takes the position in schematic-global or wafer-global coordinates as an argument:

DeviceLayout.SchematicDrivenLayout.position_dependent_replace!Function
position_dependent_replace!(sch::Schematic{S}, node_index::Int, replacement::Function;
    schematic_origin_globalcoords=zero(Point{S})) where {S}

Replaces the component c at node_index in sch with replacement.

The replacement function should have a signature matching replacement(c<:AbstractComponent, c_origin_globalcoords::Point) where c is the component to be replaced and c_origin_globalcoords is the component's origin in global coordinates. Here "global" coordinates are defined as those in which the schematic's origin is the keyword argument schematic_origin_globalcoords in position_dependent_replace!.

replacement should return a new AbstractComponent with a unique name.

source
position_dependent_replace!(sch::Schematic{S}, comptype::Type{<:AbstractComponent},
    replacement::Function;
    schematic_origin_globalcoords=zero(Point{S})) where {S}

Replaces each component instance of type comptype in sch using position_dependent_replace! with the replacement function replacement(c<:AbstractComponent, c_origin_globalcoords::Point).

source

One other useful trick allows a kind of interactive routing. When you view your final layout built from the schematic, you may find that a route bends too sharply or goes too close to a component. You can write down the points it needs to go to in the schematic's global coordinate system, and add them as waypoints to the route. That is, if you go back to your layout script, before you render! the layout, you can do something like

### original script
g = SchematicGraph("example")
...
route_node = route!(g, args...)
...
floorplan = plan(g)
check!(floorplan)
### modifications
# waypoints are global coordinates, not relative to the route's origin
route_node.component.global_waypoints = true
route_node.component.r.waypoints = [Point(600.0μm, -3000.0μm)]
route_node.component.r.waydirs = [90°]
### render to `cell` with options from `target`
render!(cell, floorplan, target)

Now the route in route_node is guaranteed to pass through the point (600.0μm, -3000.0μm) on its way to its destination. If the RouteRule's implementation uses waydirs, then it will also have a direction of 90° at that point.

Automatic crossover generation

You can automatically generate crossovers between Paths and RouteComponents, including those nested within composite components. This is based on Path intersection functionality.

DeviceLayout.SchematicDrivenLayout.crossovers!Function
crossovers!(sch::Schematic, xsty::DeviceLayout.Paths.Intersect.IntersectStyle)

Splice crossovers into intersecting Paths and RouteComponents.

RouteComponents will cross over Paths, and otherwise components added to the schematic graph later will cross over those added earlier.

Example

    g = SchematicGraph("crossover_example")
    # ...
    floorplan = plan(g)
    xsty = Intersect.AirBridge(
        crossing_gap=5μm,
        foot_gap=2μm,
        foot_length=4μm,
        extent_gap=2μm,
        scaffold_gap=5μm,
        scaffold_meta=GDSMeta(5),
        air_bridge_meta=GDSMeta(3)
        )
    SchematicDrivenLayout.crossovers!(floorplan, xsty)
source
crossovers!(sch::Schematic, xsty::DeviceLayout.Paths.Intersect.IntersectStyle,
    nodes_1, nodes_2)

Splice crossovers into intersecting Paths and RouteComponents.

Components in nodes_2 will cross over any components in nodes_1, and otherwise components added to the schematic graph later will cross over those added earlier.

source

Checking schematics

It is often necessary to check that a planned Schematic obeys a set of constraints set by the fabrication process. For instance, one may want to verify that all the junctions in a floorplan are oriented in the right direction, e.g. pointing north. Instead of doing this by eye, users should call check!(sch::Schematic). This method can run any number of checks provided by the user, but by default it only checks the global orientation of checkable components via the following methods:

DeviceLayout.SchematicDrivenLayout.rotations_validFunction
rotations_valid(sch::Schematic)

Verifies that all checkable components in sch have the right orientation in the global coordinate system (returns true if successful and throws an error otherwise). A component of type T is checkable if check_rotation(::T) = true.

source

To be able to build! or render! a floorplan (i.e. turn components into their geometries), users must run check! first. Otherwise, these functions will throw an error.

Visualization

SchematicDrivenLayout also contains some prototype schematic-visualization methods: if GraphMakie is present in the environment, then GraphMakie.graphplot can be used with a SchematicGraph to show nodes and edges or with Schematic to also place the components at their on-chip positions.