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.
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
endRepresents 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::MetaWraps 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 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.
In more detail: Typically, each fuse! operation fully determines the position and rotation of the new node. The exception is that the first node added to each connected component of the SchematicGraph is positioned at the origin.
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.
For example, a workflow might 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 and launchers, 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. Using fuse! to connect a Path to both ports would overconstrain the layout, causing floorplanning with plan to fail unless the Path is drawn precisely to agree with the existing constraints. But the designer may want to vary parameters that change the port positions without redefining that Path, or they may simply not want to have to calculate the precise path themselves. So the remaining connections are instead defined with the desired flexibility using route!. This creates a RouteNode with edges to the ComponentNodes at its start and end points. Then, after plan, the initial floorplanning phase has determined the position of all fixed Components, so the route node can find 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!.
Differences between schematic and geometry-level routing
In geometry-level layout, we can extend a Path using route!(path, p1, α1, rule, style; waypoints=[], waydirs=[]). The schematic-level call looks a bit different: route_node = route!(graph, rule, node1=>hook1, node2=>hook2, style, metadata; waypoints=[], waydirs=[], global_waypoints=false, kwargs...). In this case, the start and end points and directions are not known until after plan, and no path is actually calculated until until we build! or render! the schematic, or we call path(route_node.component).
By default, global_waypoints=false, meaning that waypoints and directions are viewed as relative the the route start, with the positive x axis oriented along the route's initial start direction. Often global_waypoints=true is more useful, especially for a simple interactive routing workflow: 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, you can modify the route! call:
route_node = route!(
g,
rule,
node1 => hook1,
node2 => hook2,
sty,
meta; # Original route command
# Add waypoint information to to `route!` call
global_waypoints=true, # Waypoints are relative to global schematic coordsys
# If global_waypoints=false (default), waypoints are relative to the route start
# with the initial route direction as the +x axis
waypoints=[Point(600.0μm, -3000.0μm)],
waydirs=[90°]
)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.
Channel routing at the schematic level also gets some special handling. When using the Paths.SingleChannelRouting rule, the router will look for the rule's Paths.RouteChannel in the schematic to get its global coordinates for routing. Additionally, paths are not assigned tracks in the rule using Paths.set_track! before route!. Instead, a route's track is set using the track keyword in route!, defaulting to a new track at the bottom of the channel so far (track=num_tracks(channel)+1). Because the routes are not drawn until later, the track offsets are still calculated using a number of tracks given by the maximum track number of all routes that are eventually added to the channel with the same rule. (Each route in the channel should still use the same instance of the SingleChannelRouting rule.)
Note that routes through a channel are no different from other routes as far as the schematic graph is concerned. That is, they are still just routes from one component's hook to another component's hook; they just happen to have a RouteRule that references the channel between them. One way to think about it is that the channel acts as a kind of extended waypoint. In particular, routes are not fused to the channel, and the channel component doesn't contain any individual route geometries in its own geometry (which is just empty).
Schematics
DeviceLayout.SchematicDrivenLayout.Schematic — Typestruct 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).
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 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.
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 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.
DeviceLayout.render! — Methodrender!(
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.
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.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 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).
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 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)).
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).
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! — Functioncrossovers!(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)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.
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_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! 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.