Schematics
Schematic graphs
The schematic graph is a collection of logical information about the presence of components and their connections. Its nodes are ComponentNode
s, representing instances of AbstractComponent
s in the design. The edges are labeled with the names of the components' Hook
s that are to be fused together.
DeviceLayout.SchematicDrivenLayout.SchematicGraph
— TypeSchematicGraph <: AbstractMetaGraph{Int}
Graph describing the components and connectivity of a device schematic.
DeviceLayout.SchematicDrivenLayout.ComponentNode
— Typemutable struct ComponentNode
id::String
component::AbstractComponent
end
Represents an instance of a component in the context of a schematic graph.
Adding and connecting Components
DeviceLayout.SchematicDrivenLayout.add_node!
— Functionadd_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.
DeviceLayout.SchematicDrivenLayout.fuse!
— Functionfuse!(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.
DeviceLayout.Paths.route!
— Methodroute!(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.
DeviceLayout.SchematicDrivenLayout.RouteComponent
— Typestruct 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.
DeviceLayout.Paths.attach!
— Methodattach!(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.
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 Route
s 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.
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 ComponentNode
s at its start and end points. Then, during plan
, after the initial floorplanning phase has determined position of all fixed Component
s, the route node finds a path between its start and end points.
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.Schematic
— Typestruct Schematic{S} <: AbstractCoordinateSystem{S}
Spatial representation of a layout in terms of AbstractComponent
s rather than polygons.
Can be constructed from a SchematicGraph
g
using plan(g)
.
DeviceLayout.SchematicDrivenLayout.plan
— Functionplan(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 CoordinateSystem
s 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.
DeviceLayout.SchematicDrivenLayout.check!
— Functioncheck!(sch::Schematic; rules=[rotations_valid])
Verifies that sch
satisfies rule(sch) == true
for each rule
in rules
, and sets sch.checked = true
if it does.
By default, checks that all checkable components have an allowed global orientation (rotations_valid(::Schematic)
).
DeviceLayout.SchematicDrivenLayout.build!
— Functionbuild!(sch::Schematic, geometry_fn=geometry; strict=:error)
build!(sch::Schematic, t::Target; strict=:error)
Replace the AbstractComponent
s 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.
DeviceLayout.render!
— Methodrender!(
cs::AbstractCoordinateSystem,
sch::Schematic,
target::LayoutTarget;
strict=:error,
kwargs...
)
Run build!(sch, target)
and render the resulting geometry to cs
using target
's rendering options.
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.
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 build
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.SchematicDrivenLayout.indexof
— Methodindexof(n::ComponentNode, g::SchematicGraph)
Finds the index of the node n
in g
.
DeviceLayout.bounds
— Methodbounds(sch::Schematic, node::ComponentNode)
bounds(sch::Schematic, node_idx::Int)
The Rectangle
bounding the component in node
in the global coordinate system of sch
.
DeviceLayout.center
— Methodcenter(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
.
DeviceLayout.hooks
— Methodhooks(sch::Schematic, node::ComponentNode)
The hooks belonging to the component in node
in the global coordinate system of sch
.
DeviceLayout.Transformations.origin
— Methodorigin(sch::Schematic, node::ComponentNode)
origin(sch::Schematic, node_idx::Int)
The origin of node
in the global coordinate system of sch
.
DeviceLayout.transformation
— Methodtransformation(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)
.
DeviceLayout.SchematicDrivenLayout.find_components
— Functionfind_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 Tuple
s 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)
.
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
.
DeviceLayout.SchematicDrivenLayout.find_nodes
— Functionfind_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 Tuple
s 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))
.
If you have a ComponentNode
, you can replace its component using an arbitrary function that takes the original component as an argument:
DeviceLayout.SchematicDrivenLayout.replace_component!
— Functionreplace_component!(sch::Schematic, node_index::Int, replacement::Function)
Replaces the component c
at node_index
in sch
with replacement(c)
.
replacement
should return a new AbstractComponent with a unique name.
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!
— Functionposition_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.
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)
.
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 build!
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°]
### finalize
build!(floorplan)
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 Path
s and RouteComponent
s, including those nested within composite components. This is based on Path
intersection functionality.
DeviceLayout.SchematicDrivenLayout.crossovers!
— Functioncrossovers!(sch::Schematic, xsty::DeviceLayout.Paths.Intersect.IntersectStyle)
Splice crossovers into intersecting Path
s and RouteComponent
s.
RouteComponents
will cross over Path
s, 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)
crossovers!(sch::Schematic, xsty::DeviceLayout.Paths.Intersect.IntersectStyle,
nodes_1, nodes_2)
Splice crossovers into intersecting Path
s and RouteComponent
s.
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.
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)
. In the future, this method could run any number of checks, but at present it only checks the global orientation of checkable components via the following methods:
DeviceLayout.SchematicDrivenLayout.rotations_valid
— Functionrotations_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
.
To be able to build!
a floorplan (i.e. turn components into their CoordinateSystem
geometries), users must run check!
first, otherwise build
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.