diff --git a/docs/src/paths.md b/docs/src/paths.md
index 5d9f0500..530d9c25 100644
--- a/docs/src/paths.md
+++ b/docs/src/paths.md
@@ -23,7 +23,9 @@ capture an initial and final angle, a radius, and an origin. All circular turns
parameterized with these variables.
Another useful `Segment` subtype is [`Paths.BSpline`](@ref), which interpolates between two
-or more points with specified start and end tangents.
+or more points with specified start and end tangents (and curvature, optionally) using a [cubic B-spline](https://en.wikipedia.org/wiki/B-spline#Cubic_B-Splines).
+These have the property that curvature is continuous along the spline, and
+can be automatically optimized further to avoid sharp changes in curvature.
## Styles
diff --git a/docs/src/routes.md b/docs/src/routes.md
index 39cefea7..cfeddbd3 100644
--- a/docs/src/routes.md
+++ b/docs/src/routes.md
@@ -10,7 +10,7 @@ To draw a `Path` from a `Route`, you can use [`Path(r::Route, sty)`](@ref).
More often, you won't work directly with the `Route` type but will instead use [`route!`](@ref) to extend an existing path to an endpoint according to the rules you specify.
-## Route API
+## Reference
```@docs
Paths.Route
@@ -24,6 +24,8 @@ More often, you won't work directly with the `Route` type but will instead use [
Paths.StraightAnd90
Paths.StraightAnd45
Paths.CompoundRouteRule
+ Paths.SingleChannelRouting
+ Paths.RouteChannel
```
### Route drawing
@@ -53,3 +55,131 @@ A `Route` supports endpoint inspection much like a `Path` does:
Paths.p1(::Paths.Route)
Paths.α1(::Paths.Route)
```
+
+## Examples
+
+### Channel routing
+
+`RouteChannels` offer a way to run routes in parallel, with routes joining or leaving a channel at different points. Using [`Paths.SingleChannelRouting`](@ref), we can set the "track" (a curve offset from the channel centerline) for each route to follow through the channel, as well as some rules for joining and leaving the channel from route start and end points. Here's a basic example with a straight channel:
+
+```@example 1
+using DeviceLayout, .PreferredUnits, FileIO
+import DeviceLayout.Graphics: inch
+# Define start and end points for various routes
+p0s = [
+ Point(100.0, 200.0)μm, # Enter and exit from top
+ Point(50.0, 150)μm, # Enter from top, exit from right
+ Point(-100.0, -100.0)μm, # Enter from lower left, exit from right
+ Point(600.0, -150)μm, # Enter from bottom, exit from right
+ Point(100.0, -200.0)μm # Enter and exit from bottom
+]
+
+p1s = [
+ Point(900.0, 200.0)μm,
+ Point(1100.0, 150.0)μm,
+ Point(1200.0, 50.0)μm,
+ Point(1100.0, -150.0)μm,
+ Point(400.0, -200.0)μm
+]
+
+# Create channel
+channel_path = Path()
+straight!(channel_path, 1mm, Paths.Trace(0.1mm))
+channel = Paths.RouteChannel(channel_path)
+# Initialize paths
+paths = [Path(p) for p in p0s]
+# Define route rule
+transition_rule = Paths.StraightAnd90(25μm) # Manhattan with 25μm bend radius
+margin = 50.0μm # Room for bends between endpoints and channel
+rule = Paths.SingleChannelRouting(channel, transition_rule, margin)
+# Set tracks
+tracks = [1, 2, 3, 4, 4] # Last two share a track
+setindex!.(Ref(rule.segment_tracks), tracks, paths)
+# Draw routes
+for (pa, p1) in zip(paths, p1s)
+ route!(pa, p1, 0.0°, rule, Paths.Trace(2μm))
+end
+c = Cell("test")
+render!.(c, paths, GDSMeta())
+render!(c, channel_path, GDSMeta(1))
+save("straight_channel.svg", flatten(c); width=6inch, height=2inch);
+nothing; # hide
+```
+
+```@raw html
+
+```
+
+We can also have curved channels, like the `BSpline`-based example below. For the transition rule, `StraightAnd90` would no longer work for the paths that join the channel at an angle, so we use `BSplineRouting` instead. We also enable `auto_speed` and `auto_curvature` on that rule to help smooth out the B-splines and maintain continuous curvature.
+
+```@example 1
+# Create BSpline channel
+channel_path = Path()
+bspline!(
+ channel_path,
+ [Point(0.5, 0.5)mm, Point(1.0mm, 0.0μm)],
+ 0°,
+ Paths.Trace(0.1mm),
+ auto_speed=true,
+ auto_curvature=true
+)
+channel = Paths.RouteChannel(channel_path)
+# Initialize paths
+paths = [Path(p) for p in p0s]
+# Define route rule
+transition_rule = Paths.BSplineRouting(auto_speed=true, auto_curvature=true)
+margin = 50.0μm
+rule = Paths.SingleChannelRouting(channel, transition_rule, margin)
+# Set tracks
+tracks = [1, 2, 3, 4, 4] # Last two share a track
+setindex!.(Ref(rule.segment_tracks), tracks, paths)
+# Draw routes
+for (pa, p1) in zip(paths, p1s)
+ route!(pa, p1, 0.0°, rule, Paths.Trace(2μm))
+end
+c = Cell("test")
+render!.(c, paths, GDSMeta())
+render!(c, channel_path, GDSMeta(1))
+save("bspline_channel.svg", flatten(c); width=6inch, height=4inch);
+nothing; # hide
+```
+
+```@raw html
+
+```
+
+Channels can also have variable width, like the example below using the `TaperTrace` style on a compound segment consisting of four turns.
+
+```@example 1
+# Create tapered, composite channel
+channel_path = Path()
+turn!(channel_path, 90°, 0.25mm, Paths.Trace(0.1mm))
+turn!(channel_path, -90°, 0.25mm)
+turn!(channel_path, -90°, 0.25mm)
+turn!(channel_path, 90°, 0.25mm)
+simplify!(channel_path)
+setstyle!(channel_path[1], Paths.TaperTrace(0.1mm, 0.05mm))
+channel = Paths.RouteChannel(channel_path)
+# Initialize paths
+paths = [Path(p) for p in p0s]
+# Define route rule
+transition_rule = Paths.BSplineRouting(auto_speed=true, auto_curvature=true)
+margin = 50.0μm
+rule = Paths.SingleChannelRouting(channel, transition_rule, margin)
+# Set tracks
+tracks = [1, 2, 3, 4, 4] # Last two share a track
+setindex!.(Ref(rule.segment_tracks), tracks, paths)
+# Draw routes
+for (pa, p1) in zip(paths, p1s)
+ route!(pa, p1, 0.0°, rule, Paths.Trace(2μm))
+end
+c = Cell("test")
+render!.(c, paths, GDSMeta())
+render!(c, channel_path, GDSMeta(1))
+save("compound_channel.svg", flatten(c); width=6inch, height=4inch);
+nothing; # hide
+```
+
+```@raw html
+
+```
diff --git a/docs/src/schematicdriven/schematics.md b/docs/src/schematicdriven/schematics.md
index 57c7cbe3..9425d974 100644
--- a/docs/src/schematicdriven/schematics.md
+++ b/docs/src/schematicdriven/schematics.md
@@ -35,18 +35,61 @@ attach!(::SchematicDrivenLayout.SchematicGraph,
A [`Paths.Route`](@ref) 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!`](@ref), 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.
+
!!! info "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`](@ref SchematicDrivenLayout.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.
+For example, a workflow might start by creating a node with a "chip template" component containing port [`hooks`](@ref SchematicDrivenLayout.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. 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!`](@ref), 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.
+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!`](@ref). This creates a `RouteNode` with edges to the `ComponentNode`s at its start and end points. Then, after `plan`, the initial floorplanning phase has determined the position of all fixed `Component`s, so the route node can find a path between its start and end points.
!!! tip "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!`.
+### 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:
+
+```julia
+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`](@ref) rule, the router will look for the rule's [`Paths.RouteChannel`](@ref)
+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
```@docs
@@ -96,33 +139,6 @@ position in schematic-global or wafer-global coordinates as an argument:
SchematicDrivenLayout.position_dependent_replace!
```
-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
-
-```julia
-### 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 `Path`s and `RouteComponent`s, including those nested within composite components. This is based on [`Path` intersection functionality](../paths.md#Intersections).
diff --git a/src/DeviceLayout.jl b/src/DeviceLayout.jl
index e8e65465..de761e9f 100644
--- a/src/DeviceLayout.jl
+++ b/src/DeviceLayout.jl
@@ -191,7 +191,7 @@ Return the coordinate type of the geometry.
coordinatetype(::Type{S}) where {T, S <: AbstractGeometry{T}} = T
coordinatetype(::S) where {T, S <: AbstractGeometry{T}} = T
coordinatetype(::AbstractArray{S}) where {T, S <: AbstractGeometry{T}} = T
-coordinatetype(iterable) = promote_type(coordinatetype.(iterable))
+coordinatetype(iterable) = promote_type(coordinatetype.(iterable)...)
# Entity interface
include("entities.jl")
diff --git a/src/hooks.jl b/src/hooks.jl
index 9b82b38e..032458b6 100644
--- a/src/hooks.jl
+++ b/src/hooks.jl
@@ -4,6 +4,7 @@
Contains information describing how one component can attach to others.
"""
abstract type Hook{T} <: AbstractGeometry{T} end
+getp(h::Hook) = h.p
"""
PointHook(p::Point, in_direction)
diff --git a/src/paths/channels.jl b/src/paths/channels.jl
new file mode 100644
index 00000000..42cb5f55
--- /dev/null
+++ b/src/paths/channels.jl
@@ -0,0 +1,319 @@
+"""
+ RouteChannel{T} <: AbstractComponent{T}
+ RouteChannel(pa::Path)
+
+A channel that routes can be guided through along parallel tracks.
+
+Used in `route!` with [`Paths.SingleChannelRouting`](@ref).
+
+The `Path` used to construct a `RouteChannel` should use `Trace` styles only.
+
+A `RouteChannel` is an `AbstractComponent` with the same hooks as its `Path` and
+an empty geometry.
+"""
+struct RouteChannel{T} <: AbstractComponent{T}
+ path::Path{T}
+ node::Node{T} # path as single node
+end
+name(ch::RouteChannel) = name(ch.path)
+DeviceLayout.hooks(ch::RouteChannel) = DeviceLayout.hooks(ch.path)
+
+function RouteChannel(pa::Path{T}) where {T}
+ length(nodes(pa)) != 1 && return RouteChannel{T}(pa, simplify(pa))
+ return RouteChannel{T}(pa, only(nodes(pa)))
+end
+
+# Return a node corresponding to the section of the channel that the segment actually runs through
+function segment_channel_section(
+ ch::RouteChannel{T},
+ wireseg_start,
+ wireseg_stop,
+ prev_width,
+ next_width;
+ margin=zero(T)
+) where {T}
+ d = wireseg_stop - wireseg_start
+ # Adjust for margins and track vs channel direction to get the channel node section used by actual segment
+ if abs(d) <= 2 * margin + prev_width / 2 + next_width / 2
+ # handle case where margin consumes entire segment
+ # Just have a zero length Straight at the midpoint
+ track_mid = (wireseg_start + wireseg_stop) / 2
+ midpoint = ch.node.seg(track_mid)
+ middir = direction(ch.node.seg, track_mid)
+ channel_section = Node(
+ Straight(zero(T); p0=midpoint, α0=middir),
+ SimpleTrace(width(ch.node.sty, track_mid))
+ )
+ elseif d > zero(d) # segment is along channel direction
+ channel_section = split(
+ ch.node,
+ [
+ wireseg_start + margin + prev_width / 2,
+ wireseg_stop - margin - next_width / 2
+ ]
+ )[2]
+ elseif d < zero(d) # segment is counter to channel direction
+ channel_section = reverse(
+ split(
+ ch.node,
+ [
+ wireseg_stop + margin + next_width / 2,
+ wireseg_start - margin - prev_width / 2
+ ]
+ )[2]
+ )
+ end
+ return channel_section
+end
+
+# Actual routed path segment along a track offset from the channel path
+function track_path_segment(n_tracks, channel_section, track_idx; reversed=false)
+ return offset(
+ channel_section.seg,
+ track_section_offset(n_tracks, width(channel_section.sty), track_idx; reversed)
+ )
+end
+
+# Offset coordinate or function for the section of track with given width
+function track_section_offset(
+ n_tracks,
+ section_width::Coordinate,
+ track_idx;
+ reversed=false
+)
+ # (spacing) * number of tracks away from middle track
+ sgn = reversed ? -1 : 1
+ spacing = section_width / (n_tracks + 1)
+ return sgn * spacing * ((1 + n_tracks) / 2 - track_idx)
+end
+
+function track_section_offset(n_tracks, section_width::Function, track_idx; reversed=false)
+ # (spacing) * number of tracks away from middle track
+ return t ->
+ (reversed ? -1 : 1) *
+ (section_width(t) / (n_tracks + 1)) *
+ ((1 + n_tracks) / 2 - track_idx)
+end
+
+reverse(n::Node) = Paths.Node(reverse(n.seg), reverse(n.sty, pathlength(n.seg)))
+######## Methods required to use segments and styles as RouteChannels
+function reverse(b::BSpline{T}) where {T}
+ p = reverse(b.p)
+ t0 = RotationPi()(b.t1)
+ t1 = RotationPi()(b.t0)
+ # Use true t range for interpolations defined by points that have been scaled out of [0,1]
+ tmin = b.r.ranges[1][1]
+ tmax = b.r.ranges[1][end]
+ (tmin == 0 && tmax == 1) && return BSpline(p, t0, t1)
+ p0 = b.p1
+ p1 = b.p0
+ r = Interpolations.scale(
+ interpolate(p, Interpolations.BSpline(Cubic(NeumannBC(t0, t1)))),
+ range(1 - tmax, stop=1 - tmin, length=length(p))
+ )
+ α0 = rotated_direction(b.α1, RotationPi())
+ α1 = rotated_direction(b.α0, RotationPi())
+ return BSpline(p, t0, t1, r, p0, p1, α0, α1)
+end
+reverse(s::Turn) = Turn(-s.α, s.r, p1(s), α1(s) + 180°)
+reverse(s::Straight) = Straight(s.l, p1(s), s.α0 + 180°)
+# Reversing a GeneralTrace requires knowing its length, so we'll require that as an argument even if unused
+reverse(s::TaperTrace{T}, l) where {T} = TaperTrace{T}(s.width_end, s.width_start, s.length)
+reverse(s::SimpleTrace, l) = s
+reverse(s::GeneralTrace, l) = GeneralTrace(t -> width(s, l - t))
+# Define methods for CPW even though they're not allowed for channels
+reverse(s::TaperCPW{T}, l) where {T} =
+ TaperCPW{T}(s.trace_end, s.gap_end, s.trace_start, s.gap_start, s.length)
+reverse(s::SimpleCPW, l) = s
+reverse(s::GeneralCPW, l) = GeneralCPW(t -> trace(s, l - t), t -> gap(s, l - t))
+# For compound segments, reverse the individual sections and reverse their order
+# Keep the same tag so if a compound segment/style pair matched before they will still match
+reverse(s::CompoundSegment) = CompoundSegment(reverse(reverse.(s.segments)), s.tag)
+function reverse(s::CompoundStyle{T}, l) where {T}
+ lengths = diff(s.grid)
+ return CompoundStyle{T}(
+ reverse(reverse.(s.styles, lengths)),
+ [zero(T); cumsum(reverse(lengths))],
+ s.tag
+ )
+end
+
+abstract type AbstractMultiRouting <: RouteRule end
+
+abstract type AbstractChannelRouting <: AbstractMultiRouting end
+
+function _route!(
+ p::Path{T},
+ p1::Point,
+ α1,
+ rule::AbstractChannelRouting,
+ sty,
+ waypoints,
+ waydirs
+) where {T}
+ # Track segments for each channel
+ track_path_segs = track_path_segments(rule, p, p1)
+ waypoints = Point{T}[] # Segments too short for margins will just become waypoints for transitions
+ # Add segments and transitions
+ for (track_path_seg, next_entry_rule) in zip(track_path_segs, entry_rules(rule))
+ if iszero(pathlength(track_path_seg)) # Was too short for margins
+ push!(waypoints, p0(track_path_seg))
+ else
+ route!(
+ p,
+ p0(track_path_seg),
+ α0(track_path_seg),
+ next_entry_rule,
+ sty;
+ waypoints
+ )
+ push!(p, Node(track_path_seg, sty), reconcile=false) # p0, α0 reconciled by construction
+ p[end - 1].next = p[end]
+ p[end].prev = p[end - 1]
+ # Note `auto_curvature` BSpline uses curvature from end of previous segment
+ # and is not reconciled with the new node
+ # But we can do this ourselves
+ _reconcile_curvature!(p[end - 1], next_entry_rule)
+ empty!(waypoints)
+ end
+ end
+ # Exit
+ route!(p, p1, α1, exit_rule(rule), sty; waypoints)
+ _reconcile_curvature!(p[end], exit_rule(rule))
+ return
+end
+
+function _reconcile_curvature!(n::Node{T}, rule::RouteRule) where {T} end
+function _reconcile_curvature!(n::Node{T}, rule::BSplineRouting) where {T}
+ !rule.auto_curvature && return
+ κ0 = if n.prev === n
+ 0.0 / oneunit(T)
+ else
+ signed_curvature(segment(n.prev), pathlength(segment(n.prev)))
+ end
+ κ1 = if n.next === n
+ 0.0 / oneunit(T)
+ else
+ signed_curvature(segment(n.next), zero(coordinatetype(n)))
+ end
+ _set_endpoints_curvature!(segment(n), κ0, κ1)
+ if rule.auto_speed
+ _optimize_bspline!(segment(n); endpoints_curvature=(κ0, κ1))
+ else
+ _update_interpolation!(segment(n))
+ end
+end
+
+"""
+ struct SingleChannelRouting{T <: Coordinate} <: AbstractChannelRouting
+ SingleChannelRouting(ch::RouteChannel, transition_rule::RouteRule, margin::T)
+ SingleChannelRouting(ch::RouteChannel, transition_rules, margins)
+
+A `RouteRule` for guiding routed paths along tracks in a [`Paths.RouteChannel`](@ref).
+
+## Tracks
+
+"Tracks" are offsets of the channel's path, with equal spacing between each other
+and the extents of the channel's trace width. Tracks are ordered from left to right
+when facing along the channel. For example, track 1 is the top track
+(most positive offset) for a channel directed along the positive x axis, while
+the highest track index is its bottom track.
+
+The user manually assigns tracks to paths that will be routed with
+`rule::SingleChannelRouting` using `Paths.set_track!(rule, path, track_idx)` for each path,
+prior to calling `route!(path, ...)`. Because the track offset depends on the total number
+of tracks, and the number of tracks is determined by the maximum track index of any path
+added to `rule`, all paths should be assigned tracks before any `route!` call.
+
+If used for schematic routing, the track is supplied as a keyword argument,
+defaulting to a new track added at the bottom of the channel:
+`route!(g::SchematicGraph, rule, ...; track=num_tracks(rule)+1)`.
+
+## Routing
+
+A path routed from `p0` to `p1` using this rule will enter the channel
+at the channel's closest point to `p0` and exit at the closest point to `p1` if
+`margin` is zero. For nonzero `margin`, the entry and exit points are each shifted
+towards the other along the channel by `margin`, allowing more space for the
+transitions into and out of the channel.
+
+The middle "tracked" section is offset from the channel's center line according to
+the path's track, the maximum track assigned to any path by the rule,
+and the channel width.
+
+The path is routed from `p0` to the tracked section and from the tracked section
+to `p1` using `transition_rule`.
+
+Transition rules and margins can also be supplied as tuples to the constructor
+to allow different parameters for entry and exit transitions.
+"""
+mutable struct SingleChannelRouting{T <: Coordinate} <: AbstractChannelRouting
+ channel::RouteChannel{T}
+ transition_rules::Tuple{<:RouteRule, <:RouteRule}
+ transition_margins::Tuple{T, T}
+ segment_tracks::Dict{Path, Int}
+ global_channel::RouteChannel{T}
+ function SingleChannelRouting(
+ ch::RouteChannel{T},
+ rules,
+ margins,
+ tracks=Dict{Path, Int}()
+ ) where {T}
+ return new{T}(ch, rules, margins, tracks)
+ end
+end
+function SingleChannelRouting(
+ ch::RouteChannel{T},
+ rule::RouteRule,
+ margin,
+ tracks...
+) where {T}
+ return SingleChannelRouting(ch, (rule, rule), (margin, margin), tracks...)
+end
+function channel(rule::SingleChannelRouting)
+ isdefined(rule, :global_channel) && return rule.global_channel
+ return rule.channel
+end
+entry_rules(scr::SingleChannelRouting) = [first(scr.transition_rules)]
+exit_rule(scr::SingleChannelRouting) = last(scr.transition_rules)
+entry_margin(scr::SingleChannelRouting) = first(scr.transition_margins)
+exit_margin(scr::SingleChannelRouting) = last(scr.transition_margins)
+function num_tracks(scr::SingleChannelRouting)
+ isempty(scr.segment_tracks) && return 0
+ return maximum(values(scr.segment_tracks))
+end
+function track_idx(scr, pa)
+ return scr.segment_tracks[pa]
+end
+
+"""
+ set_track!(rule::SingleChannelRouting, pa::Path, track_idx::Int)
+
+Sets `pa` to be routed along track `track_idx` in the channel used by `rule`.
+
+Tracks are ordered from left to right when facing along the channel.
+For example, track 1 is the top track (most positive offset) for a
+channel directed along the positive x axis, while the highest track index is its bottom track.
+"""
+function set_track!(scr, pa, track_idx)
+ return scr.segment_tracks[pa] = track_idx
+end
+
+function track_path_segments(rule::SingleChannelRouting, pa::Path, endpt)
+ wireseg_start = pathlength_nearest(channel(rule).node.seg, p1(pa))
+ wireseg_stop = pathlength_nearest(channel(rule).node.seg, endpt)
+ return [
+ track_path_segment(
+ num_tracks(rule),
+ segment_channel_section(
+ channel(rule),
+ wireseg_start,
+ wireseg_stop,
+ 2 * entry_margin(rule),
+ 2 * exit_margin(rule)
+ ),
+ track_idx(rule, pa),
+ reversed=wireseg_start > wireseg_stop
+ )
+ ]
+end
diff --git a/src/paths/contstyles/compound.jl b/src/paths/contstyles/compound.jl
index 99cd1c12..ddc6519f 100644
--- a/src/paths/contstyles/compound.jl
+++ b/src/paths/contstyles/compound.jl
@@ -65,6 +65,14 @@ for x in (:extent, :width, :trace, :gap)
sty, teff = s(t)
return ($x)(sty, teff)
end
+ @eval function ($x)(s::CompoundStyle)
+ # If all the xs are the same constant we can just return the constant
+ uniquexs = unique(($x).(s.styles))
+ if length(uniquexs) == 1 && only(uniquexs) isa Coordinate
+ return only(uniquexs)
+ end
+ return Base.Fix1(($x), s)
+ end
end
summary(::CompoundStyle) = "Compound style"
diff --git a/src/paths/contstyles/cpw.jl b/src/paths/contstyles/cpw.jl
index 479855ea..371d7700 100644
--- a/src/paths/contstyles/cpw.jl
+++ b/src/paths/contstyles/cpw.jl
@@ -15,8 +15,11 @@ struct GeneralCPW{S, T} <: CPW{false}
end
copy(x::GeneralCPW) = GeneralCPW(x.trace, x.gap)
extent(s::GeneralCPW, t) = s.trace(t) / 2 + s.gap(t)
+extent(s::GeneralCPW) = Base.Fix1(extent, s)
trace(s::GeneralCPW, t) = s.trace(t)
+trace(s::GeneralCPW) = s.trace
gap(s::GeneralCPW, t) = s.gap(t)
+gap(s::GeneralCPW) = s.gap
translate(s::GeneralCPW, t) = GeneralCPW(x -> s.trace(x + t), x -> s.gap(x + t))
"""
diff --git a/src/paths/contstyles/strands.jl b/src/paths/contstyles/strands.jl
index b0bf41b9..9b59c522 100644
--- a/src/paths/contstyles/strands.jl
+++ b/src/paths/contstyles/strands.jl
@@ -32,7 +32,7 @@ extent(s::GeneralStrands, t) =
offset(s::GeneralStrands, t) = s.offset(t)
width(s::GeneralStrands, t) = s.width(t)
spacing(s::GeneralStrands, t) = s.spacing(t)
-num(s::GeneralStrands, t) = s.num
+num(s::GeneralStrands, t...) = s.num
translate(s::GeneralStrands, t) =
GeneralStrands(x -> s.offset(x + t), x -> s.width(x + t), x -> s.spacing(x + t), s.num)
diff --git a/src/paths/contstyles/tapers.jl b/src/paths/contstyles/tapers.jl
index a93d0d8d..48348ce0 100644
--- a/src/paths/contstyles/tapers.jl
+++ b/src/paths/contstyles/tapers.jl
@@ -15,8 +15,12 @@ struct TaperTrace{T <: Coordinate} <: Trace{true}
end
copy(x::TaperTrace{T}) where {T} = TaperTrace{T}(x.width_start, x.width_end, x.length)
extent(s::TaperTrace, t) = 0.5 * width(s, t)
+extent(s::TaperTrace) = t -> extent(s, t)
width(s::TaperTrace, t) =
(1 - uconvert(NoUnits, t / s.length)) * s.width_start + t / s.length * s.width_end
+width(s::TaperTrace) = t -> width(s, t)
+trace(s::TaperTrace, t) = width(s, t)
+trace(s::TaperTrace) = width(s)
function pin(s::TaperTrace{T}; start=nothing, stop=nothing) where {T}
iszero(s.length) && error("cannot `pin`; length of $s not yet determined.")
@@ -71,10 +75,13 @@ copy(x::TaperCPW{T}) where {T} =
extent(s::TaperCPW, t) =
(1 - uconvert(NoUnits, t / s.length)) * (0.5 * s.trace_start + s.gap_start) +
(t / s.length) * (0.5 * s.trace_end + s.gap_end)
+extent(s::TaperCPW) = Base.Fix1(extent, s)
trace(s::TaperCPW, t) =
(1 - uconvert(NoUnits, t / s.length)) * s.trace_start + t / s.length * s.trace_end
+trace(s::TaperCPW) = Base.Fix1(trace, s)
gap(s::TaperCPW, t) =
(1 - uconvert(NoUnits, t / s.length)) * s.gap_start + t / s.length * s.gap_end
+gap(s::TaperCPW) = Base.Fix1(gap, s)
function TaperCPW(
trace_start::Coordinate,
gap_start::Coordinate,
diff --git a/src/paths/contstyles/trace.jl b/src/paths/contstyles/trace.jl
index ece83564..673b4b6e 100644
--- a/src/paths/contstyles/trace.jl
+++ b/src/paths/contstyles/trace.jl
@@ -12,8 +12,11 @@ struct GeneralTrace{T} <: Trace{false}
end
copy(x::GeneralTrace) = GeneralTrace(x.width)
extent(s::GeneralTrace, t) = 0.5 * s.width(t)
+extent(s::GeneralTrace) = Base.Fix1(extent, s)
width(s::GeneralTrace, t) = s.width(t)
+width(s::GeneralTrace) = s.width
trace(s::GeneralTrace, t) = s.width(t)
+trace(s::GeneralTrace) = s.width
translate(s::GeneralTrace, t) = GeneralTrace(x -> s.width(x + t))
"""
diff --git a/src/paths/paths.jl b/src/paths/paths.jl
index a33a78b8..65a3040b 100644
--- a/src/paths/paths.jl
+++ b/src/paths/paths.jl
@@ -26,7 +26,8 @@ import Base:
intersect!,
show,
summary,
- dims2string
+ dims2string,
+ reverse
import Base.Iterators
@@ -48,9 +49,11 @@ import DeviceLayout:
Hook,
Meta,
PointHook,
+ Polygon,
Polygons,
Reflection,
Rotation,
+ RotationPi,
ScaledIsometry,
StructureReference,
XReflection,
@@ -349,7 +352,7 @@ Return `s` on `seg` that minimizes `norm(seg(s) - pt)`.
"""
function pathlength_nearest(seg::Paths.Segment{T}, pt::Point) where {T}
errfunc(s) = ustrip(unit(T), norm(seg(s * pathlength(seg)) - pt))
- return Optim.minimizer(optimize(errfunc, 0.0, 1.0))[1] * oneunit(T)
+ return Optim.minimizer(optimize(errfunc, 0.0, 1.0))[1] * pathlength(seg)
end
"""
@@ -506,15 +509,21 @@ end
transform(x::Node, f::Transformation) = transform(x, ScaledIsometry(f))
function transform(x::Node, f::ScaledIsometry)
y = deepcopy(x)
+ new_p0 = f(p0(y.seg)) # Handedness change can change p0, α0 for offset segment
+ new_α0 = rotated_direction(α0(y.seg), f) # So calculate them in advance
+ # But handedness matters for offset setα0p0! calculation so we still do it first
xrefl(f) && change_handedness!(y)
- setα0p0!(y.seg, rotated_direction(α0(y.seg), f), f(p0(y.seg)))
+ setα0p0!(y.seg, new_α0, new_p0)
return y
end
function transform(x::Segment, f::Transformation)
y = deepcopy(x)
+ new_p0 = f(p0(y)) # Handedness change can change p0, α0 for offset segment
+ new_α0 = rotated_direction(α0(y), f) # So calculate them in advance
+ # But handedness matters for offset setα0p0! calculation so we still do it first
xrefl(f) && change_handedness!(y)
- setα0p0!(y, rotated_direction(α0(y), f), f(p0(y)))
+ setα0p0!(y, new_α0, new_p0)
return y
end
@@ -697,9 +706,12 @@ include("segments/compound.jl")
include("segments/bspline.jl")
include("segments/offset.jl")
include("segments/bspline_approximation.jl")
+include("segments/bspline_optimization.jl")
include("routes.jl")
+include("channels.jl")
+
function change_handedness!(seg::Union{Turn, Corner})
return seg.α = -seg.α
end
@@ -1313,7 +1325,7 @@ function split(seg::Segment, sty::Style, x)
end
function split(seg::ContinuousSegment, x)
- if !(zero(x) < x < pathlength(seg))
+ if !(zero(x) <= x <= pathlength(seg))
throw(ArgumentError("x must be between 0 and pathlength(seg)"))
end
return _split(seg, x)
diff --git a/src/paths/routes.jl b/src/paths/routes.jl
index 7311ff5b..fc94f9bf 100644
--- a/src/paths/routes.jl
+++ b/src/paths/routes.jl
@@ -33,6 +33,7 @@ abstract type RouteRule end
min_bend_radius = 200μm
max_bend_radius = Inf*μm
end
+ StraightAnd90(r) = StraightAnd90(min_bend_radius=r, max_bend_radius=r)
Specifies rules for routing from one point to another using straight segments and 90° bends.
@@ -46,12 +47,14 @@ Base.@kwdef struct StraightAnd90 <: RouteRule
min_bend_radius = 200μm
max_bend_radius = Inf * μm
end
+StraightAnd90(r) = StraightAnd90(min_bend_radius=r, max_bend_radius=r)
"""
Base.@kwdef struct StraightAnd45 <: RouteRule
min_bend_radius = 200μm
max_bend_radius = Inf*μm
end
+ StraightAnd45(r) = StraightAnd45(min_bend_radius=r, max_bend_radius=r)
Specifies rules for routing from one point to another using using straight segments and 45° bends.
@@ -65,10 +68,14 @@ Base.@kwdef struct StraightAnd45 <: RouteRule
min_bend_radius = 200μm
max_bend_radius = Inf * μm
end
+StraightAnd45(r) = StraightAnd45(min_bend_radius=r, max_bend_radius=r)
"""
Base.@kwdef struct BSplineRouting <: RouteRule
endpoints_speed = 2500μm
+ auto_speed = false
+ endpoints_curvature = nothing
+ auto_curvature = false
end
Specifies rules for routing from one point to another using BSplines.
@@ -77,6 +84,9 @@ Ignores `waydirs`.
"""
Base.@kwdef struct BSplineRouting <: RouteRule
endpoints_speed = 2500μm
+ auto_speed = false
+ endpoints_curvature = nothing
+ auto_curvature = false
end
"""
@@ -469,7 +479,10 @@ function _route!(
vcat(waypoints, [p_end]),
α_end,
sty,
- endpoints_speed=rule.endpoints_speed
+ endpoints_speed=rule.endpoints_speed,
+ auto_speed=rule.auto_speed,
+ endpoints_curvature=rule.endpoints_curvature,
+ auto_curvature=rule.auto_curvature
)
end
diff --git a/src/paths/segments/bspline.jl b/src/paths/segments/bspline.jl
index 8d309619..9d093e1f 100644
--- a/src/paths/segments/bspline.jl
+++ b/src/paths/segments/bspline.jl
@@ -142,7 +142,7 @@ Reconcile the interpolation `b.r` with possible changes to `b.p`, `b.t0`, `b.t1`
Also updates `b.p0`, `b.p1`.
"""
-function _update_interpolation!(b::BSpline)
+function _update_interpolation!(b::BSpline{T}) where {T}
# Use true t range for interpolations defined by points that have been scaled out of [0,1]
tmin = b.r.ranges[1][1]
tmax = b.r.ranges[1][end]
@@ -316,7 +316,11 @@ function curvatureradius(b::BSpline{T}, s) where {T}
end
"""
- bspline!(p::Path{T}, nextpoints, α_end, sty::Style=contstyle1(p), endpoints_speed=2500μm)
+ bspline!(p::Path{T}, nextpoints, α_end, sty::Style=contstyle1(p);
+ endpoints_speed=2500μm,
+ endpoints_curvature=nothing,
+ auto_speed=false,
+ auto_curvature=false)
Add a BSpline interpolation from the current endpoint of `p` through `nextpoints`.
@@ -324,6 +328,19 @@ The interpolation reaches `nextpoints[end]` making the angle `α_end` with the p
The `endpoints_speed` is "how fast" the interpolation leaves and enters its endpoints. Higher
speed means that the start and end angles are approximately α1(p) and α_end over a longer
distance.
+
+If `auto_speed` is `true`, then `endpoints_speed` is ignored. Instead, the
+endpoint speeds are optimized to make curvature changes gradual as possible
+(minimizing the integrated square of the curvature with respect
+to arclength).
+
+If `endpoints_curvature` (dimensions of `oneunit(T)^-1`) is specified, then
+additional waypoints are placed so that the curvature at the endpoints is equal to
+`endpoints_curvature`.
+
+If `auto_curvature` is specified, then `endpoints_curvature` is ignored.
+Instead, the curvature at the end of the previous segment of the path is used, or
+zero curvature if the path was empty.
"""
function bspline!(
p::Path{T},
@@ -331,6 +348,9 @@ function bspline!(
α_end,
sty::Style=contstyle1(p);
endpoints_speed=2500.0 * DeviceLayout.onemicron(T),
+ endpoints_curvature=nothing,
+ auto_speed=false,
+ auto_curvature=false,
kwargs...
) where {T}
!isempty(p) &&
@@ -342,6 +362,16 @@ function bspline!(
t0 = endpoints_speed * Point(cos(α1(p)), sin(α1(p)))
t1 = endpoints_speed * Point(cos(α_end), sin(α_end))
seg = BSpline(ps, t0, t1)
+ auto_curvature && (endpoints_curvature = _last_curvature(p))
+ if auto_speed
+ seg.t0 = Point(cos(α0(seg)), sin(α0(seg))) * norm(seg.p[2] - seg.p[1])
+ seg.t1 = Point(cos(α1(seg)), sin(α1(seg))) * norm(seg.p[end] - seg.p[end - 1])
+ _set_endpoints_curvature!(seg, endpoints_curvature, add_points=true)
+ _optimize_bspline!(seg; endpoints_curvature)
+ elseif !isnothing(endpoints_curvature)
+ _set_endpoints_curvature!(seg, endpoints_curvature, add_points=true)
+ _update_interpolation!(seg)
+ end
push!(p, Node(seg, convert(ContinuousStyle, sty)))
return nothing
end
diff --git a/src/paths/segments/bspline_optimization.jl b/src/paths/segments/bspline_optimization.jl
new file mode 100644
index 00000000..8d076178
--- /dev/null
+++ b/src/paths/segments/bspline_optimization.jl
@@ -0,0 +1,193 @@
+
+function _optimize_bspline!(b::BSpline; endpoints_curvature=nothing)
+ scale0 = Point(cos(α0(b)), sin(α0(b))) * norm(b.p[2] - b.p[1])
+ scale1 = Point(cos(α1(b)), sin(α1(b))) * norm(b.p[end] - b.p[end - 1])
+ if _symmetric_optimization(b)
+ errfunc_sym(p) = _int_dκ2(b, p[1], scale0, scale1; endpoints_curvature)
+ p = Optim.minimizer(optimize(errfunc_sym, [1.0]))
+ b.t0 = p[1] * scale0
+ b.t1 = p[1] * scale1
+ else
+ errfunc_asym(p) = _int_dκ2(b, p[1], p[2], scale0, scale1; endpoints_curvature)
+ p = Optim.minimizer(optimize(errfunc_asym, [1.0, 1.0]))
+ b.t0 = p[1] * scale0
+ b.t1 = p[2] * scale1
+ end
+ _set_endpoints_curvature!(b, endpoints_curvature)
+ return _update_interpolation!(b)
+end
+
+function _set_endpoints_curvature!(::BSpline, ::Nothing; add_points=false) end
+function _set_endpoints_curvature!(b::BSpline, κ0κ1; add_points=false)
+ return _set_endpoints_curvature!(b, κ0κ1[1], κ0κ1[2]; add_points)
+end
+
+function _set_endpoints_curvature!(
+ b::BSpline{T},
+ κ0::Union{Float64, DeviceLayout.InverseLength}=0.0 / oneunit(T),
+ κ1=κ0;
+ add_points=false
+) where {T}
+ # Set waypoints after the start and before the end to fix curvature
+ # Usually, interpolation coefficients c are solutions to Ac = b0
+ # For tangent boundary condition, b0 = [t0, p..., t1]
+ # And A is tridiagonal (1/6, 2/3, 16) except for first and last row
+ # which are [-1/2, 0, 1/2, ...] and [..., -1/2, 0, 1/2] for tangent constraints
+ # We'll add two rows so that p_2 and p_{n-1} are unknowns
+ # Giving enough degrees of freedom to also constrain curvature
+ # Then update both A and b0 to give the following equations:
+ # 1/6 * c1 + 2/3 * c2 + 1/6 * c3 - p_2 = 0 (i.e. move p_2 to LHS)
+ # c1 - 2 * c2 + c3 = h0 (where h is the second derivative)
+ # And similarly for p_{n-1}
+ # Then solve A * c = b0 for c, so that our new p_2 is c[n-1] and p_{n-1} is c[n]
+ if add_points # Add extra waypoints rather than adjust existing ones
+ insert!(b.p, 2, b.p[1])
+ insert!(b.p, length(b.p), b.p[end])
+ # Rescale gradient BC
+ b.t0 = b.t0 * (length(b.p) - 3) / (length(b.p) - 1)
+ b.t1 = b.t1 * (length(b.p) - 3) / (length(b.p) - 1)
+ end
+ # If there are only 4 points the formula is simple to write out
+ if length(b.p) == 4 && iszero(κ0) && iszero(κ1)
+ b.p .= [
+ b.p[1],
+ 5 / 6 * b.p[1] + 2 / 3 * b.t0 + 1 / 6 * b.p[end] - b.t1 / 6,
+ 5 / 6 * b.p[end] - 2 / 3 * b.t1 + 1 / 6 * b.p[1] + b.t0 / 6,
+ b.p[end]
+ ]
+ return
+ end
+ # Define LHS matrix
+ n = length(b.p) + 4
+ dl = fill(1 / 6, n - 1)
+ d = fill(2 / 3, n)
+ du = fill(1 / 6, n - 1)
+ A = Array(Tridiagonal(dl, d, du))
+ A[1, 1:3] .= [-1 / 2, 0, 1 / 2] # -c1/2 + c3/2 = g0
+ A[3, n - 1] = -1 # c1/6 + 2c2/3 + c3/6 - p_2 = 0
+ A[n - 4, n] = -1 # ... -p_{n-1} = 0
+ A[n - 2, (n - 4):(n - 1)] .= [-1 / 2, 0, 1 / 2, 0] # g1, need to erase another tridiagonal too
+ A[n - 1, :] .= 0 # erase tridiagonal for κ0
+ A[n - 1, 1:3] .= [1, -2, 1] # c1 - 2c2 + c3 = h0
+ A[n, :] .= 0 # κ1
+ A[n, (n - 4):(n - 2)] .= [1, -2, 1] # κ1
+ zer = zero(Point{T})
+ # Set curvature with zero acceleration
+ h0 = Point{T}(-b.t0.y * κ0 * norm(b.t0), b.t0.x * κ0 * norm(b.t0))
+ h1 = Point{T}(-b.t1.y * κ1 * norm(b.t1), b.t1.x * κ1 * norm(b.t1))
+ # RHS
+ b0 = [b.t0, b.p[1], zer, b.p[3:(end - 2)]..., zer, b.p[end], b.t1, h0, h1]
+ # Solve
+ cx = A \ ustrip.(unit(T), getx.(b0))
+ cy = A \ ustrip.(unit(T), gety.(b0))
+ # Update points
+ b.p[2] = oneunit(T) * Point(cx[n - 1], cy[n - 1])
+ b.p[end - 1] = oneunit(T) * Point(cx[n], cy[n])
+ # The rest of c already defines the interpolation
+ # But we'll just use the usual constructor after this
+ # when _update_interpolation! is called
+ return
+end
+
+# True iff endpoints_speed should be assumed to be equal for optimization
+# I.e., tangent directions, endpoints, and waypoints have mirror or 180° symmetry
+function _symmetric_optimization(b::BSpline{T}) where {T}
+ center = (b.p0 + b.p1) / 2
+ # 180 rotation?
+ if α0(b) ≈ α1(b)
+ return isapprox(
+ reverse(RotationPi(; around_pt=center).(b.p)),
+ b.p,
+ atol=1e-3 * DeviceLayout.onenanometer(T)
+ )
+ end
+
+ # Reflection?
+ mirror_axis = Point(-(b.p1 - b.p0).y, (b.p1 - b.p0).x)
+ refl = Reflection(mirror_axis; through_pt=center)
+ return isapprox_angle(α1(b), -rotated_direction(α0(b), refl)) &&
+ isapprox(reverse(refl.(b.p)), b.p, atol=1e-3 * DeviceLayout.onenanometer(T))
+end
+
+# Integrated square of curvature derivative (scale free)
+function _int_dκ2(
+ b::BSpline{T},
+ t0,
+ t1,
+ scale0::Point{T},
+ scale1::Point{T};
+ endpoints_curvature=nothing
+) where {T}
+ t0 <= zero(t0) || t1 <= zero(t1) && return Inf
+ b.t0 = t0 * scale0
+ b.t1 = t1 * scale1
+ _set_endpoints_curvature!(b, endpoints_curvature)
+ _update_interpolation!(b)
+ return _int_dκ2(b, sqrt(norm(scale0) * norm(scale1)))
+end
+
+# Symmetric version
+function _int_dκ2(
+ b::BSpline{T},
+ t0,
+ scale0::Point{T},
+ scale1::Point{T};
+ endpoints_curvature=nothing
+) where {T}
+ t0 <= zero(t0) && return Inf
+ b.t0 = t0 * scale0
+ b.t1 = t0 * scale1
+ _set_endpoints_curvature!(b, endpoints_curvature)
+ _update_interpolation!(b)
+ return _int_dκ2(b, sqrt(norm(scale0) * norm(scale1)))
+end
+
+function _int_dκ2(b::BSpline{T}, scale) where {T}
+ G = StaticArrays.@MVector [zero(Point{T})]
+ H = StaticArrays.@MVector [zero(Point{T})]
+ J = StaticArrays.@MVector [zero(Point{T})]
+
+ return uconvert(
+ NoUnits,
+ quadgk(t -> scale^3 * (dκdt_scaled!(b, t, G, H, J))^2, 0.0, 1.0, rtol=1e-3)[1]
+ )
+end
+
+# Third derivative of Cubic BSpline (piecewise constant)
+d3_weights(::Interpolations.Cubic, _) = (-1, 3, -3, 1)
+function d3r_dt3!(J, r, t)
+ n_rescale = (length(r.itp.coefs) - 2) - 1
+ wis = Interpolations.weightedindexes(
+ (d3_weights,),
+ Interpolations.itpinfo(r)...,
+ (t * n_rescale + 1,)
+ )
+ return J[1] = Interpolations.symmatrix(
+ map(inds -> Interpolations.InterpGetindex(r)[inds...], wis)
+ )[1]
+end
+
+# Derivative of curvature with respect to pathlength
+# As a function of BSpline parameter
+function dκdt_scaled!(
+ b::BSpline{T},
+ t::Float64,
+ G::AbstractArray{Point{T}},
+ H::AbstractArray{Point{T}},
+ J::AbstractArray{Point{T}}
+) where {T}
+ Paths.Interpolations.gradient!(G, b.r, t)
+ Paths.Interpolations.hessian!(H, b.r, t)
+ d3r_dt3!(J, b.r, t)
+ g = G[1]
+ h = H[1]
+ j = J[1]
+
+ dκdt = ( # d/dt ((g.x*h.y - g.y*h.x) / ||g||^3)
+ (g.x * j.y - g.y * j.x) / norm(g)^3 +
+ -3 * (g.x * h.y - g.y * h.x) * (g.x * h.x + g.y * h.y) / norm(g)^5
+ )
+ # Return so that (dκ/dt)^2 will be normalized by speed
+ # So we can integrate over t and retain scale independence
+ return dκdt / sqrt(norm(g))
+end
diff --git a/src/paths/segments/compound.jl b/src/paths/segments/compound.jl
index ed9b2ca9..3b1e3e73 100644
--- a/src/paths/segments/compound.jl
+++ b/src/paths/segments/compound.jl
@@ -28,7 +28,7 @@ end
copy(s::CompoundSegment) = (typeof(s))(s.segments, s.tag)
CompoundSegment(nodes::AbstractVector{Node{T}}, tag=gensym()) where {T} =
CompoundSegment{T}(map(segment, nodes), tag)
-CompoundSegment(segments::AbstractVector{Segment{T}}, tag=gensym()) where {T} =
+CompoundSegment(segments::AbstractVector{<:Segment{T}}, tag=gensym()) where {T} =
CompoundSegment{T}(segments, tag)
convert(::Type{CompoundSegment{T}}, x::CompoundSegment) where {T} =
diff --git a/src/paths/segments/offset.jl b/src/paths/segments/offset.jl
index df29937b..b100e321 100644
--- a/src/paths/segments/offset.jl
+++ b/src/paths/segments/offset.jl
@@ -18,7 +18,7 @@ abstract type OffsetSegment{T, S <: Segment{T}} <: ContinuousSegment{T} end
"""
struct ConstantOffset{T,S} <: OffsetSegment{T,S}
"""
-struct ConstantOffset{T, S} <: OffsetSegment{T, S}
+mutable struct ConstantOffset{T, S} <: OffsetSegment{T, S}
seg::S
offset::T
end
@@ -26,11 +26,21 @@ end
"""
struct GeneralOffset{T,S} <: OffsetSegment{T,S}
"""
-struct GeneralOffset{T, S} <: OffsetSegment{T, S}
+mutable struct GeneralOffset{T, S} <: OffsetSegment{T, S}
seg::S
offset
end
+convert(::Type{ConstantOffset{T}}, x::ConstantOffset) where {T} =
+ ConstantOffset(convert(Segment{T}, x.seg), convert(T, x.offset))
+convert(::Type{ConstantOffset{T}}, x::ConstantOffset{T}) where {T} = x
+convert(::Type{Segment{T}}, x::ConstantOffset) where {T} = convert(ConstantOffset{T}, x)
+
+convert(::Type{GeneralOffset{T}}, x::GeneralOffset) where {T} =
+ OffsetSegment(convert(Segment{T}, x.seg), x.offset)
+convert(::Type{GeneralOffset{T}}, x::GeneralOffset{T}) where {T} = x
+convert(::Type{Segment{T}}, x::GeneralOffset) where {T} = convert(GeneralOffset{T}, x)
+
copy(s::OffsetSegment) = OffsetSegment(copy(s.seg), s.offset)
getoffset(s::ConstantOffset, l...) = s.offset
getoffset(s::GeneralOffset{T}) where {T} = l -> uconvert(unit(T), s.offset(l))
@@ -65,6 +75,23 @@ function direction(s::GeneralOffset{T}, t) where {T}
return uconvert(°, atan(tang.y, tang.x))
end
+function setα0p0!(s::OffsetSegment, angle, p::Point)
+ rotation_angle = angle - α0(s)
+ rotated_offset = Rotation(rotation_angle)(p0(s) - p0(s.seg))
+ return setα0p0!(s.seg, α0(s.seg) + rotation_angle, p - rotated_offset)
+end
+
+function change_handedness!(x::ConstantOffset)
+ x.offset = -x.offset
+ return change_handedness!(x.seg)
+end
+
+function change_handedness!(x::GeneralOffset)
+ orig_offset = x.offset
+ x.offset = (t -> -orig_offset(t))
+ return change_handedness!(x.seg)
+end
+
function tangent(s::OffsetSegment, t)
# calculate derivative w.r.t. s.seg arclength
# d/dt (s.seg(t) + offset(s, t) * normal(s.seg, t))
@@ -83,6 +110,11 @@ function signed_curvature(seg::Segment, s)
return 1 / curvatureradius(seg, s)
end
+function _last_curvature(pa::Path{T}) where {T}
+ isempty(pa) && return 0.0 / oneunit(T)
+ return signed_curvature(pa[end].seg, pathlength(pa[end].seg))
+end
+
function curvatureradius(seg::ConstantOffset, s)
r = curvatureradius(seg.seg, s) # Ignore offset * dκds term
return r - getoffset(seg, s)
@@ -131,21 +163,15 @@ summary(s::OffsetSegment) = summary(s.seg) * " offset by $(s.offset)"
# Methods for creating offset segments
OffsetSegment(seg::S, offset::Coordinate) where {T, S <: Segment{T}} =
- ConstantOffset{T, S}(seg, offset)
-OffsetSegment(seg::S, offset) where {T, S <: Segment{T}} = GeneralOffset{T, S}(seg, offset)
+ ConstantOffset{T, S}(copy(seg), offset)
+OffsetSegment(seg::S, offset) where {T, S <: Segment{T}} =
+ GeneralOffset{T, S}(copy(seg), offset)
offset(seg::Segment, s) = OffsetSegment(seg, s)
offset(seg::ConstantOffset, s::Coordinate) = offset(seg.seg, s + seg.offset)
offset(seg::ConstantOffset, s) = offset(seg.seg, t -> s(t) + seg.offset)
offset(seg::GeneralOffset, s::Coordinate) = offset(seg.seg, t -> s + seg.offset(t))
offset(seg::GeneralOffset, s) = offset(seg.seg, t -> s(t) + seg.offset(t))
-function transform(x::ConstantOffset, f::Transformation)
- y = deepcopy(x)
- xrefl(f) && change_handedness!(y)
- setα0p0!(y.seg, rotated_direction(α0(y.seg), f), f(p0(y.seg)))
- return y
-end
-
# Define outer constructors for Turn and Straight from
Straight(x::ConstantOffset{T, Straight{T}}) where {T} =
Straight{T}(x.seg.l, p0=p0(x), α0=α0(x))
diff --git a/src/render/cpw.jl b/src/render/cpw.jl
index f8328734..728c7b18 100644
--- a/src/render/cpw.jl
+++ b/src/render/cpw.jl
@@ -45,6 +45,16 @@ function to_polygons(f, len, s::Paths.CPW; kwargs...)
return [Polygon(uniquepoints(ppts)), Polygon(uniquepoints(mpts))]
end
+function to_polygons(
+ seg::Paths.OffsetSegment{T},
+ s::Paths.CPW;
+ atol=DeviceLayout.onenanometer(T),
+ kwargs...
+) where {T}
+ bsp = Paths.bspline_approximation(seg; atol)
+ return to_polygons(bsp, s; atol, kwargs...)
+end
+
function to_polygons(f::Paths.Straight{T}, s::Paths.SimpleCPW; kwargs...) where {T}
g = cpw_points(f, s)
diff --git a/src/render/trace.jl b/src/render/trace.jl
index 854acf0a..ee86f697 100644
--- a/src/render/trace.jl
+++ b/src/render/trace.jl
@@ -13,6 +13,16 @@ function to_polygons(f, len, s::Paths.Trace; kwargs...)
return Polygon(uniquepoints(pts))
end
+function to_polygons(
+ seg::Paths.OffsetSegment{T},
+ s::Paths.Trace;
+ atol=DeviceLayout.onenanometer(T),
+ kwargs...
+) where {T}
+ bsp = Paths.bspline_approximation(seg; atol)
+ return to_polygons(bsp, s; atol, kwargs...)
+end
+
function to_polygons(segment::Paths.Straight{T}, s::Paths.SimpleTrace; kwargs...) where {T}
dir = direction(segment, zero(T))
dp, dm = dir + 90.0°, dir - 90.0°
diff --git a/src/schematics/components/builtin_components.jl b/src/schematics/components/builtin_components.jl
index 45b966ad..610d6582 100644
--- a/src/schematics/components/builtin_components.jl
+++ b/src/schematics/components/builtin_components.jl
@@ -36,6 +36,7 @@ A component with empty geometry and an 8-point [compass](@ref SchematicDrivenLay
p1::Point{T} = zero(Point{T})
end
Spacer(; kwargs...) = Spacer{typeof(1.0UPREFERRED)}(; kwargs...)
+Spacer(dx, dy; kwargs...) = Spacer(; p1=Point(dx, dy), kwargs...)
function hooks(s::Spacer{T}) where {T}
return merge(compass("p0_", p0=zero(Point{T})), compass("p1_", p0=s.p1))
end
diff --git a/src/schematics/routes.jl b/src/schematics/routes.jl
index 484f8b9a..cb0ff904 100644
--- a/src/schematics/routes.jl
+++ b/src/schematics/routes.jl
@@ -56,19 +56,35 @@ _undec(sty::Paths.Style) = sty
_undec(sty::Paths.DecoratedStyle) = sty.s # just remove attachments, not overlays
function path(rc::RouteComponent)
!isempty(rc._path) && return rc._path
-
+ path = rc._path
+ r = rc.r
+ path.p0 = r.p0
+ path.α0 = r.α0
+ path.name = rc.name
+ path.metadata = rc.meta
if length(rc.sty) == 1
- path = Path(rc.r, _undec(rc.sty[1])) # draw path with underlying style
+ route!(
+ path,
+ r.p1,
+ r.α1,
+ r.rule,
+ _undec(rc.sty[1]);
+ waypoints=r.waypoints,
+ waydirs=r.waydirs
+ )
redecorate!(path, rc.sty[1]) # apply decorations
else # Vector of styles for each segment
- path = Path(rc.r, _undec.(rc.sty))
+ route!(
+ path,
+ r.p1,
+ r.α1,
+ r.rule,
+ _undec.(rc.sty);
+ waypoints=r.waypoints,
+ waydirs=r.waydirs
+ )
redecorate!.(Ref(path), rc.sty)
end
- rc._path.name = rc.name
- rc._path.p0 = path.p0
- rc._path.α0 = path.α0
- rc._path.metadata = rc.meta
- rc._path.nodes = path.nodes
return rc._path
end
@@ -192,6 +208,7 @@ function route!(
fuse!(g, nodehook1, rn => :p0)
# fuse to nodehook2
fuse!(g, nodehook2, rn => :p1)
+ _update_with_graph!(rule, rn, g; kwargs...)
return rn
end
@@ -218,3 +235,35 @@ function attach!(
attach!(r, c, ti, location=li, mark_dirty=false)
end
end
+
+# Update rules with information from schematic or graph
+# Called from `route!(g::SchematicGraph, rule, ...)` and `plan`, respectively
+function _update_with_graph!(rule::RouteRule, route_node, graph; kwargs...) end
+function _update_with_plan!(rule::RouteRule, route_node, schematic) end
+
+# SingleChannelRouting
+# Set tracks when adding with `route!`
+function _update_with_graph!(
+ rule::Paths.SingleChannelRouting,
+ route_node,
+ graph;
+ track=Paths.num_tracks(rule) + 1,
+ kwargs...
+)
+ return Paths.set_track!(rule, route_node.component._path, track)
+end
+
+# Use global position of channel if it is a component in sch
+function _update_with_plan!(rule::Paths.SingleChannelRouting, route_node, sch)
+ isdefined(rule, :global_channel) && return
+ idx = find_nodes(n -> component(n) === rule.channel, sch.graph)
+ isempty(idx) && return # channel is not a component
+ length(idx) > 1 && @error """
+ Channel $(name(rule.channel)) appears multiple times in schematic. \
+ To transform the channel to global coordinates for routing, it must \
+ appear exactly once.
+ """
+ trans = transformation(sch, only(idx))
+ global_node = trans(rule.channel.node)
+ return rule.global_channel = Paths.RouteChannel(Path([global_node]))
+end
diff --git a/src/schematics/schematics.jl b/src/schematics/schematics.jl
index 999cdfa5..c737150d 100644
--- a/src/schematics/schematics.jl
+++ b/src/schematics/schematics.jl
@@ -1041,6 +1041,7 @@ function _plan_route!(sch::Schematic, node_cs, node, hooks_fn=hooks)
comp1.r.waypoints = rot.(comp1.r.waypoints) .+ Ref(comp1.r.p0)
comp1.r.waydirs = comp1.r.waydirs .+ comp1.r.α0
end
+ _update_with_plan!(comp1.r.rule, node, sch)
return addref!(node_cs, comp1)
end
diff --git a/src/utils.jl b/src/utils.jl
index 44f7102b..87a9251d 100644
--- a/src/utils.jl
+++ b/src/utils.jl
@@ -229,7 +229,7 @@ function discretization_grid(
)
t = ts[i - 1]
# Set dt based on distance from chord assuming constant curvature
- if cc >= 1e-9 * oneunit(typeof(cc)) # Update dt if curvature is not near zero
+ if cc >= 100 * 8 * tolerance / t_scale^2 # Update dt if curvature is not near zero
dt = uconvert(NoUnits, sqrt(8 * tolerance / cc) / t_scale)
end
if t + dt >= bnds[2]
diff --git a/test/coverage/Project.toml b/test/coverage/Project.toml
index 3dd44e8d..a1258879 100644
--- a/test/coverage/Project.toml
+++ b/test/coverage/Project.toml
@@ -1,3 +1,4 @@
[deps]
+DeviceLayout = "ebf59a4a-04ec-49d7-8cd4-c9382ceb8e85"
LocalCoverage = "5f6e1e16-694c-5876-87ef-16b5274f298e"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
diff --git a/test/runtests.jl b/test/runtests.jl
index 29734030..bbe7144f 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -31,6 +31,7 @@ include("test_entity.jl")
include("test_intersection.jl")
include("test_shapes.jl")
include("test_routes.jl")
+include("test_channels.jl")
include("test_texts.jl")
include("test_pointinpoly.jl")
include("test_solidmodel.jl")
diff --git a/test/test_bspline.jl b/test/test_bspline.jl
index 612f02b0..ed8f669b 100644
--- a/test/test_bspline.jl
+++ b/test/test_bspline.jl
@@ -202,3 +202,120 @@ end
cps = vcat(pathtopolys(pa2)...)
@test_logs (:warn, r"Maximum error") Paths.bspline_approximation(cps[1].curves[1])
end
+
+@testset "BSpline optimization" begin
+ ## 90 degree turn
+ pa = Path() # auto_speed
+ bspline!(pa, [Point(100μm, 100μm)], 90°, Paths.Trace(1μm); auto_speed=true)
+ pa2 = Path() # auto_curvature, fixed speed
+ bspline!(
+ pa2,
+ [Point(100μm, 100μm)],
+ 90°,
+ Paths.Trace(1μm);
+ endpoints_speed=180μm,
+ auto_curvature=true
+ ) # Close to but not optimal
+ pa3 = Path() # auto_speed, auto_curvature
+ bspline!(
+ pa3,
+ [Point(100μm, 100μm)],
+ 90°,
+ Paths.Trace(1μm);
+ auto_speed=true,
+ auto_curvature=true
+ )
+ pa_turn = Path() # For comparison
+ turn!(pa_turn, 90°, 100μm, Paths.Trace(1μm))
+ # auto_speed is close to a circle (about 140nm max distance)
+ @test Paths.norm(pa_turn[1].seg(π / 4 * 100μm) - pa[1].seg.r(0.5)) < 0.141μm
+ @test abs(pathlength(pa) - 100μm * pi / 2) < 100nm # |-92.695nm| < 100nm
+ # Equal tangents -- symmetric optimization was used
+ @test Paths._symmetric_optimization(pa[1].seg)
+ @test Paths.norm(pa[1].seg.t0) == Paths.norm(pa[1].seg.t1)
+ # Less penalty when auto_speed is used
+ @test Paths._int_dκ2(pa2[1].seg, 100μm) > Paths._int_dκ2(pa3[1].seg, 100μm)
+ # Curvature is zero at endpoints
+ @test Paths.signed_curvature(pa2[1].seg, 0nm) ≈ 0 / nm atol = 1e-9 / nm
+ @test Paths.signed_curvature(pa2[1].seg, pathlength(pa2)) ≈ 0 / nm atol = 1e-9 / nm
+ @test Paths.signed_curvature(pa3[1].seg, 0nm) ≈ 0 / nm atol = 1e-9 / nm
+ @test Paths.signed_curvature(pa3[1].seg, pathlength(pa2)) ≈ 0 / nm atol = 1e-9 / nm
+ # Speed is preserved when only curvature is optimized
+ @test Paths.norm(Paths.Interpolations.gradient(pa2[1].seg.r, 0)[1]) == 180μm
+ @test Paths.norm(Paths.Interpolations.gradient(pa2[1].seg.r, 1)[1]) == 180μm
+
+ ## Scale independence
+ pa_small = Path()
+ bspline!(pa_small, [Point(1μm, 1μm)], 90°, Paths.Trace(1μm); auto_speed=true)
+ @test pa_small[1].seg.t0 ≈ pa[1].seg.t0 / 100
+
+ pa3_small = Path()
+ bspline!(
+ pa3_small,
+ [Point(1μm, 1μm)],
+ 90°,
+ Paths.Trace(1μm);
+ auto_speed=true,
+ auto_curvature=true
+ )
+ @test pa3_small[1].seg.t0 ≈ pa3[1].seg.t0 / 100
+
+ ## 180 degree symmetry
+ pa_snake = Path()
+ bspline!(
+ pa_snake,
+ [Point(100μm, 20μm), Point(200μm, 80μm), Point(300μm, 100μm)],
+ 0°,
+ Paths.Trace(1μm);
+ auto_speed=true
+ )
+ @test Paths._symmetric_optimization(pa_snake[1].seg)
+
+ ## Nonzero curvature
+ @test Paths._last_curvature(pa_turn) == 1 / (100μm)
+ bspline!(pa_turn, [Point(0μm, 200μm)], 180°; auto_speed=true, auto_curvature=true)
+ @test Paths.signed_curvature(pa_turn[2].seg, 0nm) ≈ 1 / (100μm)
+ @test Paths.signed_curvature(pa_turn[2].seg, pathlength(pa_turn[2])) ≈ 1 / (100μm)
+
+ ## Manual curvature
+ pa4 = Path()
+ bspline!(
+ pa4,
+ [Point(0μm, 200μm)],
+ -90°,
+ Paths.Trace(1μm);
+ auto_speed=true,
+ endpoints_curvature=1 / (50μm)
+ )
+ @test Paths.signed_curvature(pa4[1].seg, 0nm) ≈ 1 / (50μm)
+ @test Paths.signed_curvature(pa4[1].seg, pathlength(pa4[1])) ≈ 1 / (50μm)
+
+ ## Multiple waypoints
+ bspline!(
+ pa4,
+ [Point(100μm, 100μm), Point(200μm, 200μm), Point(0μm, 0μm)],
+ -90°,
+ Paths.Trace(1μm);
+ endpoints_speed=160μm,
+ auto_curvature=true
+ )
+ @test Paths.signed_curvature(pa4[2].seg, 0nm) ≈ 1 / (50μm)
+ @test Paths.signed_curvature(pa4[2].seg, pathlength(pa4[2])) ≈ 1 / (50μm)
+ @test Paths.norm(Paths.Interpolations.gradient(pa4[2].seg.r, 0)[1]) ≈ 160μm
+ @test Paths.norm(Paths.Interpolations.gradient(pa4[2].seg.r, 1)[1]) ≈ 160μm
+
+ ## Renders successfully
+ # (discretization doesn't see zero curvature and think it can just skip the whole thing)
+ pa5 = Path()
+ bspline!(
+ pa5,
+ [Point(0μm, 200μm)],
+ 180°,
+ Paths.Trace(1μm);
+ auto_speed=true,
+ auto_curvature=true
+ )
+ c = Cell("test")
+ render!(c, pa5, GDSMeta())
+ @test length(elements(c)[1].p) > 500 # 856, but discretization is subject to change
+end
diff --git a/test/test_channels.jl b/test/test_channels.jl
new file mode 100644
index 00000000..74647e10
--- /dev/null
+++ b/test/test_channels.jl
@@ -0,0 +1,197 @@
+import DeviceLayout.Paths: RouteChannel
+using .SchematicDrivenLayout
+
+function test_single_channel_reversals(r, seg, sty)
+ paths = test_single_channel(r, seg, sty; reverse_channel=false, reverse_paths=false)
+ paths_revch =
+ test_single_channel(r, seg, sty; reverse_channel=true, reverse_paths=false)
+ paths_revp = test_single_channel(r, seg, sty; reverse_channel=false, reverse_paths=true)
+ paths_rev_ch_p =
+ test_single_channel(r, seg, sty; reverse_channel=true, reverse_paths=true)
+ # Segments are approximately the same when channel is reversed
+ for (pa1, pa2) in zip(paths, paths_revch)
+ for (n1, n2) in zip(pa1, pa2)
+ @test p0(n1.seg) ≈ p0(n2.seg) atol = 1nm
+ @test p1(n1.seg) ≈ p1(n2.seg) atol = 1nm
+ @test isapprox_angle(α0(n1.seg), α0(n2.seg), atol=1e-6)
+ @test isapprox_angle(α1(n1.seg), α1(n2.seg), atol=1e-6)
+ @test pathlength(n1.seg) ≈ pathlength(n2.seg) atol = 1nm
+ end
+ end
+ for (pa1, pa2) in zip(paths_revp, paths_rev_ch_p)
+ for (n1, n2) in zip(pa1, pa2)
+ @test p0(n1.seg) ≈ p0(n2.seg) atol = 1nm
+ @test p1(n1.seg) ≈ p1(n2.seg) atol = 1nm
+ @test isapprox_angle(α0(n1.seg), α0(n2.seg), atol=1e-6)
+ @test isapprox_angle(α1(n1.seg), α1(n2.seg), atol=1e-6)
+ @test pathlength(n1.seg) ≈ pathlength(n2.seg) atol = 1nm
+ end
+ end
+ # Segments are approximately reversed when paths are reversed
+ for (pa1, pa2) in zip(paths, paths_revp)
+ for (n1, n2) in zip(pa1, reverse(pa2.nodes))
+ @test p0(n1.seg) ≈ p1(n2.seg) atol = 1nm
+ @test p1(n1.seg) ≈ p0(n2.seg) atol = 1nm
+ @test isapprox_angle(α0(n1.seg), α1(n2.seg) + 180°, atol=1e-6)
+ @test isapprox_angle(α1(n1.seg), α0(n2.seg) + 180°, atol=1e-6)
+ @test pathlength(n1.seg) ≈ pathlength(n2.seg) atol = 1nm
+ # Some reversed paths are visibly different with taper trace and auto_speed (1um length difference)
+ # because the asymmetry causes speed optimization to find a different optimum
+ # depending on which is t0 and which is t1. So we use manual speed
+ # (also because it runs faster and we don't need to test auto further)
+ end
+ end
+ return paths
+end
+
+function test_single_channel(
+ transition_rule,
+ channel_segment,
+ channel_style;
+ reverse_channel=false,
+ reverse_paths=false
+)
+ channel = Path(0.0μm, 0.0μm)
+ if channel_segment == Paths.Straight
+ straight!(channel, 1mm, channel_style)
+ elseif channel_segment == Paths.Turn
+ if channel_style isa Paths.TaperTrace
+ # This path is not simplified, and tapers inside CompoundStyle are not supported for channels
+ return Path[]
+ end
+ turn!(channel, 0.04, 10mm, channel_style)
+ turn!(channel, -0.06, 10mm, channel_style)
+ elseif channel_segment == Paths.CompoundSegment
+ turn!(channel, 90°, 0.25mm, channel_style)
+ turn!(channel, -90°, 0.25mm)
+ turn!(channel, -90°, 0.25mm)
+ turn!(channel, 90°, 0.25mm)
+ simplify!(channel)
+ setstyle!(channel[1], channel_style)
+ elseif channel_segment == Paths.BSpline
+ bspline!(
+ channel,
+ [Point(0.5, 0.5)mm, Point(1.0mm, 0.0μm)],
+ 0°,
+ channel_style,
+ auto_speed=true,
+ auto_curvature=true
+ )
+ end
+
+ reverse_channel && (channel = Path(reverse(reverse.(channel.nodes))))
+
+ # Variety of cases
+ p0s = [
+ Point(100.0, 200.0)μm, # Enter and exit from top
+ Point(50.0, 150)μm, # Enter from top, exit from right
+ Point(-100.0, 100.0)μm, # Enter from upper left, exit from right
+ Point(-100.0, 0.0)μm, # Centered
+ Point(-100.0, -100.0)μm, # Enter from lower left, exit from right
+ Point(50.0, -150)μm, # Enter from bottom, exit from right
+ Point(100.0, -200.0)μm # Enter and exit from bottom
+ ]
+
+ p1s = [
+ Point(900.0, 200.0)μm,
+ Point(1100.0, 150.0)μm,
+ Point(1100.0, 100.0)μm,
+ Point(1100.0, 0.0)μm,
+ Point(1100.0, -100.0)μm,
+ Point(1100.0, -150.0)μm,
+ Point(900.0, -200.0)μm
+ ]
+ reverse_paths && ((p0s, p1s) = (p1s, p0s))
+
+ α0s = fill(reverse_paths ? 180.0° : 0.0°, length(p0s))
+ α1s = copy(α0s)
+
+ paths = [Path(p, α0=α0) for (p, α0) in zip(p0s, α0s)]
+ tracks = reverse_channel ? reverse(eachindex(paths)) : eachindex(paths)
+
+ rule = Paths.SingleChannelRouting(Paths.RouteChannel(channel), transition_rule, 50.0μm)
+ setindex!.(Ref(rule.segment_tracks), tracks, paths)
+ for (pa, p1, α1) in zip(paths, p1s, α1s)
+ route!(pa, p1, α1, rule, Paths.Trace(2μm))
+ end
+ return paths
+end
+
+function test_schematic_single_channel()
+ g = SchematicGraph("test")
+ pa = Path(0.0nm, 0.0nm)
+ straight!(pa, 1mm, Paths.Trace(300μm))
+ ch = Paths.RouteChannel(pa)
+ spacer1 = add_node!(g, Spacer(-1mm, 1mm, name="s1"))
+ spacer2 = add_node!(g, Spacer(1mm, 1mm, name="s2"))
+ spacer3 = add_node!(g, Spacer(-500μm, 500μm, name="s3"))
+ fuse!(g, spacer3 => :p1_west, ch => :p0)
+ rule = Paths.SingleChannelRouting(ch, Paths.StraightAnd90(50μm), 0μm)
+ r1 = route!(
+ g,
+ rule,
+ spacer1 => :p1_west,
+ spacer2 => :p1_east,
+ Paths.CPW(1μm, 1μm),
+ GDSMeta()
+ )
+ r2 = route!(
+ g,
+ rule,
+ spacer1 => :p1_west,
+ spacer2 => :p1_east,
+ Paths.SimpleTrace(1μm),
+ GDSMeta()
+ )
+ sch = plan(g; log_dir=nothing) # channel global coordinate update happens in plan
+ pa1 = SchematicDrivenLayout.path(r1.component)
+ pa2 = SchematicDrivenLayout.path(r2.component)
+ # Track assignment means pa1 track is 100um above pa2 track
+ @test pathlength(pa1) ≈ 2 * (350μm + pi * 50μm + 0.4mm) + 1mm atol = 1nm
+ @test pathlength(pa2) ≈ 2 * (450μm + pi * 50μm + 0.4mm) + 1mm atol = 1nm
+ c = Cell("test", nm)
+ return render!(c, pa1, GDSMeta()) # No error
+end
+
+@testset "Channels" begin
+ ### Single-channel integration tests
+ ## Geometry-level routing
+ transition_rules = [
+ Paths.StraightAnd90(min_bend_radius=25μm)
+ Paths.BSplineRouting(endpoints_speed=150μm, auto_curvature=true)
+ ]
+ channel_segments = [Paths.Straight, Paths.Turn, Paths.BSpline, Paths.CompoundSegment]
+ channel_styles = [Paths.Trace(100μm), Paths.TaperTrace(100μm, 50μm)]
+ # StraightAnd90 only works with straight channel and simple trace
+ @testset "Straight" begin
+ rule = transition_rules[1]
+ paths = test_single_channel_reversals(rule, channel_segments[1], channel_styles[1])
+ @test isempty(Intersect.intersections(paths...))
+ c = Cell("test", nm)
+ render!(c, paths[1], GDSMeta()) # No error
+ end
+ rule = transition_rules[2] # BSpline rule for all-angle transitions
+ for segtype in channel_segments[2:end]
+ @testset "$segtype channel" begin
+ for sty in channel_styles
+ test_single_channel_reversals(rule, segtype, sty)
+ end
+ end
+ end
+ # Channel too short
+ pa = Path(0.0nm, 0.0nm)
+ straight!(pa, 100nm, Paths.Trace(0.1mm))
+ ch = RouteChannel(pa)
+ pa2 = Path(-0.1mm, 0mm)
+ rule = Paths.SingleChannelRouting(
+ ch,
+ Paths.BSplineRouting(auto_curvature=true, auto_speed=true),
+ 50μm
+ )
+ Paths.set_track!(rule, pa2, 1)
+ route!(pa2, Point(0.1mm, 0.1mm), 0, rule, Paths.CPW(2nm, 2nm))
+ @test length(pa2) == 1 # Channel segment turned into waypoint
+
+ ## Schematic-level routing
+ test_schematic_single_channel()
+end
diff --git a/test/test_render.jl b/test/test_render.jl
index 234694ea..d39f836e 100644
--- a/test/test_render.jl
+++ b/test/test_render.jl
@@ -209,6 +209,9 @@ end
pa = Path{Float64}()
straight!(pa, 20.0, Paths.Trace(x -> 2.0 * x))
render!(c, pa)
+ revsty = reverse(pa[1]).sty
+ @test Paths.width(revsty, 0) == Paths.trace(pa[1].sty, 20)
+ @test Paths.extent(revsty)(20) == 0.5 * Paths.width(pa[1].sty)(0)
end
@testset "Straight, SimpleCPW" begin
@@ -228,6 +231,9 @@ end
p(20.082731241720513, 1.7128648145206729),
p(0.5197792270443984, -2.4453690018345142)
]
+ revsty = reverse(pa[1]).sty
+ @test Paths.trace(revsty, 0) == Paths.trace(pa[1].sty, 20)
+ @test Paths.trace(revsty, 20) == Paths.trace(pa[1].sty, 0)
c = Cell("main", pm2μm)
pa = Path(μm2μm, α0=12°)
@@ -249,9 +255,16 @@ end
] * 10^6
end
- # @testset "Straight, GeneralCPW" begin
- #
- # end
+ @testset "Straight, GeneralCPW" begin
+ c = Cell{Float64}("main")
+ pa = Path(NoUnits, α0=12°)
+ straight!(pa, 20.0, Paths.CPW(x -> 2 * x, x -> 3 * x))
+ revsty = reverse(pa[1]).sty
+ @test Paths.trace(revsty, 0) == Paths.trace(pa[1].sty, 20)
+ @test Paths.trace(revsty, 20) == Paths.trace(pa[1].sty, 0)
+ @test Paths.extent(revsty)(5) ==
+ Paths.gap(pa[1].sty)(15) + Paths.trace(pa[1].sty)(15) / 2
+ end
@testset "Turn, SimpleTrace" begin
c = Cell{Float64}("main")
@@ -424,6 +437,9 @@ end
p(50000.0nm, -4000.0nm),
p(0.0nm, -5000.0nm)
]
+ revsty = reverse(pa[1]).sty
+ @test Paths.trace(revsty, 0.0μm) == Paths.trace(pa[1].sty, 50.0μm)
+ @test Paths.trace(revsty, 50.0μm) == Paths.trace(pa[1].sty, 0.0μm)
@test_throws "length" split(Paths.TaperCPW(10.0μm, 6.0μm, 8.0μm, 2.0μm), 10μm)
@@ -447,6 +463,7 @@ end
pa = Path(μm)
turn!(pa, π / 2, 20μm, Paths.TaperTrace(10μm, 20μm))
render!(c, pa, GDSMeta(0))
+ @test Paths.trace(pa[1].sty, 0μm) == 10μm
@test (elements(c)[1]).p[1] ≈ p(0.0nm, -5000.0nm)
@test (elements(c)[1]).p[end] ≈ p(0.0nm, 5000.0nm)
@@ -556,6 +573,10 @@ end
straight!(pa, 20μm, Paths.Trace(15μm))
straight!(pa, 20μm, Paths.Trace(20μm))
simplify!(pa)
+ revsty = reverse(pa[1]).sty
+ @test Paths.trace(revsty, 55μm) == Paths.trace(pa[1].sty, 5μm)
+ @test Paths.trace(revsty)(5μm) == Paths.trace(pa[1].sty)(55μm)
+ @test Paths.extent(revsty)(5μm) == 0.5 * Paths.width(pa[1].sty)(55μm)
pa2 = split(pa[1], 20μm)
@test length(pa2) == 2
@@ -878,6 +899,42 @@ end
@test length(c.elements) == 15 # 3 rectangles + 10 traces + 2 terminations
end
+ @testset "OffsetSegments" begin
+ pa = Path(μm; α0=90°)
+ straight!(pa, 10μm, Paths.Trace(2.0μm))
+ pa1 = Path(
+ [Paths.Node(Paths.offset(pa[1].seg, 5000nm), pa[1].sty)],
+ metadata=GDSMeta()
+ )
+ @test p0(pa1) == Point(-5.0, 0.0)μm
+ c_dec = Cell("decoration", nm)
+ render!(c_dec, Rectangle(2μm, 2μm), GDSMeta(1))
+ attach!(pa1, sref(c_dec), 5μm)
+ cs1 = CoordinateSystem("test", nm)
+ pathref = sref(pa1, Point(5μm, 5μm), rot=pi / 2, xrefl=true)
+ addref!(cs1, pathref)
+ flatten!(cs1)
+ c1 = Cell(cs1)
+ c_path = Cell("pathonly", nm)
+ render!(c_path, pa1, GDSMeta())
+ @test bounds(c1) ≈ bounds(transformation(pathref)(c_path)) atol = 1e-6nm
+ # GeneralOffset
+ pa2 = Path(
+ [Paths.Node(Paths.offset(pa[1].seg, x -> 2μm + x), pa[1].sty)],
+ metadata=GDSMeta()
+ )
+ @test p0(pa2) == Point(-2.0, 0.0)μm
+ attach!(pa2, sref(c_dec), 10μm, location=-1)
+ cs2 = CoordinateSystem("test", nm)
+ pathref = sref(pa2, Point(5μm, 5μm), rot=pi / 2, xrefl=true)
+ addref!(cs2, pa2, Point(5μm, 5μm), rot=pi / 2, xrefl=true)
+ flatten!(cs2)
+ c2 = Cell(cs2)
+ c_path = Cell("pathonly", nm)
+ render!(c_path, pa2, GDSMeta())
+ @test bounds(c2) ≈ bounds(transformation(pathref)(c_path)) atol = 1e-6nm
+ end
+
@testset "ClippedPolygons" begin
r1 = centered(Rectangle(12μm, 12μm))
r2 = centered(Rectangle(4μm, 4μm))
diff --git a/test/test_routes.jl b/test/test_routes.jl
index d92750d6..e21eba8f 100644
--- a/test/test_routes.jl
+++ b/test/test_routes.jl
@@ -118,14 +118,7 @@
@test α1(pa) == α_end
# Use StraightAnd45 with incompatible waypoints
- r4 = Route(
- Paths.StraightAnd45(min_bend_radius=20, max_bend_radius=20),
- p_start,
- p_end,
- α_start,
- α_end,
- waypoints=waypoints
- )
+ r4 = Route(Paths.StraightAnd45(20), p_start, p_end, α_start, α_end, waypoints=waypoints)
@test_throws ErrorException (pa = Path(r4, sty))
# Use StraightAnd45