From d9f25976a485f4f2f01859239b746eba08178897 Mon Sep 17 00:00:00 2001 From: Greg Peairs Date: Mon, 15 Sep 2025 12:17:27 -0700 Subject: [PATCH 01/32] Add channel autorouter --- src/paths/channel_autorouter.jl | 1073 +++++++++++++++++++++++++++++++ test/test_channels.jl | 111 ++++ 2 files changed, 1184 insertions(+) create mode 100644 src/paths/channel_autorouter.jl diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl new file mode 100644 index 000000000..778446601 --- /dev/null +++ b/src/paths/channel_autorouter.jl @@ -0,0 +1,1073 @@ +# Original: Hashimoto and Stevens https://cs.baylor.edu/~maurer/CSI5346/originalCR.pdf +# VCG, HCG/Zone representation, doglegs, merging: Yoshimura and Kuh https://my.ece.utah.edu/~kalla/phy_des/yk.pdf +# Crossing-aware: Condrat, Kalla, Blair https://my.ece.utah.edu/~kalla/papers/condrat_crossing-aware_channel_routing_for_photonic_waveguides.pdf + +import Graphs: + SimpleGraph, + SimpleDiGraph, + nv, + add_edge!, + rem_edge!, + adjacency_matrix, + edges, + inneighbors, + neighbors, + outneighbors, + dag_longest_path, + maximal_cliques, + yen_k_shortest_paths +import LinearAlgebra: norm +import BipartiteMatching + +# TrackWireSegment = (net, lengthwise channel, start vertex, end vertex) +# vertex is the channel index or, if the start/end is a pin, the graph index for that pin +struct TrackWireSegment + net_index::Int + running_channel::Int + start_vertex::Int + stop_vertex::Int +end + +bounding_channels(ws::TrackWireSegment) = (ws.start_vertex, ws.stop_vertex) +net_index(ws::TrackWireSegment) = ws.net_index +running_channel(ws::TrackWireSegment) = ws.running_channel + +const Track = Vector{TrackWireSegment} +const NetWire = Vector{TrackWireSegment} +# pathlength 1, pathlength 2, dir1, dir2, intersection point +const IntersectionInfo{T} = Tuple{T, T, typeof(1.0°), typeof(1.0°), Point{T}} + +""" + ChannelRouter{T <: Coordinate} + +A simple autorouter where wires can run on horizontal and vertical channels. + +The router will attempt to connect pairs of pins, each with coordinates and a direction. +The indices of connected pins are specified by `nets`. + +The router is initialized with a number of vertical and horizontal "channels", characterized +by a width and the coordinate of their center line. Each channel will be divided into some +number of tracks as necessary to route all nets. + +Routing proceeds in two steps. The first is "channel assignment", which proceeds net by net, +choosing which channels the net's wire passes through on its way from one pin to the other. The second step +is "track assigment", which proceeds channel by channel. Wire segments are assigned to +tracks within the channel such that wire segments in the same track do not overlap. +""" +struct ChannelRouter{T <: Coordinate} + channel_graph::SimpleGraph{Int} # Channels/pins are vertices, intersections are edges + net_pins::Vector{Tuple{Int, Int}} # Pairs of pins (indices in `pins`) + net_wires::Vector{NetWire} # Wire segments connecting pins for each net + pins::Vector{PointHook{T}} # Position and orientation of pins + channels::Vector{RouteChannel{T}} # List of channels + # channel_capacities::Vector{Int} # Maximum number of tracks per channel, not used + # For each edge, information to find the intersection point + channel_intersections::Dict{Tuple{Int, Int}, IntersectionInfo{T}} + # Vector of vectors of all segments in each channel + channel_segments::Vector{Vector{TrackWireSegment}} + # Vector of vectors of all tracks in each channel [where each track is a vector of wire segments] + channel_tracks::Vector{Vector{Track}} + # Waypoints for each segment (used for visualizing router state) + segment_waypoints::Dict{TrackWireSegment, PointHook{T}} + net_paths::Vector{Path{T}} # [Internals] Persistent path objects to populate after routing +end + +"""" + ChannelRouter( + nets::Vector{Tuple{Int, Int}}, + pin_hooks::Vector{<:Hook}, + channels::Vector{<:RouteChannel} + ) +""" +function ChannelRouter( + nets, + pin_hooks::Vector{<:Hook}, + channels::Vector{<:RouteChannel} +) + T = promote_type(coordinatetype(pin_hooks), coordinatetype(channels)) + net_wires = [NetWire() for i in eachindex(nets)] + channel_segments = [TrackWireSegment[] for i in eachindex(channels)] + channel_tracks = [Track[] for i in eachindex(channels)] + segment_waypoints = Dict{TrackWireSegment, PointHook{T}}() + pins = [PointHook{T}(pin.p, pin.in_direction + 180°) for pin in pin_hooks] + # Build channel graphs with full paths to avoid compound operations + channel_paths = [ch.path for ch in channels] + channel_graph, ixns = build_channel_graph(pins, channel_paths, T) + return ChannelRouter{T}( + channel_graph, + nets, + net_wires, + pins, + channels, + ixns, + channel_segments, + channel_tracks, + segment_waypoints, + [Path{T}() for net in nets] + ) +end + +function ChannelRouter(channels::Vector{RouteChannel{T}}) where {T} + channel_segments = [TrackWireSegment[] for i in eachindex(channels)] + channel_tracks = [Track[] for i in eachindex(channels)] + segment_waypoints = Dict{TrackWireSegment, PointHook{T}}() + return ChannelRouter{T}( + SimpleGraph(), + Tuple{Int,Int}[], + NetWire[], + PointHook{T}[], + channels, + Dict{Tuple{Int, Int}, IntersectionInfo{T}}(), + channel_segments, + channel_tracks, + segment_waypoints, + Path{T}[] + ) +end + +Base.broadcastable(x::ChannelRouter) = Ref(x) +num_channels(ar::ChannelRouter) = length(ar.channels) +num_nets(ar::ChannelRouter) = length(ar.net_pins) +num_pins(ar::ChannelRouter) = length(ar.pins) + +channel_graph(ar::ChannelRouter) = ar.channel_graph +net_pins(ar::ChannelRouter, net) = ar.net_pins[net] +net_wire(ar::ChannelRouter, net) = ar.net_wires[net] +pin_coordinates(ar::ChannelRouter, pin) = ar.pins[pin].p +pin_direction(ar::ChannelRouter, pin) = ar.pins[pin].in_direction +channel_coordinates(ar::ChannelRouter, channel, s) = ar.channels[channel].node.seg(s) +function channel_direction(ar::ChannelRouter, channel, s) + is_pin(ar, channel) && return pin_direction(ar, graphidx_to_pin(ar, channel)) + return direction(ar.channels[channel].node.seg, s) +end +function channel_width(ar::ChannelRouter{T}, channel, s) where {T} + # Intersecting channel is zero where a wire segment hits a pin + is_pin(ar, channel) && return zero(T) + return width(ar.channels[channel].node.sty, s) +end +channel_segments(ar::ChannelRouter, channel) = ar.channel_segments[channel] +channel_tracks(ar::ChannelRouter, channel) = ar.channel_tracks[channel] +num_tracks(ar::ChannelRouter, channel) = length(ar.channel_tracks[channel]) +pin_to_graphidx(ar::ChannelRouter, p::Int) = p + num_channels(ar) +graphidx_to_pin(ar::ChannelRouter, graphidx::Int) = graphidx - num_channels(ar) +is_pin(ar::ChannelRouter, graphidx) = graphidx > num_channels(ar) +adjoining_channel(ar::ChannelRouter, pin) = + neighbors(channel_graph(ar), pin_to_graphidx(ar, pin))[1] +channel_intersection(ar, s1, s2) = ar.channel_intersections[_swap(s1, s2)] +function pathlength_at_intersection(ar::ChannelRouter{T}, + running_channel, + intersecting_channel) where {T} + # Intersecting channel is zero where a wire segment hits a pin + if iszero(running_channel) || iszero(intersecting_channel) + return zero(T) + end + ixn_info = channel_intersection(ar, running_channel, intersecting_channel) + running_channel < intersecting_channel && return ixn_info[1] + return ixn_info[2] +end + +function direction_at_intersection(ar::ChannelRouter, + running_channel, + intersecting_channel) + # Intersecting channel is zero where a wire segment hits a pin + if iszero(running_channel) || iszero(intersecting_channel) + return 0.0° + end + ixn_info = channel_intersection(ar, running_channel, intersecting_channel) + running_channel < intersecting_channel && return ixn_info[3] + return ixn_info[4] +end + +function width_at_intersection(ar::ChannelRouter{T}, + running_channel, + intersecting_channel) where {T} + + ixn_info = channel_intersection(ar, running_channel, intersecting_channel) + running_channel < intersecting_channel && return channel_width(ar, running_channel, ixn_info[1]) + return channel_width(ar, running_channel, ixn_info[2]) +end + +segment_waypoint(ar::ChannelRouter, ws::TrackWireSegment) = ar.segment_waypoints[ws] +_swap(x, y) = (y > x ? (x, y) : (y, x)) + +pathlength_from_start(channel, node, s) = pathlength(channel[1:node-1]) + s + +# Build graph with pins/channels as vertices and intersections as edges +function build_channel_graph(pins, channels, T) + g = SimpleGraph(length(channels) + length(pins)) + intersection_dict = Dict{Tuple{Int, Int}, IntersectionInfo{T}}() + + # Create segments extending from pins + bbox = bounds(bounds(channels), bounds(Polygon(DeviceLayout.getp.(pins)))) + ray_length = max(DeviceLayout.width(bbox), DeviceLayout.height(bbox))*sqrt(2) + pin_rays = Path{T}[] + for pin in pins + path = Path{T}(pin.p, pin.in_direction) + straight!(path, ray_length, Paths.NoRender()) + push!(pin_rays, path) + end + + # Add edges for intersections between channels + intersections = DeviceLayout.Intersect.prepared_intersections( + [channels..., pin_rays...]) + pin_ixns = Dict{Int, Tuple{Int, IntersectionInfo{T}}}() + for ixn in intersections + location_1, location_2, p = ixn + v1_idx, node1_idx, s1 = location_1 + v2_idx, node2_idx, s2 = location_2 + if v1_idx >= v2_idx + # `prepared_intersections` guarantees v2 >= v1 + @assert v1_idx == v2_idx + # We will also ignore self-intersecting channels v2 == v1 + @info "Ignoring self-intersection of channel $v1_idx" + continue + elseif v1_idx > length(channels) && v2_idx > length(channels) + # Both are pins, ignore intersection + continue + elseif v2_idx > length(channels) # v2 is a pin + dir1 = direction(channels[v1_idx][node1_idx].seg, s1) + dir2 = pins[v2_idx - length(channels)].in_direction + s1 = pathlength_from_start(channels[v1_idx], node1_idx, s1) + # Record intersection if it's the closest so far + ixn_info = (s1, s2, dir1, dir2, p) + _, old_ixn_info = get(pin_ixns, v2_idx, + (v1_idx, ixn_info)) + old_distance = old_ixn_info[2] + if s2 <= old_distance + pin_ixns[v2_idx] = (v1_idx, ixn_info) + end + else # record intersection as edge in channel graph, with info in dict + haskey(intersection_dict, (v1_idx, v2_idx)) && + error("Spaces $v1_idx and $v2_idx have multiple intersections") + dir1 = direction(channels[v1_idx][node1_idx].seg, s1) + dir2 = direction(channels[v2_idx][node2_idx].seg, s2) + s1 = pathlength_from_start(channels[v1_idx], node1_idx, s1) + s2 = pathlength_from_start(channels[v2_idx], node2_idx, s2) + + add_edge!(g, v1_idx, v2_idx) + intersection_dict[(v1_idx, v2_idx)] = # All records have v1 < v2 + (s1, s2, dir1, dir2, p) + end + end + # Add min distance edge for each pin + for pin_idx in (length(channels)+1):(length(channels) + length(pins)) + orig_idx = pin_idx - length(channels) + !haskey(pin_ixns, pin_idx) && error("The ray from pin $(orig_idx) ($(pins[orig_idx])) does not intersect any channel") + channel_idx, ixn_info = pin_ixns[pin_idx] + add_edge!(g, (channel_idx, pin_idx)) + intersection_dict[(channel_idx, pin_idx)] = ixn_info + end + + return g, intersection_dict +end + +""" + print_segments(ar::ChannelRouter, net) + +Print the information for the wire segments in `net` in a human-readable format. +""" +function print_segments(ar::ChannelRouter, net) + for (i, ws) in pairs(net_wire(ar, net)) + s1, s2 = bounding_channels(ws) + channel_names = [ + is_pin(ar, s1) ? "Pin $(graphidx_to_pin(ar, s1))" : "Channel $s1", + is_pin(ar, s2) ? "Pin $(graphidx_to_pin(ar, s2))" : "Channel $s2" + ] + println( + """ + Segment $i: + Runs along Channel $(running_channel(ws)), Track $(segment_track(ar, ws)) + From $(channel_names[1]) to $(channel_names[2]) + Through waypoint $(segment_waypoint(ar, ws)[1]) at $(segment_waypoint(ar, ws)[2]) + """ + ) + end +end + +""" + segment_track(ar::ChannelRouter, ws::TrackWireSegment) + +The track index of `ws`, or `nothing` if no track has been assigned. +""" +function segment_track(ar::ChannelRouter, ws::TrackWireSegment) + channel_idx = running_channel(ws) + tracks = channel_tracks(ar, channel_idx) + track_idx = findfirst((c) -> ws in c, tracks) + return track_idx +end + +""" + segment_midpoint(ar::ChannelRouter, ws::TrackWireSegment) + +The midpoint of the segment `ws` between `bounding_channels(ws)`. + +If `ws` has been assigned a track, uses the segment along that track. +""" +function segment_midpoint(ar::ChannelRouter{T}, ws::TrackWireSegment) where {T} + channel_idx = running_channel(ws) + s0, s1 = interval(ar, ws) + s = (s0+s1)/2 + if is_pin(ar, channel_idx) + dir = pin_direction(ar, graphidx_to_pin(ar, channel_idx)) + return pin_coordinates(ar, graphidx_to_pin(ar, channel_idx)) + + s * Point(cos(dir), sin(dir)) + end + channel_midpoint = channel_coordinates(ar, channel_idx, s) + + offset_distance = segment_offset(ar, ws) + + channel_dir = channel_direction(ar, channel_idx, s) + return channel_midpoint + offset_distance * Point(-sin(channel_dir), cos(channel_dir)) +end + +""" + segment_direction(ar::ChannelRouter, ws::TrackWireSegment) + +The angle with the x-axis made by segment `ws` directed along its wire toward its end pin. +""" +function segment_direction(ar::ChannelRouter, ws::TrackWireSegment, s) + off = segment_offset(ar, ws, s) + seg = Paths.offset(ar.channels[running_channel(ws)].seg, off) + return direction(seg, s) +end + +function segment_mid_direction(ar::ChannelRouter, ws::TrackWireSegment) + s0, s1 = interval(ar, ws) + return segment_direction(ar, ws, (s0+s1)/2) +end + +function segment_offset(ar::ChannelRouter{T}, ws::TrackWireSegment, s...) where {T} + is_pin(ar, running_channel(ws)) && return zero(T) + c = segment_track(ar, ws) + isnothing(c) && return zero(T) + return track_offset(ar, running_channel(ws), c, s...) +end + +""" + track_offset(ar::ChannelRouter, channel_idx, track_idx, s) + +The offset of the centerline of track `track_idx` in channel `channel_idx`, +measured at pathlength `s` in the channel. +""" +function track_offset(ar::ChannelRouter{T}, channel_idx, track_idx, s...) where {T} + n_tracks = length(channel_tracks(ar, channel_idx)) + w = Paths.width(ar.channels[channel_idx].sty, zero(T)) + spacing = w / (n_tracks + 1) + return spacing * (track_idx - (1 + n_tracks) / 2) +end + +""" + interval(ar::ChannelRouter, ws::TrackWireSegment) + +A tuple `(start, stop)` of approximate channel pathlengths at which `ws` starts and stops. + +If tracks have been assigned to the previous or next segments, then the track offset is +taken into account. Otherwise, the start and stop are at the centre line of the intersecting channel. + +The interval is always a tuple with the lower bound as the first element. +""" +function interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=true) + start_channel, stop_channel = bounding_channels(ws) + channel_idx = running_channel(ws) + + start_channel, stop_channel = bounding_channels(ws) + s1 = pathlength_at_intersection(ar, channel_idx, start_channel) + s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) + (!use_track || is_pin(ar, channel_idx)) && return _swap(s1, s2) + # Could just do that for all cases + # But if we want to break ties we would use offsets from previous/next segments, like: + # return _swap(s1 + segment_offset(ar, prev(ws)), s2 - segment_offset(ar, next(ws))) + # But sign needs to take into account relative orientations of channels + pt, nt = prev_next_tendency(ar, ws; use_segment_direction=false) + s_start = pathlength_at_intersection(ar, start_channel, channel_idx) + s_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) + return _swap(s1 + pt*segment_offset(ar, prev(ar, ws), s_start), + s2 - nt*segment_offset(ar, next(ar, ws), s_stop)) +end + +""" + next(ar::ChannelRouter, ws::TrackWireSegment) + +The wire segment after `ws`, with the wire directed from the source to the destination pin. +""" +function next(ar::ChannelRouter, ws::TrackWireSegment) + net_idx = net_index(ws) + segs = net_wire(ar, net_idx) + idx = findfirst(isequal(ws), segs) + if idx == length(segs) + final_pin_idx = pin_to_graphidx(ar, last(net_pins(ar, net_idx))) + return TrackWireSegment(net_idx, + final_pin_idx, + running_channel(ws), + 0) + end + return segs[idx + 1] +end + +""" + prev(ar::ChannelRouter, ws::TrackWireSegment) + +The wire segment before `ws`, with the wire directed from the source to the destination pin. +""" +function prev(ar::ChannelRouter, ws::TrackWireSegment) + net_idx = net_index(ws) + segs = net_wire(ar, net_idx) + idx = findfirst(isequal(ws), segs) + if idx == 1 + first_pin_idx = pin_to_graphidx(ar, first(net_pins(ar, net_idx))) + return TrackWireSegment(net_idx, + first_pin_idx, + 0, + running_channel(ws)) + end + return segs[idx - 1] +end + +""" + shortest_path_between_pins(ar::ChannelRouter, pin_1::Int, pin_2::Int) + +A shortest path in the router's channel graph from `pin_1` to `pin_2`. + +Distance is not physical distance but graph distance (the number of edges in the path). + +In the channel graph, each channel is a vertex, and there is an edge between each intersecting +pair of channels. Each pin is also a vertex, with an edge only to its adjoining channel. A path +is a list of vertex indices `path::Vector{Int}`. +""" +function shortest_path_between_pins(ar::ChannelRouter, p0::Int, p1::Int) + ys = yen_k_shortest_paths( + channel_graph(ar), + pin_to_graphidx(ar, p0), + pin_to_graphidx(ar, p1) + ) + return ys.paths[1] +end + +""" + assign_channels!(ar::ChannelRouter) + +Performs channel assignment for `ar`. + +Currently just finds a "shortest path" between pins, where +distance is not physical distance but graph distance (the number of edges in the path). +In other words, each net takes a path that changes channels a minimal number of times. +Does not currently take congestion, crossings, or channel capacity into account. +""" +function assign_channels!( + ar::ChannelRouter; + net_indices=eachindex(ar.net_pins), + fixed_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}() +) + for (idx_net, net) in zip(net_indices, ar.net_pins[net_indices]) + p0, p1 = net + path = if idx_net in keys(fixed_paths) + [ + pin_to_graphidx(ar, p0) + fixed_paths[idx_net] + pin_to_graphidx(ar, p1) + ] + else + shortest_path_between_pins(ar, p0, p1) + end + ixns = [(path[i], path[i + 1]) for i = 1:(length(path) - 1)] + segs = [(ixns[i], ixns[i + 1]) for i = 1:(length(ixns) - 1)] + for (channel, seg) in zip(path[2:(end - 1)], segs) + ws = TrackWireSegment(idx_net, channel, first(seg[1]), last(seg[2])) + push!(net_wire(ar, idx_net), ws) + push!(channel_segments(ar, channel), ws) + end + end +end + +""" + assign_tracks!(ar::ChannelRouter) + +Performs track assigment for `ar`. +""" +function assign_tracks!(ar::ChannelRouter{T}) where {T} + # Order of channels will change results + # Generally you want early channels to be those with more pins adjoining them + # Or fewer non-pinned segments + # Because those channels will inform constraints on later channels + # For now the user can worry about that + for channel = 1:num_channels(ar) + assign_tracks_matching!(ar, channel) + end +end + +function merge_in_vcg!(vcg, v1, v2) + # Children of one become children of the other + for child in outneighbors(vcg, v1) # Directed neighbor v1 -> child + add_edge!(vcg, v2, child) + end + for child in outneighbors(vcg, v2) # Directed neighbor v2 -> child + add_edge!(vcg, v1, child) + end + # Parents of one become parents of the other + for parent in inneighbors(vcg, v1) # parent -> v1 + add_edge!(vcg, parent, v2) + end + for parent in inneighbors(vcg, v2) + add_edge!(vcg, parent, v1) + end +end + +function assign_tracks_matching!(ar, channel) + # Yoshimura and Kuh Algorithm #2 + # We will not check whether there are cycles in the vertical constraint graph, just assume + vcg, zone_ig = channel_problem_graphs(ar, channel) + zones = maximal_cliques(zone_ig) + isempty(zones) && return + # Need to sort zones left to right (by their minimal element) + sort!(zones, by=minimum) + # Same with wire segments, to follow same indexing as graphs + wiresegs_ascending = sort(channel_segments(ar, channel), by=(ws) -> interval(ar, ws)) + L = Set{Int}() + active = Set(zones[1]) + merged_groups = Dict{Int, Vector{Int}}() + merged_into = Dict{Int, Int}() # merged_into[x] = y means y was merged into x with y > x + merging_graph = SimpleGraph(length(wiresegs_ascending)) + matching = Dict{Int, Int}() + R = Set{Int}() + + for (zone, nextzone) in zip(zones, [zones[2:end]..., Int[]]) # Extra empty zone at end + # Merge nets terminating in current zone to left side + if length(zones) > 1 + for v in active # v was in nextzone last round + if !(v in nextzone) # v terminates in current zone + pop!(active, v) # no longer active + push!(L, v) # add to left side + if haskey(matching, v) + # Matched in previous round, so update VCG + merge_in_vcg!(vcg, matching[v], v) + # Record merge + merged_into[matching[v]] = v + group = get!(merged_groups, matching[v], Int[matching[v]]) + push!(group, v) + merged_groups[v] = group + # Replace match with v as representative of merged group in L + pop!(L, matching[v]) # match must have been in L + end + v in R && pop!(R, v) + end + end + end + # Add nets starting in next zone to right side + for v in nextzone + if !(v in active) + push!(R, v) + push!(active, v) + end + end + # Remove all edges in merging graph, start fresh this iteration + for edge in edges(merging_graph) + rem_edge!(merging_graph, edge) + end + # Add edges between left and right when they can be merged + for l in L + # Only use rightmost in any merged group + haskey(merged_into, l) && continue + for r in R + if !segments_overlap(ar, wiresegs_ascending[l], wiresegs_ascending[r]) + mergeable = isempty(yen_k_shortest_paths(vcg, l, r).paths) && isempty(yen_k_shortest_paths(vcg, r, l).paths) + mergeable && add_edge!(merging_graph, l, r) + end + end + end + # Find max cardinality valid matching, removing edges as necessary + matching = best_matching!(merging_graph, vcg)[1] # Just the dict, not the indicator + end + # Assign merged groups to tracks according to VCG + tracks = channel_tracks(ar, channel) + # At the end of this process, segments are merged into layers in the VCG + # So the longest directed path gives a representative of each merged group + high_to_low = dag_longest_path(vcg) # If vcg was acyclic to begin with, it is still acyclic + num_tracks = length(high_to_low) + for v in 1:nv(vcg) + if !haskey(merged_groups, v) + merged_groups[v] = [v] + end + end + for v in reverse(high_to_low) + # Create a track with `v` and all others merged with it + push!(tracks, wiresegs_ascending[merged_groups[v]]) + end +end + +function segments_overlap(ar, seg1, seg2) + low1, high1 = interval(ar, seg1) + low2, high2 = interval(ar, seg2) + if low1 <= low2 # segments are in ascending order + return low2 < high1 # no overlap for '==' means knock-knees are OK + else # descending order + return low1 < high2 + end +end + +function best_matching!(merging_graph, vcg) + # Collect set of edges to remove + to_remove = Set{Tuple{Int, Int}}() + # Create a temporary copy to help find problematic edges + edge_selection_graph = copy(merging_graph) + working_vcg = copy(vcg) + # Collect "deleted" vertices in edge_selection_graph + # Vertices aren't labelled, just indexed, so we don't actually delete them + ignored = Set{Int}() + # While working graphs have vertices, keep collecting edges to remove from merging graph + while length(ignored) < nv(edge_selection_graph) + # 1. Find nodes with no VCG ancestors, remove edges between them + # 2. If there are nodes with no edges in edge_selection_graph, remove them from working graphs and go back to 1 + # 3. Now the node with the fewest edges has at least one edge + # That edge corresponds to a merging that would cause a problem in the VCG + # so then we mark it for removal from the merging graph + # and remove it from our working graphs + min_neighbors = nv(edge_selection_graph) + v_min_neighbors = 0 + orphans = true + while orphans + min_neighbors = nv(edge_selection_graph) + v_min_neighbors = 0 + orphans = false + # Remove edges between vertices with no ancestors + # Then they will not be selected for removal from `merging_graph` + no_ancestors = Int[] + for v in 1:nv(edge_selection_graph) + v in ignored && continue + if isempty(inneighbors(working_vcg, v)) || all([w in ignored for w in inneighbors(working_vcg, v)]) + # v has no surviving ancestors, remove edges to other such vertices + for w in no_ancestors + rem_edge!(edge_selection_graph, v, w) + end + push!(no_ancestors, v) + end + end + # Find nodes with minimum number of edges + for v in 1:nv(edge_selection_graph) + v in ignored && continue + nbs = neighbors(edge_selection_graph, v) + if length(nbs) < min_neighbors + min_neighbors = length(nbs) + v_min_neighbors = v + end + # If any nodes have no edges, remove them and go back + if isempty(nbs) + min_neighbors = 0 + orphans = true + push!(ignored, v) + # No need to remove edges from edge_selection_graph because it doesn't have any + # "Removing" v from working VCG means merging edges betwen its parents and children + for parent in inneighbors(working_vcg, v) + for child in outneighbors(working_vcg, v) + add_edge!(working_vcg, parent, child) + end + end + end + end + end + # Remove a node with fewest edges, remove and collect those edges + if !iszero(v_min_neighbors) && min_neighbors > 0 + for nb in copy(neighbors(edge_selection_graph, v_min_neighbors)) # copy bc neighbors is changing in the loop + rem_edge!(edge_selection_graph, v_min_neighbors, nb) + push!(to_remove, (v_min_neighbors, nb)) + end + push!(ignored, v_min_neighbors) + # "Removing" v from working VCG means merging edges betwen its parents and children + for parent in inneighbors(working_vcg, v_min_neighbors) + for child in outneighbors(working_vcg, v_min_neighbors) + add_edge!(working_vcg, parent, child) + end + end + end + end + # Remove marked edges + for edge in to_remove + rem_edge!(merging_graph, edge...) + end + # Any matching is feasible now that we've removed marked edges + return BipartiteMatching.findmaxcardinalitybipartitematching( + BitMatrix(adjacency_matrix(merging_graph)) + ) +end + +function merge_segments!(zone_ig, vcg, ws1, ws2) + merge_vertices!(zone_ig, [ws1, ws2]) + return merge_vertices(vcg, [ws1, ws2]) # No in-place for directed graph +end + +function against_channel(ar, wireseg) + channel_idx = running_channel(wireseg) + start_channel, stop_channel = bounding_channels(wireseg) + s1 = pathlength_at_intersection(ar, channel_idx, start_channel) + s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) + return s1 > s2 +end + +function channel_problem_graphs(ar::ChannelRouter, channel) + wiresegs_ascending = sort(channel_segments(ar, channel), by=(ws) -> interval(ar, ws)) + # Y&K zone representation as interval graph + # Edge between each pair of segments that overlap + zone_ig = SimpleGraph(length(wiresegs_ascending)) + # Condrat et al. VCG with avoidable crossings as constraints + # Not handled: constraints from vertically aligned pin positions + vcg = SimpleDiGraph(length(wiresegs_ascending)) # just a fresh graph + for (idx1, seg1) in pairs(wiresegs_ascending) + low1, high1 = interval(ar, seg1) + for (idx2, seg2) in collect(pairs(wiresegs_ascending))[idx1+1:end] + low2, high2 = interval(ar, seg2) + + # If there is no overlap, break and move on to next seg1 + # Use >= instead of > to potentially allow knock-knees + low2 >= high1 && break # All subsequent seg2 have low2 >= high1 + # There is overlap, so add an edge to the interval graph + add_edge!(zone_ig, idx1, idx2) + # Now check if crossing is unavoidable + pt1, nt1 = prev_next_tendency(ar, seg1) + pt2, nt2 = prev_next_tendency(ar, seg2) + # If seg1 and seg2 enter and exit at the same place with same tendency, then crossing + # may be avoidable, but this channel can't say which goes on top yet + (low1 == high1 && low2 == high2 && pt1 == pt2 && nt1 == nt2) && break + low1_tend, high1_tend = against_channel(ar, seg1) ? (nt1, pt1) : (pt1, nt1) + low2_tend, high2_tend = against_channel(ar, seg2) ? (nt2, pt2) : (pt2, nt2) + avoidable = is_avoidable(low1, high1, low2, high2, + low1_tend, high1_tend, low2_tend, high2_tend) + !avoidable && continue + + # Crossing is avoidable, so add a constraint + # Determine which goes on top based on the lower bound tendency of seg2 + # Is prev or next the lower bound? + # top is rightmost segment iff its lower bound tends towards higher tracks + top = (idx1, idx2)[1 + (low2_tend == 1)] + bottom = (idx1, idx2)[2 - (low2_tend == 1)] + add_edge!(vcg, top, bottom) # VCG has edge from higher to lower tracks + end + end + return vcg, zone_ig +end + +function is_avoidable(low1, high1, low2, high2, low1_tend, high1_tend, low2_tend, high2_tend) + if high1 < high2 + # 1 ____ + # 2 ____ + order = [1, 2, 1, 2] # low1 <= low2 < high1 < high2 + ordered_tendency = [low1_tend, low2_tend, high1_tend, high2_tend] + else + # 1 _____ + # 2 __ + order = [1, 2, 2, 1] # low1 <= low2 < high2 <= high1 + ordered_tendency = [low1_tend, low2_tend, high2_tend, high1_tend] + end + up = (ordered_tendency .== 1) + down = (!).(up) + ccw_order = [reverse(order[up]); order[down]] + # Crossing is avoidable if same net has both endpoints adjacent + # on the clock + avoidable = (ccw_order[1] == ccw_order[2] || + ccw_order[2] == ccw_order[3]) + # Also, if seg1 and seg2 have an endpoint at the same place, + # then crossing in this channel may be avoidable but depends on + # other channel; assume other channel will agree + avoidable = (avoidable || (low1 == low2 || high1 == high2)) +end + +# +1 if segment crosses over high track index in ws's channel +function prev_next_tendency(ar, ws; use_segment_direction=true) + channel_idx = running_channel(ws) + start_channel, stop_channel = bounding_channels(ws) + # Distances along bounding and running channels + s_along_start = pathlength_at_intersection(ar, start_channel, channel_idx) + s1 = pathlength_at_intersection(ar, channel_idx, start_channel) + s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) + s_along_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) + # Directions of bounding and running channels + start_dir = direction_at_intersection(ar, start_channel, channel_idx) + dir1 = direction_at_intersection(ar, channel_idx, start_channel) + dir2 = direction_at_intersection(ar, channel_idx, stop_channel) + stop_dir = direction_at_intersection(ar, stop_channel, channel_idx) + # Tendencies + ## +ve = wire makes CCW turns + ## But actual bends depend on direction of wires vs channels + ### Signs of angles made by channel intersections + sgn_bend1 = sign(rem2pi(uconvert(NoUnits, dir1 - start_dir), RoundNearest)) + sgn_bend2 = sign(rem2pi(uconvert(NoUnits, stop_dir - dir2), RoundNearest)) + !use_segment_direction && return (sgn_bend1, sgn_bend2) + ### Need to multiply according to direction in channel + ### Is prev upper-bounded by ws? Then it goes along with channel + sgn_start = s_along_start >= last(interval(ar, prev(ar, ws), use_track=false)) ? 1 : -1 + ### Is next upper-bounded by ws? Then it goes against channel + sgn_stop = s_along_stop >= last(interval(ar, next(ar, ws), use_track=false)) ? -1 : 1 + ### Bend signs get another -1 if ws runs opposite to its channel direction + ### But then tendency definition is reversed also + return (sgn_start * sgn_bend1, sgn_stop * sgn_bend2) +end + +""" + make_routes!(ar::ChannelRouter, rule; net_indices=eachindex(ar.net_pins)) + +Using channel and track assignments, create `Route`s for the nets in `net_indices`. + +A point and direction is calculated for each `wire_segment`. These are assigned to +`ar.segment_waypoints[wire_segment]` (if not already assigned) and then added as a +waypoint/way-direction in the output `Route`. +""" +function make_routes!( + ar::ChannelRouter{T}, + rule; + net_indices=eachindex(ar.net_pins) +) where {T} + routes = Route{T}[] + for (idx_net, net_segs) in zip(net_indices, ar.net_wires[net_indices]) + waydirs = typeof(1.0°)[] + waypoints = Point{T}[] + for seg in net_segs + if haskey(ar.segment_waypoints, seg) + wp = segment_waypoint(ar, seg).p + wd = segment_waypoint(ar, seg).in_direction + push!(waypoints, wp) + push!(waydirs, wd) + else + wp, wd = segment_midpoint(ar, seg), segment_mid_direction(ar, seg) + push!(waypoints, wp) + push!(waydirs, wd) + ar.segment_waypoints[seg] = PointHook(wp, wd) + end + end + p0, p1 = pin_coordinates.(ar, net_pins(ar, idx_net)) + α0, α1 = pin_direction.(ar, net_pins(ar, idx_net)) + rt = Route(rule, p0, p1, α0, α1 + pi, waypoints=waypoints, waydirs=waydirs) + push!(routes, rt) + end + return routes +end + +function _delete_segment!(ar, ws; reset_tracks=true, from_net=true) + # Deletes the wire segment `ws` from `ar`. + + # Delete the wire segment in channel_segments + s = running_channel(ws) + deleteat!(channel_segments(ar, s), findfirst(isequal(ws), channel_segments(ar, s))) + + # Delete the segment from its track + c_idx = segment_track(ar, ws) + if !isnothing(c_idx) + if reset_tracks # by default, reset all track assignments in this channel + empty!(channel_tracks(ar, s)) + else + deleteat!( + channel_tracks(ar, s)[c_idx], + findfirst(isequal(ws), channel_tracks(ar, s)[c_idx]) + ) + end + end + # Delete the segment waypoint + delete!(ar.segment_waypoints, ws) + + # if from_net, delete ws from net_wires + # We set from_net to false when looping over net segments so it's not changing under us + return from_net && deleteat!( + net_wire(ar, net_index(ws)), + findfirst(isequal(ws), net_wire(ar, net_index(ws))) + ) +end + +""" + reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) + +Resets the nets with `net_indices` to their unrouted state. + +If `reset_tracks` is `true`, then all channels used by nets being reset will also have their +track assignments removed. +""" +function reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) + for segs in net_wire.(ar, net_indices) + for ws in segs + # delete segment from the router + # don't delete it from net yet, we'll do that after this loop + _delete_segment!(ar, ws, reset_tracks=reset_tracks, from_net=false) + end + empty!(segs) + end +end + +""" + autoroute!(ar::ChannelRouter, rule; net_indices=eachindex(ar.net_pins), + fixed_channel_paths::Dict{Int,Vector{Int}}=Dict()) + +Perform channel and track assigment, then make routes. + +Routes only the nets in `net_indices`. If the net is already routed, it is reset. A route +for a net can be specified in `fixed_channel_paths` by the indices of the channels the route +takes. For example, `fixed_channel_paths=Dict(1 => [2, 4, 1, 5])` will force net 1 to be +routed from its source pin, through channels 2, 4, 1, 5 in order, then to its destination pin. +""" +function autoroute!( + ar::ChannelRouter, + rule; + net_indices=eachindex(ar.net_pins), + fixed_channel_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}() +) + reset_nets!(ar, net_indices=net_indices) + assign_channels!(ar; net_indices=net_indices, fixed_paths=fixed_channel_paths) + assign_tracks!(ar) + return make_routes!(ar, rule; net_indices=net_indices) +end + +######## Modification + +""" + set_waypoint!(ar::ChannelRouter, net_idx, seg_idx, new_point) + set_waypoint!(ar::ChannelRouter, net_idx, seg_idx, new_point, new_direction) + +Sets the waypoint for the segment at `seg_idx` in net `net_idx` to `new_point`. + +A `new_direction` can also be specified for advanced usage. +""" +function set_waypoint!( + ar::ChannelRouter, + net_idx, + seg_idx, + new_point, + dir=segment_waypoints(ar, net_wire(ar, net_idx)[seg_idx])[2] +) + ws = net_wire(ar, net_idx)[seg_idx] + return ar.segment_waypoints[ws] = (new_point, dir) +end + +######## Visualization + +""" + visualize_router_state(ar::ChannelRouter{T}; wire_width=0.1*oneunit(T)) + +Return a `Cell` with rectangles and labels illustrating channels, tracks, and routes. + +Tracks are labeled as `S:C` where `S` is the channel index and `C` is the track index. + +Pins are labeled as `P/N` where `P` is the pin index and `N` is the net index. + +Segment waypoints are marked with circles and numbered sequentially within each net. +""" +function visualize_router_state( + ar::ChannelRouter{T}; + wire_width=0.1 * oneunit(T), + rule=StraightAnd90(min_bend_radius=wire_width, max_bend_radius=wire_width) +) where {T} + c = DeviceLayout.Cell{T}("track_viz") + + rts = make_routes!(ar, rule) + paths = [Path(rt, Paths.Trace(wire_width)) for rt in rts] + + DeviceLayout.render!.(c, paths, GDSMeta()) + rect = track_rectangles(ar) + DeviceLayout.render!.(c, rect, GDSMeta(2)) + rect2 = channel_rectangles(ar) + DeviceLayout.render!.(c, rect2, GDSMeta(3)) + lab = track_labels(ar) + [DeviceLayout.text!(c, l..., GDSMeta(4)) for l in lab] + [DeviceLayout.render!(c, DeviceLayout.circle(wire_width) + p, GDSMeta(5)) for rt in rts for p in rt.waypoints] + plab = pin_labels(ar) + [DeviceLayout.text!(c, l..., GDSMeta(6)) for l in plab] + wlab = waypoint_labels(ar) + [ + DeviceLayout.text!(c, l..., GDSMeta(8), xalign=DeviceLayout.Align.XCenter(), yalign=DeviceLayout.Align.YCenter()) + for l in wlab + ] + return c +end + +function track_rectangles(ar::ChannelRouter) + return [ + track_rectangle(ar, s, c) for s = 1:num_channels(ar) for c = 1:num_tracks(ar, s) + ] +end + +channel_rectangles(ar::ChannelRouter) = [channel_rectangle(ar, s) for s = 1:num_channels(ar)] + +function track_labels(ar::ChannelRouter) + return [ + ( + "$s:$c", + p0(track_rectangle(ar, s, c)) + ) for s = 1:num_channels(ar) for c = 1:num_tracks(ar, s) + ] +end + +function pin_labels(ar::ChannelRouter) + return [ + ("$p/$n", pin_coordinates(ar, p)) for n = 1:num_nets(ar) for p in net_pins(ar, n) + ] +end + +function waypoint_labels(ar::ChannelRouter) + return [ + ("$i", segment_waypoint(ar, segs[i]).p) for segs in ar.net_wires for + i = 1:length(segs) + ] +end + +function channel_rectangle(ar::ChannelRouter, channel_idx) + return ar.channels[channel_idx] +end + +function track_rectangle(ar::ChannelRouter{T}, channel_idx, track_idx) where {T} + off = track_offset(ar, channel_idx, track_idx, zero(T)) + seg = Paths.offset(ar.channels[channel_idx].seg, off) + n_tracks = length(channel_tracks(ar, channel_idx)) + w = Paths.width(ar.channels[channel_idx].sty, zero(T)) + pa = Path([Paths.Node(seg, Paths.Trace(w / (n_tracks+1)))]) + return pa +end + +######## Actually doing the path construction +struct AutoChannelRouting{T <: Coordinate} <: AbstractMultiRouting + channels::Vector{RouteChannel{T}} + transition_rule::RouteRule + transition_margin::T + router::ChannelRouter{T} +end +entry_rules(r::AutoChannelRouting) = Iterators.repeated(r.transition_rule) +exit_rule(r::AutoChannelRouting) = r.transition_rule + +function track_path_segments(rule::AutoChannelRouting, pa::Path, _) + return [track_path_segment(rule.router, channel, pa; margin=rule.transition_margin) + for channel in rule.channels[channels_taken(rule.router, pa)]] +end + +function track_path_segment(r::ChannelRouter{T}, ch::RouteChannel, pa::Path; margin=zero(T)) where {T} + # Get the track wire segment from the router + # Assume there is exactly one wire segment belonging to this path in the channel + # Channel node might have been converted to store in router, so just check start point/direction + channel_idx = findfirst(chn -> p0(chn.seg) ≈ p0(ch.path) && α0(chn.seg) == α0(ch.path), r.channels) + wireseg_idx = findfirst(ws -> running_channel(ws) == channel_idx, channel_segments(r, channel_idx)) + wireseg = channel_segments(r, channel_idx)[wireseg_idx] + track_idx = segment_track(r, wireseg) + # Get the starting and ending pathlengths + start_channel, stop_channel = bounding_channels(wireseg) + wireseg_start = pathlength_at_intersection(ar, channel_idx, start_channel) + wireseg_stop = pathlength_at_intersection(ar, channel_idx, stop_channel) + prev_width = width_at_intersection(ar, start_channel, channel_idx) + next_width = width_at_intersection(ar, stop_channel, channel_idx) + channel_section = segment_channel_section(channel, wireseg_start, wireseg_stop, prev_width, next_width; margin) + # Return channel section segment offset by width according to track + return track_path_segment(length(channel_tracks(ar, channel_idx)), channel_section, track_idx) +end + +function channels_taken(r::ChannelRouter, pa::Path) + net_idx = only(indexin(pa, r.net_paths)) + channels_taken = [running_channel(wireseg) for wireseg in net_wire(r, net_idx)] +end + +function _update_with_graph!(rule::AutoChannelRouting, route_node, graph; kwargs...) + push!(rule.router.net_paths, route_node.component._path) +end + +function _update_with_plan!(rule::AutoChannelRouting{T}, route_node, sch) where {T} + pin_idx = length(rule.router.pins) + 1 + push!(pins, hooks(route_node.component).p0) + push!(pins, hooks(route_node.component).p1) + push!(rule.router.net_pins, (pin_idx, pin_idx + 1)) + # If all paths have been added, go ahead and run autorouting + if length(rule.router.net_pins) == length(rule.router.net_paths) + build_channel_graph(rule.router.pins, rule.router.channels, T) + assign_channels!(rule.router) + assign_tracks!(ar) + end +end \ No newline at end of file diff --git a/test/test_channels.jl b/test/test_channels.jl index 531a09943..d58114911 100644 --- a/test/test_channels.jl +++ b/test/test_channels.jl @@ -287,3 +287,114 @@ ## Schematic-level routing test_schematic_single_channel() end + +function test_simple(; split=false) + + mypins = [ + Point(4.0, 3.0), + Point(6.0, 3.0), + Point(4.0, 7.0), + Point(6.0, 7.0), + Point(3.0, 4.0), + Point(3.0, 6.0), + Point(7.0, 4.0), + Point(7.0, 6.0) + ] + dirs = [0, pi, 0, pi, pi / 2, -pi / 2, pi / 2, -pi / 2] .+ pi + pins = PointHook.(mypins, dirs) + + space_paths = Path[] + for x0 in [1.0, 5.0, 9.0] + pa = Path(x0, 0.0, α0=90°) + if split && x0 == 5.0 + straight!(pa, 4.5, Paths.Trace(2.0)) + push!(space_paths, pa) + pa = Path(x0, 5.5, α0=90°) + straight!(pa, 4.5, Paths.Trace(2.0)) + push!(space_paths, pa) + continue + end + straight!(pa, 10.0, Paths.Trace(2.0)) + push!(space_paths, pa) + end + for y0 in [1.0, 5.0, 9.0] + pa = Path(0.0, y0) + if split && y0 == 5.0 + pa = Path(0.0, y0+0.7) + straight!(pa, 10.0, Paths.Trace(1.0)) + push!(space_paths, pa) + pa = Path(0.0, y0-0.7) + straight!(pa, 10.0, Paths.Trace(1.0)) + push!(space_paths, pa) + continue + end + straight!(pa, 10.0, Paths.Trace(2.0)) + push!(space_paths, pa) + end + + n_wires = 4 + mynets = [(i, i + 4) for i = 1:n_wires] + ar = ChannelRouter( + mynets, + pins, + RouteChannel.(space_paths) + ) + + + # # # # Split space demo + # # # Cut space 2 in half + # # pin_adjoining_spaces = [2, 2, 4, 4, 8, 6, 8, 6] + # # space_coord = [1.0, 5.0, 9.0, 5.0, 1.0, 5.5, 9.0, 4.5] + # # space_coord_idx = [1, 1, 1, 1, 2, 2, 2, 2] + # # space_widths = [2.0, 2.0, 2.0, 2.0, 2.0, 1.0, 2.0, 1.0] + + # # # Split space demo + # # 2 doesn't connect to upper half of horizontal spaces + # rem_edge!(ar.space_graph, 2, 6) + # rem_edge!(ar.space_graph, 2, 7) + # # 4 (other half of 2) doesn't connect to bottom half + # rem_edge!(ar.space_graph, 4, 5) + # rem_edge!(ar.space_graph, 4, 8) + + assign_channels!(ar) #, fixed_paths=Dict(2=>[2, 8, 4, 6])) + assign_tracks!(ar) + + rule = Paths.StraightAnd90(min_bend_radius=0.1, max_bend_radius=0.1) + # rule = Paths.StraightAnd45(min_bend_radius=0.1, max_bend_radius=0.1) + # rule = Paths.BSplineRouting(endpoints_speed=7.5) + # rts = make_routes!(ar, rule) + # paths = [Path(rt, Paths.Trace(0.1)) for rt in rts] + + c = visualize_router_state(ar); + + save("autoroute_test.gds", c) +end + +function test_fanout() + lx_outer = ly_outer = 10e6nm + lx_inner = ly_inner = 5e6nm + + fanout_space_bottom = Path(Point(-lx_outer/2, -(ly_inner/2 + (ly_outer - ly_inner)/4))) + straight!(fanout_space_bottom, lx_outer, Paths.Trace(0.8*(ly_outer - ly_inner)/4)) + + n_nets = 40 + x0s = range(-lx_outer/2, stop=lx_outer/2, length=n_nets+2)[2:end-1] + x1s = range(-lx_inner/2, stop=lx_inner/2, length=n_nets+2)[2:end-1] + p0s = [PointHook(x, -ly_outer/2, -90°) for x in x0s] + p1s = [PointHook(x, -ly_inner/2, 90°) for x in x1s] + # n_nets=10 + mynets = [(i, i + n_nets) for i = 1:n_nets] + ar = ChannelRouter( + mynets, + vcat(p0s, p1s), + [RouteChannel(fanout_space_bottom)] + ) + + assign_channels!(ar) + assign_tracks!(ar) + + c = visualize_router_state(ar); + + save("autoroute_test.gds", c) + return ar +end From 42152ca59ea736900c007b2f7005e871e132d675 Mon Sep 17 00:00:00 2001 From: Greg Peairs Date: Fri, 6 Mar 2026 08:16:53 -0800 Subject: [PATCH 02/32] Add grid escape test and fix autorouter --- Project.toml | 2 + src/hooks.jl | 9 + src/paths/channel_autorouter.jl | 32 ++-- src/paths/paths.jl | 1 + test/test_channels.jl | 294 ++++++++++++++++++++++---------- 5 files changed, 229 insertions(+), 109 deletions(-) diff --git a/Project.toml b/Project.toml index 7972a6c2d..615c5087a 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ version = "1.13.0" [deps] Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" +BipartiteMatching = "79040ab4-24c8-4c92-950c-d48b5991a0f6" Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" Clipper = "c8f6d549-b3ab-5508-a0d1-48fe138e8cc1" ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" @@ -47,6 +48,7 @@ SchematicGraphMakieExt = "GraphMakie" [compat] Accessors = "0.1" Aqua = "0.8" +BipartiteMatching = "0.1.1" Cairo = "1.0" Clipper = "0.6" ColorSchemes = "3" diff --git a/src/hooks.jl b/src/hooks.jl index 1ad51b78c..78e2dd65f 100644 --- a/src/hooks.jl +++ b/src/hooks.jl @@ -35,6 +35,13 @@ The outward-pointing angle opposite to the direction stored by the PointHook """ out_direction(h::Hook) = in_direction(h) + 180° +""" + getp(h::Hook) + +The point defining the position of `h`. +""" +getp(h::Hook) = h.p + """ compass(prefix=""; p0=Point(0μm, 0μm)) @@ -112,6 +119,8 @@ end transformation(h1::PointHook, h2::HandedPointHook) = transformation(h1, h2.h) transformation(h1::HandedPointHook, h2::PointHook) = transformation(h1.h, h2) +getp(h::HandedPointHook) = h.h.p + function Base.getproperty(h::HandedPointHook, s::Symbol) if s in (:p, :in_direction) return getfield(h.h, s) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 778446601..1f24c6d72 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -327,7 +327,7 @@ The angle with the x-axis made by segment `ws` directed along its wire toward it """ function segment_direction(ar::ChannelRouter, ws::TrackWireSegment, s) off = segment_offset(ar, ws, s) - seg = Paths.offset(ar.channels[running_channel(ws)].seg, off) + seg = Paths.offset(ar.channels[running_channel(ws)].node.seg, off) return direction(seg, s) end @@ -351,7 +351,7 @@ measured at pathlength `s` in the channel. """ function track_offset(ar::ChannelRouter{T}, channel_idx, track_idx, s...) where {T} n_tracks = length(channel_tracks(ar, channel_idx)) - w = Paths.width(ar.channels[channel_idx].sty, zero(T)) + w = Paths.width(ar.channels[channel_idx].node.sty, zero(T)) spacing = w / (n_tracks + 1) return spacing * (track_idx - (1 + n_tracks) / 2) end @@ -947,7 +947,7 @@ Segment waypoints are marked with circles and numbered sequentially within each """ function visualize_router_state( ar::ChannelRouter{T}; - wire_width=0.1 * oneunit(T), + wire_width=0.1*oneunit(T), rule=StraightAnd90(min_bend_radius=wire_width, max_bend_radius=wire_width) ) where {T} c = DeviceLayout.Cell{T}("track_viz") @@ -957,12 +957,12 @@ function visualize_router_state( DeviceLayout.render!.(c, paths, GDSMeta()) rect = track_rectangles(ar) - DeviceLayout.render!.(c, rect, GDSMeta(2)) - rect2 = channel_rectangles(ar) - DeviceLayout.render!.(c, rect2, GDSMeta(3)) + # DeviceLayout.render!.(c, rect, GDSMeta(2)) + channels = channel_paths(ar) + DeviceLayout.render!.(c, channels, GDSMeta(3)) lab = track_labels(ar) [DeviceLayout.text!(c, l..., GDSMeta(4)) for l in lab] - [DeviceLayout.render!(c, DeviceLayout.circle(wire_width) + p, GDSMeta(5)) for rt in rts for p in rt.waypoints] + [DeviceLayout.render!(c, DeviceLayout.Circle(wire_width) + p, GDSMeta(5)) for rt in rts for p in rt.waypoints] plab = pin_labels(ar) [DeviceLayout.text!(c, l..., GDSMeta(6)) for l in plab] wlab = waypoint_labels(ar) @@ -979,7 +979,7 @@ function track_rectangles(ar::ChannelRouter) ] end -channel_rectangles(ar::ChannelRouter) = [channel_rectangle(ar, s) for s = 1:num_channels(ar)] +channel_paths(ar::ChannelRouter) = [channel_path(ar, s) for s = 1:num_channels(ar)] function track_labels(ar::ChannelRouter) return [ @@ -1003,15 +1003,15 @@ function waypoint_labels(ar::ChannelRouter) ] end -function channel_rectangle(ar::ChannelRouter, channel_idx) - return ar.channels[channel_idx] +function channel_path(ar::ChannelRouter, channel_idx) + return ar.channels[channel_idx].path end function track_rectangle(ar::ChannelRouter{T}, channel_idx, track_idx) where {T} off = track_offset(ar, channel_idx, track_idx, zero(T)) - seg = Paths.offset(ar.channels[channel_idx].seg, off) + seg = Paths.offset(ar.channels[channel_idx].node.seg, off) n_tracks = length(channel_tracks(ar, channel_idx)) - w = Paths.width(ar.channels[channel_idx].sty, zero(T)) + w = Paths.width(ar.channels[channel_idx].node.sty, zero(T)) pa = Path([Paths.Node(seg, Paths.Trace(w / (n_tracks+1)))]) return pa end @@ -1061,13 +1061,13 @@ end function _update_with_plan!(rule::AutoChannelRouting{T}, route_node, sch) where {T} pin_idx = length(rule.router.pins) + 1 - push!(pins, hooks(route_node.component).p0) - push!(pins, hooks(route_node.component).p1) + push!(rule.router.pins, hooks(route_node.component).p0) + push!(rule.router.pins, hooks(route_node.component).p1) push!(rule.router.net_pins, (pin_idx, pin_idx + 1)) # If all paths have been added, go ahead and run autorouting if length(rule.router.net_pins) == length(rule.router.net_paths) - build_channel_graph(rule.router.pins, rule.router.channels, T) + build_channel_graph(rule.router.pins, getproperty.(rule.router.channels, :path), T) assign_channels!(rule.router) - assign_tracks!(ar) + assign_tracks!(rule.router) end end \ No newline at end of file diff --git a/src/paths/paths.jl b/src/paths/paths.jl index df8b6a91a..14bd05d11 100644 --- a/src/paths/paths.jl +++ b/src/paths/paths.jl @@ -762,6 +762,7 @@ include("segments/bspline_optimization.jl") include("routes.jl") include("channels.jl") +include("channel_autorouter.jl") function change_handedness!(seg::Union{Turn, Corner}) return seg.α = -seg.α diff --git a/test/test_channels.jl b/test/test_channels.jl index d58114911..ee0e513d5 100644 --- a/test/test_channels.jl +++ b/test/test_channels.jl @@ -288,113 +288,221 @@ test_schematic_single_channel() end -function test_simple(; split=false) - - mypins = [ - Point(4.0, 3.0), - Point(6.0, 3.0), - Point(4.0, 7.0), - Point(6.0, 7.0), - Point(3.0, 4.0), - Point(3.0, 6.0), - Point(7.0, 4.0), - Point(7.0, 6.0) - ] - dirs = [0, pi, 0, pi, pi / 2, -pi / 2, pi / 2, -pi / 2] .+ pi - pins = PointHook.(mypins, dirs) - - space_paths = Path[] - for x0 in [1.0, 5.0, 9.0] - pa = Path(x0, 0.0, α0=90°) - if split && x0 == 5.0 - straight!(pa, 4.5, Paths.Trace(2.0)) - push!(space_paths, pa) - pa = Path(x0, 5.5, α0=90°) - straight!(pa, 4.5, Paths.Trace(2.0)) +@testitem "Channel Autorouter" setup = [CommonTestSetup] begin + import DeviceLayout.Paths: RouteChannel, ChannelRouter + using .SchematicDrivenLayout + + function test_simple(; split=false) + + mypins = [ + Point(4.0, 3.0), + Point(6.0, 3.0), + Point(4.0, 7.0), + Point(6.0, 7.0), + Point(3.0, 4.0), + Point(3.0, 6.0), + Point(7.0, 4.0), + Point(7.0, 6.0) + ] + dirs = [0, pi, 0, pi, pi / 2, -pi / 2, pi / 2, -pi / 2] .+ pi + pins = PointHook.(mypins, dirs) + + space_paths = Path[] + for x0 in [1.0, 5.0, 9.0] + pa = Path(x0, 0.0, α0=90°) + if split && x0 == 5.0 + straight!(pa, 4.5, Paths.Trace(2.0)) + push!(space_paths, pa) + pa = Path(x0, 5.5, α0=90°) + straight!(pa, 4.5, Paths.Trace(2.0)) + push!(space_paths, pa) + continue + end + straight!(pa, 10.0, Paths.Trace(2.0)) push!(space_paths, pa) - continue end - straight!(pa, 10.0, Paths.Trace(2.0)) - push!(space_paths, pa) - end - for y0 in [1.0, 5.0, 9.0] - pa = Path(0.0, y0) - if split && y0 == 5.0 - pa = Path(0.0, y0+0.7) - straight!(pa, 10.0, Paths.Trace(1.0)) - push!(space_paths, pa) - pa = Path(0.0, y0-0.7) - straight!(pa, 10.0, Paths.Trace(1.0)) + for y0 in [1.0, 5.0, 9.0] + pa = Path(0.0, y0) + if split && y0 == 5.0 + pa = Path(0.0, y0+0.7) + straight!(pa, 10.0, Paths.Trace(1.0)) + push!(space_paths, pa) + pa = Path(0.0, y0-0.7) + straight!(pa, 10.0, Paths.Trace(1.0)) + push!(space_paths, pa) + continue + end + straight!(pa, 10.0, Paths.Trace(2.0)) push!(space_paths, pa) - continue end - straight!(pa, 10.0, Paths.Trace(2.0)) - push!(space_paths, pa) - end - n_wires = 4 - mynets = [(i, i + 4) for i = 1:n_wires] - ar = ChannelRouter( - mynets, - pins, - RouteChannel.(space_paths) - ) + n_wires = 4 + mynets = [(i, i + 4) for i = 1:n_wires] + ar = ChannelRouter( + mynets, + pins, + RouteChannel.(space_paths) + ) - # # # # Split space demo - # # # Cut space 2 in half - # # pin_adjoining_spaces = [2, 2, 4, 4, 8, 6, 8, 6] - # # space_coord = [1.0, 5.0, 9.0, 5.0, 1.0, 5.5, 9.0, 4.5] - # # space_coord_idx = [1, 1, 1, 1, 2, 2, 2, 2] - # # space_widths = [2.0, 2.0, 2.0, 2.0, 2.0, 1.0, 2.0, 1.0] + # # # # Split space demo + # # # Cut space 2 in half + # # pin_adjoining_spaces = [2, 2, 4, 4, 8, 6, 8, 6] + # # space_coord = [1.0, 5.0, 9.0, 5.0, 1.0, 5.5, 9.0, 4.5] + # # space_coord_idx = [1, 1, 1, 1, 2, 2, 2, 2] + # # space_widths = [2.0, 2.0, 2.0, 2.0, 2.0, 1.0, 2.0, 1.0] - # # # Split space demo - # # 2 doesn't connect to upper half of horizontal spaces - # rem_edge!(ar.space_graph, 2, 6) - # rem_edge!(ar.space_graph, 2, 7) - # # 4 (other half of 2) doesn't connect to bottom half - # rem_edge!(ar.space_graph, 4, 5) - # rem_edge!(ar.space_graph, 4, 8) + # # # Split space demo + # # 2 doesn't connect to upper half of horizontal spaces + # rem_edge!(ar.space_graph, 2, 6) + # rem_edge!(ar.space_graph, 2, 7) + # # 4 (other half of 2) doesn't connect to bottom half + # rem_edge!(ar.space_graph, 4, 5) + # rem_edge!(ar.space_graph, 4, 8) - assign_channels!(ar) #, fixed_paths=Dict(2=>[2, 8, 4, 6])) - assign_tracks!(ar) + Paths.assign_channels!(ar) #, fixed_paths=Dict(2=>[2, 8, 4, 6])) + Paths.assign_tracks!(ar) - rule = Paths.StraightAnd90(min_bend_radius=0.1, max_bend_radius=0.1) - # rule = Paths.StraightAnd45(min_bend_radius=0.1, max_bend_radius=0.1) - # rule = Paths.BSplineRouting(endpoints_speed=7.5) - # rts = make_routes!(ar, rule) - # paths = [Path(rt, Paths.Trace(0.1)) for rt in rts] + rule = Paths.StraightAnd90(min_bend_radius=0.1, max_bend_radius=0.1) + # rule = Paths.StraightAnd45(min_bend_radius=0.1, max_bend_radius=0.1) + # rule = Paths.BSplineRouting(endpoints_speed=7.5) + # rts = make_routes!(ar, rule) + # paths = [Path(rt, Paths.Trace(0.1)) for rt in rts] - c = visualize_router_state(ar); + c = Paths.visualize_router_state(ar); - save("autoroute_test.gds", c) -end + save("autoroute_test.svg", c, width=10DeviceLayout.Graphics.inch) + return c + end -function test_fanout() - lx_outer = ly_outer = 10e6nm - lx_inner = ly_inner = 5e6nm - - fanout_space_bottom = Path(Point(-lx_outer/2, -(ly_inner/2 + (ly_outer - ly_inner)/4))) - straight!(fanout_space_bottom, lx_outer, Paths.Trace(0.8*(ly_outer - ly_inner)/4)) - - n_nets = 40 - x0s = range(-lx_outer/2, stop=lx_outer/2, length=n_nets+2)[2:end-1] - x1s = range(-lx_inner/2, stop=lx_inner/2, length=n_nets+2)[2:end-1] - p0s = [PointHook(x, -ly_outer/2, -90°) for x in x0s] - p1s = [PointHook(x, -ly_inner/2, 90°) for x in x1s] - # n_nets=10 - mynets = [(i, i + n_nets) for i = 1:n_nets] - ar = ChannelRouter( - mynets, - vcat(p0s, p1s), - [RouteChannel(fanout_space_bottom)] - ) + function test_fanout() + lx_outer = ly_outer = 10e6nm + lx_inner = ly_inner = 5e6nm + + fanout_space_bottom = Path(Point(-lx_outer/2, -(ly_inner/2 + (ly_outer - ly_inner)/4))) + straight!(fanout_space_bottom, lx_outer, Paths.Trace(0.8*(ly_outer - ly_inner)/4)) + + n_nets = 40 + x0s = range(-lx_outer/2, stop=lx_outer/2, length=n_nets+2)[2:end-1] + x1s = range(-lx_inner/2, stop=lx_inner/2, length=n_nets+2)[2:end-1] + p0s = [PointHook(x, -ly_outer/2, -90°) for x in x0s] + p1s = [PointHook(x, -ly_inner/2, 90°) for x in x1s] + # n_nets=10 + mynets = [(i, i + n_nets) for i = 1:n_nets] + ar = ChannelRouter( + mynets, + vcat(p0s, p1s), + [RouteChannel(fanout_space_bottom)] + ) - assign_channels!(ar) - assign_tracks!(ar) + Paths.assign_channels!(ar) + Paths.assign_tracks!(ar) - c = visualize_router_state(ar); + c = Paths.visualize_router_state(ar); - save("autoroute_test.gds", c) - return ar + save("autoroute_test.png", c, width=10DeviceLayout.Graphics.inch) + return c + end + function test_grid_escape() + # Grid of sites + dx = 1.5 + dy = 1.5 + nx = 20 + ny = 20 + grid = [(ix, iy) for ix in 1:nx for iy in 1:ny] + sites = [Point(dx * (ix - 1), dy * (iy - 1)) for (ix, iy) in grid] + site_size = 0.5 + + # Channel coordinates + n_xch = nx + 1 # vertical channels + n_ych = ny + 1 # horizontal channels + x_coords = range(-dx / 2, step=dx, length=n_xch) + y_coords = range(-dy / 2, step=dy, length=n_ych) + + # Build channel Paths (vertical then horizontal) + # Margin ensures channels cross each other but don't extend to edge pin positions + margin = 0.5 + space_paths = Path[] + for x in x_coords + pa = Path(x, first(y_coords) - margin, α0=90°) + straight!(pa, last(y_coords) - first(y_coords) + 2margin, Paths.Trace(1.0)) + push!(space_paths, pa) + end + for y in y_coords + pa = Path(first(x_coords) - margin, y) + straight!(pa, last(x_coords) - first(x_coords) + 2margin, Paths.Trace(1.0)) + push!(space_paths, pa) + end + + # Site pins: each site escapes toward the nearest grid edge + site_dirs = map(grid) do (ix, iy) + ix_delta = ix - nx / 2 + iy_delta = iy - ny / 2 + if abs(ix_delta) > abs(iy_delta) + return ix_delta > 0 ? 0.0 : pi + end + return iy_delta > 0 ? pi / 2 : -pi / 2 + end + site_pin_pos = [ + site + 0.5site_size * Point(cos(d), sin(d)) + for (site, d) in zip(sites, site_dirs) + ] + # PointHook in_direction points inward (away from channel); site_dirs point toward channel + site_hooks = [PointHook(p, d + pi) for (p, d) in zip(site_pin_pos, site_dirs)] + + # Edge pins: placed at grid boundary, one per site + edge_dirs = [rem2pi(d + pi, RoundNearest) for d in site_dirs] + + # Edge geometry: fixed coordinate and orientation for each of 4 edges + edge_fixed = [first(x_coords), last(x_coords), first(y_coords), last(y_coords)] + edge_is_vertical = [true, true, false, false] + function edge_index(dir) + abs(dir) < 0.1 && return 1 # left edge (dir ≈ 0) + abs(abs(dir) - pi) < 0.1 && return 2 # right edge (dir ≈ ±π) + abs(dir - pi / 2) < 0.1 && return 3 # bottom edge (dir ≈ π/2) + return 4 # top edge (dir ≈ -π/2) + end + + # Pre-compute edge assignments and per-edge coordinate ranges + edge_assignments = [edge_index(edir) for edir in edge_dirs] + pins_per_edge = [count(==(ei), edge_assignments) for ei in 1:4] + coord_span = (-dx / 2, -dx / 2 + nx * dx) + coord_ranges = [range(coord_span..., length=n) for n in pins_per_edge] + + edge_count = [1, 1, 1, 1] + edge_hooks = map(zip(edge_dirs, edge_assignments)) do (edir, ei) + ci = edge_count[ei] + edge_count[ei] += 1 + fixed = edge_fixed[ei] + c = coord_ranges[ei][ci] + pos = if edge_is_vertical[ei] + Point(fixed - cos(edir), c) + else + Point(c, fixed - sin(edir)) + end + return PointHook(pos, edir + pi) + end + + # Assemble: each site pin connects to its corresponding edge pin + all_hooks = [site_hooks; edge_hooks] + n_wires = length(sites) + mynets = [(i, i + n_wires) for i in 1:n_wires] + + ar = ChannelRouter( + mynets, + all_hooks, + RouteChannel.(space_paths) + ) + + Paths.assign_channels!(ar) + Paths.assign_tracks!(ar) + + c = Paths.visualize_router_state(ar, wire_width=0.001) + save("autoroute_escape_test.gds", c) + return c + end + # Runs without error + test_simple() + test_fanout() + test_grid_escape() end From 6bfffe9ed6620974a77543161588d6e0a42b5ec1 Mon Sep 17 00:00:00 2001 From: Greg Peairs Date: Mon, 9 Mar 2026 07:56:26 -0700 Subject: [PATCH 03/32] Add actual shortest-distance pathfinding, fix issue with merge groups not in longest vcg dag path --- Project.toml | 4 + src/paths/channel_autorouter.jl | 126 ++++++++++++++++++++++++++++---- 2 files changed, 117 insertions(+), 13 deletions(-) diff --git a/Project.toml b/Project.toml index 615c5087a..74f92cac5 100644 --- a/Project.toml +++ b/Project.toml @@ -32,6 +32,7 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Preferences = "21216c6a-2e73-6563-6e65-726566657250" QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SpatialIndexing = "d4ead438-fe20-5cc5-a293-4fd39a41b74c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" ThreadSafeDicts = "4239201d-c60e-5e0a-9702-85d713665ba7" @@ -86,5 +87,8 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" +[preferences.DeviceLayout] +units = "PreferMicrons" + [targets] test = ["Aqua", "Test", "TestItemRunner"] diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 1f24c6d72..1bf6ffad4 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -6,6 +6,7 @@ import Graphs: SimpleGraph, SimpleDiGraph, nv, + ne, add_edge!, rem_edge!, adjacency_matrix, @@ -15,8 +16,12 @@ import Graphs: outneighbors, dag_longest_path, maximal_cliques, - yen_k_shortest_paths + yen_k_shortest_paths, + dijkstra_shortest_paths, + enumerate_paths, + topological_sort_by_dfs import LinearAlgebra: norm +import SparseArrays: sparse import BipartiteMatching # TrackWireSegment = (net, lengthwise channel, start vertex, end vertex) @@ -37,6 +42,13 @@ const NetWire = Vector{TrackWireSegment} # pathlength 1, pathlength 2, dir1, dir2, intersection point const IntersectionInfo{T} = Tuple{T, T, typeof(1.0°), typeof(1.0°), Point{T}} +struct AuxiliaryGraph{T, M <: AbstractMatrix{T}} + graph::SimpleGraph{Int} + distmx::M + aux_to_edge::Vector{Tuple{Int, Int}} # aux vertex → original (u, v) edge + edge_to_aux::Dict{Tuple{Int, Int}, Int} # original (u, v) edge → aux vertex +end + """ ChannelRouter{T <: Coordinate} @@ -190,6 +202,14 @@ end segment_waypoint(ar::ChannelRouter, ws::TrackWireSegment) = ar.segment_waypoints[ws] _swap(x, y) = (y > x ? (x, y) : (y, x)) +function _shared_vertex(edge_a::Tuple{Int,Int}, edge_b::Tuple{Int,Int}) + a1, a2 = edge_a + b1, b2 = edge_b + a1 in (b1, b2) && return a1 + a2 in (b1, b2) && return a2 + error("Edges $edge_a and $edge_b share no vertex") +end + pathlength_from_start(channel, node, s) = pathlength(channel[1:node-1]) + s # Build graph with pins/channels as vertices and intersections as edges @@ -423,16 +443,70 @@ function prev(ar::ChannelRouter, ws::TrackWireSegment) return segs[idx - 1] end +""" + build_auxiliary_graph(ar::ChannelRouter) + +Build an auxiliary graph where each vertex represents an intersection point (edge in the +channel graph) and edges connect consecutive intersections along the same channel, weighted +by the physical distance between them. +""" +function build_auxiliary_graph(ar::ChannelRouter{T}) where {T} + g = channel_graph(ar) + n_aux = ne(g) + + # Map channel-graph edges to auxiliary vertices + aux_to_edge = Vector{Tuple{Int,Int}}(undef, n_aux) + edge_to_aux = Dict{Tuple{Int,Int}, Int}() + for (i, e) in enumerate(edges(g)) + key = _swap(e.src, e.dst) + aux_to_edge[i] = key + edge_to_aux[key] = i + end + + # Build auxiliary graph: chain consecutive intersections per channel + aux_g = SimpleGraph(n_aux) + I = Int[] + J = Int[] + V = T[] + for ch in 1:num_channels(ar) + nbs = neighbors(g, ch) + length(nbs) < 2 && continue + + # Collect (pathlength_along_ch, aux_vertex) for each intersection on this channel + ch_ixns = Tuple{T, Int}[ + (pathlength_at_intersection(ar, ch, nb), edge_to_aux[_swap(ch, nb)]) + for nb in nbs + ] + sort!(ch_ixns, by=first) + + # Connect consecutive intersection points + for k in 1:(length(ch_ixns) - 1) + s1, aux1 = ch_ixns[k] + s2, aux2 = ch_ixns[k + 1] + add_edge!(aux_g, aux1, aux2) + w = abs(s2 - s1) + push!(I, aux1); push!(J, aux2); push!(V, w) + push!(I, aux2); push!(J, aux1); push!(V, w) + end + end + + distmx = sparse(I, J, V, n_aux, n_aux) + return AuxiliaryGraph(aux_g, distmx, aux_to_edge, edge_to_aux) +end + """ shortest_path_between_pins(ar::ChannelRouter, pin_1::Int, pin_2::Int) -A shortest path in the router's channel graph from `pin_1` to `pin_2`. +A shortest path in the router's channel graph from `pin_1` to `pin_2`, minimizing +hop count (number of channel transitions). -Distance is not physical distance but graph distance (the number of edges in the path). + shortest_path_between_pins(ar::ChannelRouter, pin_1::Int, pin_2::Int, aux::AuxiliaryGraph) -In the channel graph, each channel is a vertex, and there is an edge between each intersecting -pair of channels. Each pin is also a vertex, with an edge only to its adjoining channel. A path -is a list of vertex indices `path::Vector{Int}`. +A shortest path minimizing physical distance (sum of arclengths along channels between +intersection points), using a precomputed [`AuxiliaryGraph`](@ref). + +In both cases, the returned path is a list of vertex indices +`[pin_gidx, ch1, ..., chN, pin_gidx]`. """ function shortest_path_between_pins(ar::ChannelRouter, p0::Int, p1::Int) ys = yen_k_shortest_paths( @@ -443,14 +517,39 @@ function shortest_path_between_pins(ar::ChannelRouter, p0::Int, p1::Int) return ys.paths[1] end +function shortest_path_between_pins(ar::ChannelRouter, p0::Int, p1::Int, aux::AuxiliaryGraph) + pin0_gidx = pin_to_graphidx(ar, p0) + pin1_gidx = pin_to_graphidx(ar, p1) + src_aux = aux.edge_to_aux[_swap(pin0_gidx, adjoining_channel(ar, p0))] + dst_aux = aux.edge_to_aux[_swap(pin1_gidx, adjoining_channel(ar, p1))] + + ds = dijkstra_shortest_paths(aux.graph, src_aux, aux.distmx) + aux_path = enumerate_paths(ds, dst_aux) + isempty(aux_path) && error("No path between pins $p0 and $p1") + + # Convert aux vertices back to channel-graph vertex sequence. + # Skip consecutive duplicates: the aux path may traverse multiple intersections on + # the same channel, which just means a longer segment on that channel. + path = Int[pin0_gidx] + for i in 1:(length(aux_path) - 1) + ch = _shared_vertex(aux.aux_to_edge[aux_path[i]], + aux.aux_to_edge[aux_path[i + 1]]) + if ch != last(path) + push!(path, ch) + end + end + push!(path, pin1_gidx) + return path +end + """ assign_channels!(ar::ChannelRouter) Performs channel assignment for `ar`. -Currently just finds a "shortest path" between pins, where -distance is not physical distance but graph distance (the number of edges in the path). -In other words, each net takes a path that changes channels a minimal number of times. +Finds a shortest path between pins minimizing physical distance along channels +(sum of arclengths between intersection points), using an auxiliary intersection-point +graph with Dijkstra's algorithm. Does not currently take congestion, crossings, or channel capacity into account. """ function assign_channels!( @@ -458,6 +557,7 @@ function assign_channels!( net_indices=eachindex(ar.net_pins), fixed_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}() ) + aux = build_auxiliary_graph(ar) for (idx_net, net) in zip(net_indices, ar.net_pins[net_indices]) p0, p1 = net path = if idx_net in keys(fixed_paths) @@ -467,7 +567,7 @@ function assign_channels!( pin_to_graphidx(ar, p1) ] else - shortest_path_between_pins(ar, p0, p1) + shortest_path_between_pins(ar, p0, p1, aux) end ixns = [(path[i], path[i + 1]) for i = 1:(length(path) - 1)] segs = [(ixns[i], ixns[i + 1]) for i = 1:(length(ixns) - 1)] @@ -581,7 +681,7 @@ function assign_tracks_matching!(ar, channel) tracks = channel_tracks(ar, channel) # At the end of this process, segments are merged into layers in the VCG # So the longest directed path gives a representative of each merged group - high_to_low = dag_longest_path(vcg) # If vcg was acyclic to begin with, it is still acyclic + high_to_low = topological_sort_by_dfs(vcg) # If vcg was acyclic to begin with, it is still acyclic num_tracks = length(high_to_low) for v in 1:nv(vcg) if !haskey(merged_groups, v) @@ -735,7 +835,7 @@ function channel_problem_graphs(ar::ChannelRouter, channel) # Crossing is avoidable, so add a constraint # Determine which goes on top based on the lower bound tendency of seg2 # Is prev or next the lower bound? - # top is rightmost segment iff its lower bound tends towards higher tracks + # top is rightmost segment iff its lower bound tends towards higher (lower index) tracks top = (idx1, idx2)[1 + (low2_tend == 1)] bottom = (idx1, idx2)[2 - (low2_tend == 1)] add_edge!(vcg, top, bottom) # VCG has edge from higher to lower tracks @@ -769,7 +869,7 @@ function is_avoidable(low1, high1, low2, high2, low1_tend, high1_tend, low2_tend avoidable = (avoidable || (low1 == low2 || high1 == high2)) end -# +1 if segment crosses over high track index in ws's channel +# +1 if segment crosses over low track index in ws's channel function prev_next_tendency(ar, ws; use_segment_direction=true) channel_idx = running_channel(ws) start_channel, stop_channel = bounding_channels(ws) From 105abbb34d5af46149e96946c9792b2997e75e99 Mon Sep 17 00:00:00 2001 From: Greg Peairs Date: Mon, 9 Mar 2026 08:13:17 -0700 Subject: [PATCH 04/32] Don't assign tracks multiple times --- src/paths/channel_autorouter.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 1bf6ffad4..5f40073c3 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -680,7 +680,8 @@ function assign_tracks_matching!(ar, channel) # Assign merged groups to tracks according to VCG tracks = channel_tracks(ar, channel) # At the end of this process, segments are merged into layers in the VCG - # So the longest directed path gives a representative of each merged group + # The longest directed path gives a representative of each merged group where track height is max + # But VCG may be a partial order so use topological sort high_to_low = topological_sort_by_dfs(vcg) # If vcg was acyclic to begin with, it is still acyclic num_tracks = length(high_to_low) for v in 1:nv(vcg) @@ -688,9 +689,12 @@ function assign_tracks_matching!(ar, channel) merged_groups[v] = [v] end end + assigned = Int[] for v in reverse(high_to_low) # Create a track with `v` and all others merged with it + v in assigned && continue push!(tracks, wiresegs_ascending[merged_groups[v]]) + append!(assigned, merged_groups[v]) end end From acbae38e2e62f488a82720e920121fb9e3a15fa3 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Thu, 2 Apr 2026 16:01:22 +0200 Subject: [PATCH 05/32] Allow endpoint overlap (knock-knees) only with opposite tendency --- src/paths/channel_autorouter.jl | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 5f40073c3..2d774895b 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -702,12 +702,26 @@ function segments_overlap(ar, seg1, seg2) low1, high1 = interval(ar, seg1) low2, high2 = interval(ar, seg2) if low1 <= low2 # segments are in ascending order - return low2 < high1 # no overlap for '==' means knock-knees are OK - else # descending order - return low1 < high2 + low2 < high1 && return true + low2 == high1 || return false + # Boundary case: check if knock-knee is actually possible + return _same_tendency_at_boundary(ar, seg1, seg2) + else # descending order, just reverse the roles + low1 < high2 && return true + low1 == high2 || return false + return _same_tendency_at_boundary(ar, seg2, seg1) end end +function _same_tendency_at_boundary(ar, seg1, seg2) + p1, n1 = bounding_channels(seg1) + p2, n2 = bounding_channels(seg2) + shared_boundary = [2 - 1*(p1 == p2 || p1 == n2), 2 - 1*(p2 == p1 || p2 == n1)] + t1 = prev_next_tendency(ar, seg1) + t2 = prev_next_tendency(ar, seg2) + return t1[shared_boundary[1]] == t2[shared_boundary[2]] +end + function best_matching!(merging_graph, vcg) # Collect set of edges to remove to_remove = Set{Tuple{Int, Int}}() From 624126e4f162222bbcd03aa813346764e3360eb2 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Thu, 2 Apr 2026 18:15:28 +0200 Subject: [PATCH 06/32] WIP AutoChannelRouting --- src/paths/channel_autorouter.jl | 19 +++++++++++-------- src/paths/paths.jl | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 2d774895b..cfd54ffc0 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -1149,28 +1149,31 @@ function track_path_segments(rule::AutoChannelRouting, pa::Path, _) for channel in rule.channels[channels_taken(rule.router, pa)]] end -function track_path_segment(r::ChannelRouter{T}, ch::RouteChannel, pa::Path; margin=zero(T)) where {T} +function track_path_segment(ar::ChannelRouter{T}, ch::RouteChannel, pa::Path; margin=zero(T)) where {T} # Get the track wire segment from the router # Assume there is exactly one wire segment belonging to this path in the channel # Channel node might have been converted to store in router, so just check start point/direction - channel_idx = findfirst(chn -> p0(chn.seg) ≈ p0(ch.path) && α0(chn.seg) == α0(ch.path), r.channels) - wireseg_idx = findfirst(ws -> running_channel(ws) == channel_idx, channel_segments(r, channel_idx)) - wireseg = channel_segments(r, channel_idx)[wireseg_idx] - track_idx = segment_track(r, wireseg) + channel_idx = findfirst(chn -> p0(chn.seg) ≈ p0(ch.path) && α0(chn.seg) == α0(ch.path), ar.channels) + net_idx = indexin(pa, ar.net_paths) + wireseg_idx = findfirst(ws -> running_channel(ws) == channel_idx, net_wire(ar, net_idx)) + wireseg = channel_segments(ar, channel_idx)[wireseg_idx] + track_idx = segment_track(ar, wireseg) # Get the starting and ending pathlengths start_channel, stop_channel = bounding_channels(wireseg) wireseg_start = pathlength_at_intersection(ar, channel_idx, start_channel) wireseg_stop = pathlength_at_intersection(ar, channel_idx, stop_channel) prev_width = width_at_intersection(ar, start_channel, channel_idx) next_width = width_at_intersection(ar, stop_channel, channel_idx) - channel_section = segment_channel_section(channel, wireseg_start, wireseg_stop, prev_width, next_width; margin) + channel_section = segment_channel_section(channel, + wireseg_start, wireseg_stop, prev_width, next_width; margin) # Return channel section segment offset by width according to track - return track_path_segment(length(channel_tracks(ar, channel_idx)), channel_section, track_idx) + return track_path_segment(length(channel_tracks(ar, channel_idx)), + channel_section, track_idx; reversed=wireseg_start > wireseg_stop) end function channels_taken(r::ChannelRouter, pa::Path) net_idx = only(indexin(pa, r.net_paths)) - channels_taken = [running_channel(wireseg) for wireseg in net_wire(r, net_idx)] + return [running_channel(wireseg) for wireseg in net_wire(r, net_idx)] end function _update_with_graph!(rule::AutoChannelRouting, route_node, graph; kwargs...) diff --git a/src/paths/paths.jl b/src/paths/paths.jl index 14bd05d11..0122f6ac8 100644 --- a/src/paths/paths.jl +++ b/src/paths/paths.jl @@ -709,8 +709,8 @@ end Generic fallback, approximating a [`Paths.Segment`](@ref) using many [`Polygons.LineSegment`](@ref) objects. Returns a vector of `LineSegment`s. """ -function line_segments(seg::Paths.Segment) - return Polygons.segmentize(seg.(discretization(seg)), false) +function line_segments(seg::Paths.Segment{T}) where {T} + return Polygons.segmentize(DeviceLayout.discretize_curve(seg, DeviceLayout.onenanometer(T)), false) end """ From d59dda21b8ff4ecc2437c25ec83aa8f2abe776b5 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Thu, 2 Apr 2026 21:18:44 +0200 Subject: [PATCH 07/32] Fix track offset/ordering in autorouter --- src/paths/channel_autorouter.jl | 45 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index cfd54ffc0..77bc9d700 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -334,7 +334,7 @@ function segment_midpoint(ar::ChannelRouter{T}, ws::TrackWireSegment) where {T} end channel_midpoint = channel_coordinates(ar, channel_idx, s) - offset_distance = segment_offset(ar, ws) + offset_distance = segment_offset(ar, ws; use_wire_direction=false) channel_dir = channel_direction(ar, channel_idx, s) return channel_midpoint + offset_distance * Point(-sin(channel_dir), cos(channel_dir)) @@ -346,7 +346,7 @@ end The angle with the x-axis made by segment `ws` directed along its wire toward its end pin. """ function segment_direction(ar::ChannelRouter, ws::TrackWireSegment, s) - off = segment_offset(ar, ws, s) + off = segment_offset(ar, ws) seg = Paths.offset(ar.channels[running_channel(ws)].node.seg, off) return direction(seg, s) end @@ -356,11 +356,14 @@ function segment_mid_direction(ar::ChannelRouter, ws::TrackWireSegment) return segment_direction(ar, ws, (s0+s1)/2) end -function segment_offset(ar::ChannelRouter{T}, ws::TrackWireSegment, s...) where {T} - is_pin(ar, running_channel(ws)) && return zero(T) - c = segment_track(ar, ws) - isnothing(c) && return zero(T) - return track_offset(ar, running_channel(ws), c, s...) +function segment_offset(ar::ChannelRouter{T}, ws::TrackWireSegment, s...; use_wire_direction=true) where {T} + channel_idx = running_channel(ws) + is_pin(ar, channel_idx) && return zero(T) + track_idx = segment_track(ar, ws) + isnothing(track_idx) && return zero(T) + reversed = use_wire_direction && against_channel(ar, ws) + return track_section_offset(length(ar.channel_tracks[channel_idx]), + Paths.width(ar.channels[channel_idx].node.sty, s...), track_idx; reversed) end """ @@ -373,7 +376,7 @@ function track_offset(ar::ChannelRouter{T}, channel_idx, track_idx, s...) where n_tracks = length(channel_tracks(ar, channel_idx)) w = Paths.width(ar.channels[channel_idx].node.sty, zero(T)) spacing = w / (n_tracks + 1) - return spacing * (track_idx - (1 + n_tracks) / 2) + return spacing * ((1 + n_tracks) / 2 - track_idx) end """ @@ -395,12 +398,11 @@ function interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=true) s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) (!use_track || is_pin(ar, channel_idx)) && return _swap(s1, s2) # Could just do that for all cases - # But if we want to break ties we would use offsets from previous/next segments, like: - # return _swap(s1 + segment_offset(ar, prev(ws)), s2 - segment_offset(ar, next(ws))) - # But sign needs to take into account relative orientations of channels - pt, nt = prev_next_tendency(ar, ws; use_segment_direction=false) + # But if we want to break ties we use offsets from previous/next segments + # Offset sign needs to take into account relative directions of segments in channels s_start = pathlength_at_intersection(ar, start_channel, channel_idx) s_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) + pt, nt = prev_next_tendency(ar, ws) return _swap(s1 + pt*segment_offset(ar, prev(ar, ws), s_start), s2 - nt*segment_offset(ar, next(ar, ws), s_stop)) end @@ -683,14 +685,13 @@ function assign_tracks_matching!(ar, channel) # The longest directed path gives a representative of each merged group where track height is max # But VCG may be a partial order so use topological sort high_to_low = topological_sort_by_dfs(vcg) # If vcg was acyclic to begin with, it is still acyclic - num_tracks = length(high_to_low) for v in 1:nv(vcg) if !haskey(merged_groups, v) merged_groups[v] = [v] end end assigned = Int[] - for v in reverse(high_to_low) + for v in high_to_low # high in vcg => low track index # Create a track with `v` and all others merged with it v in assigned && continue push!(tracks, wiresegs_ascending[merged_groups[v]]) @@ -888,7 +889,7 @@ function is_avoidable(low1, high1, low2, high2, low1_tend, high1_tend, low2_tend end # +1 if segment crosses over low track index in ws's channel -function prev_next_tendency(ar, ws; use_segment_direction=true) +function prev_next_tendency(ar, ws; use_wire_direction=true) channel_idx = running_channel(ws) start_channel, stop_channel = bounding_channels(ws) # Distances along bounding and running channels @@ -907,7 +908,7 @@ function prev_next_tendency(ar, ws; use_segment_direction=true) ### Signs of angles made by channel intersections sgn_bend1 = sign(rem2pi(uconvert(NoUnits, dir1 - start_dir), RoundNearest)) sgn_bend2 = sign(rem2pi(uconvert(NoUnits, stop_dir - dir2), RoundNearest)) - !use_segment_direction && return (sgn_bend1, sgn_bend2) + !use_wire_direction && return (sgn_bend1, sgn_bend2) ### Need to multiply according to direction in channel ### Is prev upper-bounded by ws? Then it goes along with channel sgn_start = s_along_start >= last(interval(ar, prev(ar, ws), use_track=false)) ? 1 : -1 @@ -1135,7 +1136,7 @@ function track_rectangle(ar::ChannelRouter{T}, channel_idx, track_idx) where {T} end ######## Actually doing the path construction -struct AutoChannelRouting{T <: Coordinate} <: AbstractMultiRouting +struct AutoChannelRouting{T <: Coordinate} <: AbstractChannelRouting channels::Vector{RouteChannel{T}} transition_rule::RouteRule transition_margin::T @@ -1153,10 +1154,10 @@ function track_path_segment(ar::ChannelRouter{T}, ch::RouteChannel, pa::Path; ma # Get the track wire segment from the router # Assume there is exactly one wire segment belonging to this path in the channel # Channel node might have been converted to store in router, so just check start point/direction - channel_idx = findfirst(chn -> p0(chn.seg) ≈ p0(ch.path) && α0(chn.seg) == α0(ch.path), ar.channels) - net_idx = indexin(pa, ar.net_paths) + channel_idx = findfirst(chn -> p0(chn.node.seg) ≈ p0(ch.path) && α0(chn.node.seg) == α0(ch.path), ar.channels) + net_idx = findfirst(p -> p === pa, ar.net_paths) wireseg_idx = findfirst(ws -> running_channel(ws) == channel_idx, net_wire(ar, net_idx)) - wireseg = channel_segments(ar, channel_idx)[wireseg_idx] + wireseg = net_wire(ar, net_idx)[wireseg_idx] track_idx = segment_track(ar, wireseg) # Get the starting and ending pathlengths start_channel, stop_channel = bounding_channels(wireseg) @@ -1164,7 +1165,7 @@ function track_path_segment(ar::ChannelRouter{T}, ch::RouteChannel, pa::Path; ma wireseg_stop = pathlength_at_intersection(ar, channel_idx, stop_channel) prev_width = width_at_intersection(ar, start_channel, channel_idx) next_width = width_at_intersection(ar, stop_channel, channel_idx) - channel_section = segment_channel_section(channel, + channel_section = segment_channel_section(ch, wireseg_start, wireseg_stop, prev_width, next_width; margin) # Return channel section segment offset by width according to track return track_path_segment(length(channel_tracks(ar, channel_idx)), @@ -1172,7 +1173,7 @@ function track_path_segment(ar::ChannelRouter{T}, ch::RouteChannel, pa::Path; ma end function channels_taken(r::ChannelRouter, pa::Path) - net_idx = only(indexin(pa, r.net_paths)) + net_idx = findfirst(p -> p === pa, r.net_paths) return [running_channel(wireseg) for wireseg in net_wire(r, net_idx)] end From 70128d1991015522673fec3acc7bc705787a1fe0 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Thu, 2 Apr 2026 21:19:06 +0200 Subject: [PATCH 08/32] Add autoroute examples --- test/autoroute_examples.jl | 454 +++++++++++++++++++++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 test/autoroute_examples.jl diff --git a/test/autoroute_examples.jl b/test/autoroute_examples.jl new file mode 100644 index 000000000..de2be393a --- /dev/null +++ b/test/autoroute_examples.jl @@ -0,0 +1,454 @@ +using DeviceLayout +using FileIO + +import .Paths: ChannelRouter, RouteChannel, AutoChannelRouting, autoroute!, visualize_router_state + +# ── Helpers ────────────────────────────────────────────────────────────────── + +"Horizontal channel at height `y`, from `x0` to `x1`." +function hchannel(x0, x1, y; width=2.0) + pa = Path(Float64(x0), Float64(y)) + straight!(pa, Float64(x1 - x0), Paths.Trace(Float64(width))) + return pa +end + +"Vertical channel at `x`, from `y0` to `y1`." +function vchannel(x, y0, y1; width=2.0) + pa = Path(Float64(x), Float64(y0), α0=90°) + straight!(pa, Float64(y1 - y0), Paths.Trace(Float64(width))) + return pa +end + +"Diagonal channel from `(x0,y0)` at angle `α` for length `len`." +function dchannel(x0, y0, α, len; width=2.0) + pa = Path(Float64(x0), Float64(y0), α0=α) + straight!(pa, Float64(len), Paths.Trace(Float64(width))) + return pa +end + +"B-spline channel from `(x0,y0)` to `(x1, y1)` at angle `α` at endpoints." +function bchannel(x0, y0, α, x1, y1; width=2.0) + pa = Path(Float64(x0), Float64(y0), α0=α) + bspline!(pa, [Point(x1, y1)], α, Paths.Trace(Float64(width)), auto_speed=true, auto_curvature=true, endpoints_speed=1, endpoints_curvature=0) + return pa +end + +# Pin convenience: PointHook with in_direction pointing INWARD (away from routing) +# Left pin (route goes right): lpin(x, y) → in_direction = 180° +# Right pin (route goes left): rpin(x, y) → in_direction = 0° +# Bottom pin (route goes up): bpin(x, y) → in_direction = 270° +# Top pin (route goes down): tpin(x, y) → in_direction = 90° +lpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 180°) +rpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 0°) +bpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 270°) +tpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 90°) + +const R = Paths.StraightAnd90(0.1) +const WW = 0.05 + +# ── Example 1: Simple ──────────────────────────────────────────────────────── +# 1 horizontal channel, 2 pins, 1 net. +# Pins offset vertically so their rays cross the channel perpendicularly. +# +# p1 (below) p2 (above) +# ↑ ↓ +# ═══════════════════════ h1 (y=0) + +function example_simple() + channels = [hchannel(0, 10, 0)] + hooks = [ + bpin(2, -0.5), # pin 1: below channel, ray goes up + tpin(8, 0.5), # pin 2: above channel, ray goes down + ] + nets = [(1, 2)] + + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + autoroute!(ar, R) + c = visualize_router_state(ar; wire_width=WW) + + @assert length(ar.net_wires) == 1 + @assert length(ar.net_wires[1]) > 0 "Net should be routed" + @assert length(ar.channel_tracks[1]) == 1 "Should use exactly 1 track" + return c, ar +end + +# ── Example 2: Parallel ────────────────────────────────────────────────────── +# H/V grid, 3 nets routed left→right at matching heights. No crossings. +# +# p1 → ║══════════════║ ← p4 h_bot (y=0) +# ║ ║ +# p2 → ║══════════════║ ← p5 h_mid (y=4) +# ║ ║ +# p3 → ║══════════════║ ← p6 h_top (y=8) +# v_left v_right + +function example_parallel() + channels = [ + vchannel(0, -1, 9), # v_left + vchannel(10, -1, 9), # v_right + hchannel(-1, 11, 0), # h_bot + hchannel(-1, 11, 4), # h_mid + hchannel(-1, 11, 8), # h_top + ] + hooks = [ + lpin(-0.5, 0), # p1: left, at h_bot level + lpin(-0.5, 4), # p2: left, at h_mid level + lpin(-0.5, 8), # p3: left, at h_top level + rpin(10.5, 0), # p4: right, at h_bot level + rpin(10.5, 4), # p5: right, at h_mid level + rpin(10.5, 8), # p6: right, at h_top level + ] + nets = [(1, 4), (2, 5), (3, 6)] + + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + routes = autoroute!(ar, R) + paths = Path.(routes, Ref(Paths.Trace(WW))) + c = visualize_router_state(ar; wire_width=WW) + @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + return c, ar +end + +# ── Example 3: Crossing ────────────────────────────────────────────────────── +# 2 horizontal channels + 1 vertical channel. 2 nets cross in the shared +# vertical channel, forcing multiple tracks. +# +# p3 ← ═══════╪═══ → p4 h_top (y=6) +# ║ +# p1 ← ═══════╪═══ → p2 h_bot (y=0) +# v_mid (x=5) + +function example_crossing() + channels = [ + vchannel(5, -1, 7), # v_mid + hchannel(-1, 9, 0), # h_bot + hchannel(-1, 9, 6), # h_top + ] + hooks = [ + bpin(2, -0.5), # p1: below h_bot, left side + bpin(8, -0.5), # p2: below h_bot, right side + tpin(2, 6.5), # p3: above h_top, left side + tpin(8, 6.5), # p4: above h_top, right side + ] + # Crossed: bottom-left↔top-right, bottom-right↔top-left + nets = [(1, 4), (2, 3)] + + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + routes = autoroute!(ar, R) + paths = Path.(routes, Ref(Paths.Trace(WW))) + c = visualize_router_state(ar; wire_width=WW) + + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + # Both nets traverse v_mid, so it needs ≥2 tracks + @assert length(ar.channel_tracks[1]) >= 2 "Crossing nets need multiple tracks in shared channel" + # Due to assignment order, only one horizontal channel has two tracks + @assert length(ar.channel_tracks[2]) + length(ar.channel_tracks[3]) == 3 "Crossing nets need multiple horizontal tracks only when they overlap due to vertical assignment" + @assert length(Intersect.prepared_intersections(paths)) == 1 "Exactly one crossing" + return c, ar +end + +# ── Example 4: Fan-in/fan-out ───────────────────────────────────────────────── +# Left and right pins spread out, must fan in/out asymmetrically to horizontal channel +# 2 vertical + 1 horizontal channels. +# +# v_left v_right +# p4 → (-0.5, 6) ║ ║ (10.5, 9) ← p8 +# p3 → (-0.5, 5) ║════════════════════║ (10.5, 6) ← p7 h_mid (y=7) +# p2 → (-0.5, 4) ║ ║ (10.5, 3) ← p6 +# p1 → (-0.5, 3) ║ ║ (10.5, 0) ← p5 +function example_fanin_fanout() + channels = [ + vchannel(0, -2, 11), # v_left + hchannel(-2, 12, 7), # h_mid + vchannel(10, -2, 11), # v_right + ] + # Left pins clustered at y=3,4,5,6 — all cross v_left + # Right pins spread at y=0,3,6,9 — all cross v_right + hooks = [ + lpin(-1, 3), # p1 + lpin(-1, 4), # p2 + lpin(-1, 5), # p3 + lpin(-1, 6), # p4 + rpin(11, 0), # p5 + rpin(11, 3), # p6 + rpin(11, 6), # p7 + rpin(11, 9), # p8 + ] + nets = [(1, 5), (2, 6), (3, 7), (4, 8)] + + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + routes = autoroute!(ar, R) + paths = Path.(routes, Ref(Paths.Trace(WW))) + c = visualize_router_state(ar; wire_width=WW) + + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + # @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + # @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" + return c, ar +end + +# ── Example 5: Fan-out ─────────────────────────────────────────────────────── +# Left pins clustered (simulating component outputs), right pins spread out. +# 2 vertical + 4 horizontal channels. +# +# v_left v_right +# p4 → (-0.5, 6) ║════════════════════║ (10.5, 9) ← p8 h4 (y=9) +# p3 → (-0.5, 5) ║════════════════════║ (10.5, 6) ← p7 h3 (y=6) +# p2 → (-0.5, 4) ║════════════════════║ (10.5, 3) ← p6 h2 (y=3) +# p1 → (-0.5, 3) ║════════════════════║ (10.5, 0) ← p5 h1 (y=0) +function example_multichannel_fanout() + channels = [ + vchannel(0, -2, 11), # v_left + hchannel(-2, 12, 1.5), # h_1 + hchannel(-2, 12, 3.5), # h_2 + hchannel(-2, 12, 5.5), # h_3 + hchannel(-2, 12, 7.5), # h_4 + vchannel(10, -2, 11), # v_right + ] + # Left pins clustered at y=3,4,5,6 — all cross v_left + # Right pins spread at y=0,3,6,9 — all cross v_right + hooks = [ + lpin(-1, 3), # p1 + lpin(-1, 4), # p2 + lpin(-1, 5), # p3 + lpin(-1, 6), # p4 + rpin(11, 0), # p5 + rpin(11, 3), # p6 + rpin(11, 6), # p7 + rpin(11, 9), # p8 + ] + nets = [(1, 5), (2, 6), (3, 7), (4, 8)] + + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + routes = autoroute!(ar, R) + paths = Path.(routes, Ref(Paths.Trace(WW))) + c = visualize_router_state(ar; wire_width=WW) + + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + @assert all(length.(ar.channel_tracks[2:5]) .== 1) "Each net should use its nearest horizontal channel" + @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + return c, ar +end + +# ── Example 6: Grid ────────────────────────────────────────────────────────── +# 4×4 H/V grid. 3 nets connecting pins on different edges, requiring +# multi-channel paths through the grid. +# +# v1 v2 v3 v4 +# | | | | +# p5 → ════╪════╪════╪════╪════ ← p6 h4 (y=9) +# | | | | +# ════╪════╪════╪════╪════ h3 (y=6) +# | | | | +# p1 → ════╪════╪════╪════╪════ h2 (y=3) +# | | | | +# ════╪════╪════╪════╪════ h1 (y=0) +# | | | | +# p3 p4 +# (bottom) (bottom) + +function example_grid() + channels = [ + # Vertical channels (indices 1-4) + vchannel(0, -2, 11), + vchannel(3, -2, 11), + vchannel(6, -2, 11), + vchannel(9, -2, 11), + # Horizontal channels (indices 5-8) + hchannel(-2, 11, 0), + hchannel(-2, 11, 3), + hchannel(-2, 11, 6), + hchannel(-2, 11, 9), + ] + hooks = [ + lpin(-0.5, 3), # p1: left edge, at h2 level → adj to v1 + rpin(9.5, 6), # p2: right edge, at h3 level → adj to v4 + bpin(1.5, -0.5), # p3: bottom edge → adj to h1 + bpin(7.5, -0.5), # p4: bottom edge → adj to h1 + lpin(-0.5, 9), # p5: left edge, at h4 level → adj to v1 + rpin(9.5, 0), # p6: right edge, at h1 level → adj to v4 + ] + nets = [ + (1, 2), # left(y=3) → right(y=6): diagonal traverse + (3, 5), # bottom(x=1.5) → left(y=9): corner path + (4, 6), # bottom(x=7.5) → right(y=0): short path + ] + + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + autoroute!(ar, R) + c = visualize_router_state(ar; wire_width=WW) + + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + # At least one net should traverse 3+ wire segments (multi-hop path) + max_segs = maximum(length.(ar.net_wires)) + @assert max_segs >= 2 "Grid routing should produce multi-segment paths" + return c, ar +end + +# ── Example 7: Angled ──────────────────────────────────────────────────────── +# Non-Manhattan channels: two 45° diagonals crossing a horizontal channel. +# Demonstrates that the router handles arbitrary path geometry. +# +# ╲ ╱ +# ╲ ╱ +# ╲ ╱ +# ╳ +# ═══════════╱═╲═══════════ h1 (y=3) +# ╱ ╲ +# d1 d2 + +function example_angled() + channels = [ + dchannel(-1, 0, 45°, 10*sqrt(2); width=2.0), # d1: NE from (-1,0) + hchannel(-2, 12, 3; width=2.0), # h1 (y=3) + dchannel(-1, 10, -45°, 10*sqrt(2); width=2.0), # d2: SE from (-1,10) + ] + # Pins offset so rays cross diagonal + hooks = [ + bpin(0, -2), # p1: below d1, left side + bpin(8, -2), # p2: below d2, right side + rpin(12, 1), # p4: above d2, right side + lpin(-2, 1), # p3: below d1, left side + ] + # Net 1 goes left→right via h1 and diagonals + # Net 2 goes right→left via h1 and diagonals + nets = [(1, 2), (3, 4)] + + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + routes = autoroute!(ar, R) + paths = Path.(routes, Ref(Paths.Trace(WW))) + c = visualize_router_state(ar; wire_width=WW, rule=StraightAnd45(0.1)) + + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + @assert all(length.(ar.channel_tracks) .== 2) "Each channel needs two tracks" + @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + return c, ar +end + +# ── Example 8: Dense ───────────────────────────────────────────────────────── +# 6 nets sharing just 2 horizontal + 2 vertical channels. +# Forces multiple tracks per channel. +# +# p1-p6 on left p7-p12 on right +# → ║════════════════║ ← +# ║ h1 (y=0) ║ +# ║════════════════║ +# ║ h2 (y=5) ║ +# → ║════════════════║ ← +# v_left v_right + +function example_dense() + channels = [ + vchannel(0, -2, 7), # v_left (idx 1) + hchannel(-2, 10, -1), # h1 (idx 3) + hchannel(-2, 10, 6), # h2 (idx 4) + vchannel(8, -2, 7), # v_right (idx 2) + ] + # 6 left pins, tightly spaced, all crossing v_left + # 6 right pins, same y-positions, all crossing v_right + ys = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + hooks = [ + [lpin(-1, y) for y in ys]..., # p1-p6 + [rpin(9, y) for y in ys]..., # p7-p12 + ] + nets = [(i, i + 6) for i in 1:6] + + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + routes = autoroute!(ar, R) + paths = Path.(routes, Ref(Paths.Trace(WW))) + c = visualize_router_state(ar; wire_width=WW) + + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + @assert all(length.(ar.channel_tracks) .== 3) "Requires 3 tracks on each channel" + @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + return c, ar +end + +# ── Example 9: Fan-in/fan-out with B-splines ────────────────────────────────── +# Left and right pins spread out, must fan in/out asymmetrically to horizontal channel +# 2 quasi-vertical + 1 quasi-horizontal channels. +# +# v_left v_right +# p4 → (-0.5, 6) ║ ║ (10.5, 9) ← p8 h4 (y=9) +# p3 → (-0.5, 5) ║════════════════════║ (10.5, 6) ← p7 h3 (y=6) +# p2 → (-0.5, 4) ║ ║ (10.5, 3) ← p6 h2 (y=3) +# p1 → (-0.5, 3) ║ ║ (10.5, 0) ← p5 h1 (y=0) +function example_bspline() + channels = [ + bchannel(0, -2, 30°, 1, 12), # v_left + bchannel(-1, 2, -30°, 12, 9), # h_mid + bchannel(10, -2, 30°, 11, 12), # v_right + ] + # Left pins clustered at y=3,4,5,6 — all cross v_left + # Right pins spread at y=0,3,6,9 — all cross v_right + hooks = [ + lpin(-3, 3), # p1 + lpin(-3, 4), # p2 + lpin(-3, 5), # p3 + lpin(-3, 6), # p4 + rpin(14, 0), # p5 + rpin(14, 3), # p6 + rpin(14, 6), # p7 + rpin(14, 9), # p8 + ] + nets = [(1, 5), (2, 6), (3, 7), (4, 8)] + + ar = Paths.ChannelRouter(nets, hooks, RouteChannel.(channels)) + transition_rule = Paths.BSplineRouting(auto_speed=true, auto_curvature=true, endpoints_speed=1, endpoints_curvature=0) + autoroute!(ar, transition_rule) + rule = Paths.AutoChannelRouting( + ar.channels, + transition_rule, + 0.2, + ar + ) + c = visualize_router_state(ar; wire_width=WW, rule=transition_rule) + for i in 1:4 + pa = path_out(hooks[i]) + ar.net_paths[i] = pa + pa = ar.net_paths[i] + route!(pa, hooks[i+4].p, hooks[i+4].in_direction, rule, Paths.Trace(WW)) + render!(c, pa, GDSMeta(1)) + end + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + # prepared_intersections doesn't work (trying to differentiate through adapted_grid) + # @assert isempty(Intersect.prepared_intersections(ar.net_paths)) "No crossings" + # @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" + return c, ar +end + + +# ── Example 10: Schematic integration ──────────────────────────────────────── +# TODO: AutoChannelRouting with schematic workflow. +# Requires components with hooks, route! calls, and schematic compilation. +# Placeholder for now. + +# function example_schematic() +# ... +# end + +# ── Assembly ───────────────────────────────────────────────────────────────── + +function run_all_examples(; save_gds=true) + examples = [ + "simple" => example_simple, + "parallel" => example_parallel, + "crossing" => example_crossing, + "fanout" => example_fanout, + "grid" => example_grid, + "angled" => example_angled, + "dense" => example_dense, + ] + results = Pair{String, Tuple{Cell, ChannelRouter}}[] + for (name, fn) in examples + @info "Running $name..." + push!(results, name => fn()) + end + if save_gds + for (name, (c, _)) in results + save("autoroute_$name.gds", c; spec_warnings=false) + end + @info "Saved $(length(results)) GDS files" + end + return results +end From d5a43187d2c93d50c5848684852fe6e5ab0e64e6 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 3 Apr 2026 16:14:31 +0200 Subject: [PATCH 09/32] Fix examples --- src/paths/channel_autorouter.jl | 147 +++++++++++++++++--------------- src/paths/channels.jl | 10 +-- test/autoroute_examples.jl | 53 +++++------- 3 files changed, 105 insertions(+), 105 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 77bc9d700..bd0a1ea21 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -81,6 +81,7 @@ struct ChannelRouter{T <: Coordinate} channel_tracks::Vector{Vector{Track}} # Waypoints for each segment (used for visualizing router state) segment_waypoints::Dict{TrackWireSegment, PointHook{T}} + net_routes::Vector{Route{T}} net_paths::Vector{Path{T}} # [Internals] Persistent path objects to populate after routing end @@ -115,6 +116,7 @@ function ChannelRouter( channel_segments, channel_tracks, segment_waypoints, + Route{T}[], [Path{T}() for net in nets] ) end @@ -133,6 +135,7 @@ function ChannelRouter(channels::Vector{RouteChannel{T}}) where {T} channel_segments, channel_tracks, segment_waypoints, + Route{T}[], Path{T}[] ) end @@ -403,8 +406,21 @@ function interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=true) s_start = pathlength_at_intersection(ar, start_channel, channel_idx) s_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) pt, nt = prev_next_tendency(ar, ws) - return _swap(s1 + pt*segment_offset(ar, prev(ar, ws), s_start), - s2 - nt*segment_offset(ar, next(ar, ws), s_stop)) + start_dir = direction_at_intersection(ar, start_channel, channel_idx) + dir1 = direction_at_intersection(ar, channel_idx, start_channel) + dir2 = direction_at_intersection(ar, channel_idx, stop_channel) + stop_dir = direction_at_intersection(ar, stop_channel, channel_idx) + # Offset also depends neighbor track offsets + α_ixn_start = (dir1 - start_dir) + α_ixn_stop = (stop_dir - dir2) + prev_offset_proj = segment_offset(ar, prev(ar, ws), s_start; use_wire_direction=false) / sin(α_ixn_start) + next_offset_proj = segment_offset(ar, next(ar, ws), s_stop; use_wire_direction=false) / sin(α_ixn_stop) + # Offset *also* depends on this wire segment's offset at a non-90° intersection + start_offset_proj = -segment_offset(ar, ws, s_start; use_wire_direction=false) / tan(α_ixn_start) + stop_offset_proj = -segment_offset(ar, ws, s_stop; use_wire_direction=false) / tan(α_ixn_stop) + # This is approximate on bending or tapered tracks + return _swap(s1 + (prev_offset_proj + start_offset_proj), + s2 - (next_offset_proj + stop_offset_proj)) end """ @@ -923,39 +939,19 @@ end make_routes!(ar::ChannelRouter, rule; net_indices=eachindex(ar.net_pins)) Using channel and track assignments, create `Route`s for the nets in `net_indices`. - -A point and direction is calculated for each `wire_segment`. These are assigned to -`ar.segment_waypoints[wire_segment]` (if not already assigned) and then added as a -waypoint/way-direction in the output `Route`. """ function make_routes!( ar::ChannelRouter{T}, - rule; - net_indices=eachindex(ar.net_pins) + rule ) where {T} - routes = Route{T}[] - for (idx_net, net_segs) in zip(net_indices, ar.net_wires[net_indices]) - waydirs = typeof(1.0°)[] - waypoints = Point{T}[] - for seg in net_segs - if haskey(ar.segment_waypoints, seg) - wp = segment_waypoint(ar, seg).p - wd = segment_waypoint(ar, seg).in_direction - push!(waypoints, wp) - push!(waydirs, wd) - else - wp, wd = segment_midpoint(ar, seg), segment_mid_direction(ar, seg) - push!(waypoints, wp) - push!(waydirs, wd) - ar.segment_waypoints[seg] = PointHook(wp, wd) - end - end + empty!(ar.net_routes) + for idx_net in eachindex(ar.net_pins) p0, p1 = pin_coordinates.(ar, net_pins(ar, idx_net)) α0, α1 = pin_direction.(ar, net_pins(ar, idx_net)) - rt = Route(rule, p0, p1, α0, α1 + pi, waypoints=waypoints, waydirs=waydirs) - push!(routes, rt) + rt = Route(rule, p0, p1, α0, α1 + pi) + push!(ar.net_routes, rt) end - return routes + return ar.net_routes end function _delete_segment!(ar, ws; reset_tracks=true, from_net=true) @@ -1020,14 +1016,21 @@ routed from its source pin, through channels 2, 4, 1, 5 in order, then to its de """ function autoroute!( ar::ChannelRouter, - rule; + transition_rule, + margin; net_indices=eachindex(ar.net_pins), fixed_channel_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}() ) reset_nets!(ar, net_indices=net_indices) assign_channels!(ar; net_indices=net_indices, fixed_paths=fixed_channel_paths) assign_tracks!(ar) - return make_routes!(ar, rule; net_indices=net_indices) + rule = Paths.AutoChannelRouting( + ar.channels, + transition_rule, + margin, + ar + ) + return make_routes!(ar, rule) end ######## Modification @@ -1066,46 +1069,48 @@ Segment waypoints are marked with circles and numbered sequentially within each """ function visualize_router_state( ar::ChannelRouter{T}; - wire_width=0.1*oneunit(T), - rule=StraightAnd90(min_bend_radius=wire_width, max_bend_radius=wire_width) + wire_width=0.1*oneunit(T) ) where {T} c = DeviceLayout.Cell{T}("track_viz") - rts = make_routes!(ar, rule) - paths = [Path(rt, Paths.Trace(wire_width)) for rt in rts] - - DeviceLayout.render!.(c, paths, GDSMeta()) - rect = track_rectangles(ar) - # DeviceLayout.render!.(c, rect, GDSMeta(2)) + paths = Path.(ar.net_routes, Ref(Paths.Trace(wire_width))) + DeviceLayout.render!.(c, paths, GDSMeta(5)) channels = channel_paths(ar) DeviceLayout.render!.(c, channels, GDSMeta(3)) - lab = track_labels(ar) - [DeviceLayout.text!(c, l..., GDSMeta(4)) for l in lab] - [DeviceLayout.render!(c, DeviceLayout.Circle(wire_width) + p, GDSMeta(5)) for rt in rts for p in rt.waypoints] + tracks = track_paths(ar) + DeviceLayout.render!.(c, tracks, GDSMeta(2)) + trlab = track_labels(ar, tracks) + DeviceLayout.text!.(c, trlab, GDSMeta(4)) + for pa in paths + for node in pa[1:end-1] + DeviceLayout.render!(c, DeviceLayout.Circle(1.5wire_width) + p1(node.seg), GDSMeta(5)) + end + end plab = pin_labels(ar) - [DeviceLayout.text!(c, l..., GDSMeta(6)) for l in plab] - wlab = waypoint_labels(ar) - [ - DeviceLayout.text!(c, l..., GDSMeta(8), xalign=DeviceLayout.Align.XCenter(), yalign=DeviceLayout.Align.YCenter()) - for l in wlab - ] + for l in plab + DeviceLayout.text!(c, l..., GDSMeta(6)) + end return c end -function track_rectangles(ar::ChannelRouter) +function track_paths(ar::ChannelRouter) return [ - track_rectangle(ar, s, c) for s = 1:num_channels(ar) for c = 1:num_tracks(ar, s) + track_path(ar, s, c) for s = 1:num_channels(ar) for c = 1:num_tracks(ar, s) ] end channel_paths(ar::ChannelRouter) = [channel_path(ar, s) for s = 1:num_channels(ar)] -function track_labels(ar::ChannelRouter) +function track_labels(ar::ChannelRouter{T}, tracks) where {T} return [ - ( - "$s:$c", - p0(track_rectangle(ar, s, c)) - ) for s = 1:num_channels(ar) for c = 1:num_tracks(ar, s) + DeviceLayout.Texts.Text( + track.name, + p0(track), + width=width(track[1].sty, zero(T)), + rot=α0(track), + xalign=DeviceLayout.Align.LeftEdge(), + yalign=DeviceLayout.Align.YCenter() + ) for track in tracks ] end @@ -1126,12 +1131,12 @@ function channel_path(ar::ChannelRouter, channel_idx) return ar.channels[channel_idx].path end -function track_rectangle(ar::ChannelRouter{T}, channel_idx, track_idx) where {T} - off = track_offset(ar, channel_idx, track_idx, zero(T)) - seg = Paths.offset(ar.channels[channel_idx].node.seg, off) +function track_path(ar::ChannelRouter{T}, channel_idx, track_idx) where {T} n_tracks = length(channel_tracks(ar, channel_idx)) + seg = track_path_segment(n_tracks, + ar.channels[channel_idx].node, track_idx) w = Paths.width(ar.channels[channel_idx].node.sty, zero(T)) - pa = Path([Paths.Node(seg, Paths.Trace(w / (n_tracks+1)))]) + pa = Path([Paths.Node(seg, Paths.Trace(0.9*w / (n_tracks+1)))]; name="$channel_idx:$track_idx") return pa end @@ -1155,26 +1160,32 @@ function track_path_segment(ar::ChannelRouter{T}, ch::RouteChannel, pa::Path; ma # Assume there is exactly one wire segment belonging to this path in the channel # Channel node might have been converted to store in router, so just check start point/direction channel_idx = findfirst(chn -> p0(chn.node.seg) ≈ p0(ch.path) && α0(chn.node.seg) == α0(ch.path), ar.channels) - net_idx = findfirst(p -> p === pa, ar.net_paths) + net_idx = findfirst(pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), ar.pins[first.(ar.net_pins)]) wireseg_idx = findfirst(ws -> running_channel(ws) == channel_idx, net_wire(ar, net_idx)) wireseg = net_wire(ar, net_idx)[wireseg_idx] track_idx = segment_track(ar, wireseg) # Get the starting and ending pathlengths - start_channel, stop_channel = bounding_channels(wireseg) - wireseg_start = pathlength_at_intersection(ar, channel_idx, start_channel) - wireseg_stop = pathlength_at_intersection(ar, channel_idx, stop_channel) - prev_width = width_at_intersection(ar, start_channel, channel_idx) - next_width = width_at_intersection(ar, stop_channel, channel_idx) + # Not accounting for track offsets + # start_channel, stop_channel = bounding_channels(wireseg) + # wireseg_start = pathlength_at_intersection(ar, channel_idx, start_channel) + # wireseg_stop = pathlength_at_intersection(ar, channel_idx, stop_channel) + # Account for track offsets + s1, s2 = interval(ar, wireseg) + wireseg_start = against_channel(ar, wireseg) ? s2 : s1 + wireseg_stop = against_channel(ar, wireseg) ? s1 : s2 + + prev_width = zero(T)#width_at_intersection(ar, start_channel, channel_idx) + next_width = zero(T)#width_at_intersection(ar, stop_channel, channel_idx) channel_section = segment_channel_section(ch, wireseg_start, wireseg_stop, prev_width, next_width; margin) # Return channel section segment offset by width according to track return track_path_segment(length(channel_tracks(ar, channel_idx)), - channel_section, track_idx; reversed=wireseg_start > wireseg_stop) + channel_section, track_idx; reversed=against_channel(ar, wireseg)) end -function channels_taken(r::ChannelRouter, pa::Path) - net_idx = findfirst(p -> p === pa, r.net_paths) - return [running_channel(wireseg) for wireseg in net_wire(r, net_idx)] +function channels_taken(ar::ChannelRouter, pa::Path) + net_idx = findfirst(pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), ar.pins[first.(ar.net_pins)]) + return [running_channel(wireseg) for wireseg in net_wire(ar, net_idx)] end function _update_with_graph!(rule::AutoChannelRouting, route_node, graph; kwargs...) diff --git a/src/paths/channels.jl b/src/paths/channels.jl index 4d2494133..df943c49f 100644 --- a/src/paths/channels.jl +++ b/src/paths/channels.jl @@ -48,8 +48,8 @@ function segment_channel_section( channel_section = split( ch.node, [ - wireseg_start + margin + prev_width / 2, - wireseg_stop - margin - next_width / 2 + max(zero(T), wireseg_start + margin + prev_width / 2), + min(pathlength(ch.node.seg), wireseg_stop - margin - next_width / 2) ] )[2] elseif d < zero(d) # segment is counter to channel direction @@ -57,8 +57,8 @@ function segment_channel_section( split( ch.node, [ - wireseg_stop + margin + next_width / 2, - wireseg_start - margin - prev_width / 2 + max(zero(T), wireseg_stop + margin + next_width / 2,) + min(pathlength(ch.node.seg), wireseg_start - margin - prev_width / 2) ] )[2] ) @@ -177,7 +177,7 @@ function _route!( sty; waypoints ) - push!(p, Node(resolve_offset(track_path_seg), sty), reconcile=false) # p0, α0 reconciled by construction + push!(p, Node(resolve_offset(track_path_seg), sty)) p[end - 1].next = p[end] p[end].prev = p[end - 1] # Note `auto_curvature` BSpline uses curvature from end of previous segment diff --git a/test/autoroute_examples.jl b/test/autoroute_examples.jl index de2be393a..06204147d 100644 --- a/test/autoroute_examples.jl +++ b/test/autoroute_examples.jl @@ -45,6 +45,7 @@ tpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 90°) const R = Paths.StraightAnd90(0.1) const WW = 0.05 +const MARGIN = 0.1 # ── Example 1: Simple ──────────────────────────────────────────────────────── # 1 horizontal channel, 2 pins, 1 net. @@ -63,7 +64,7 @@ function example_simple() nets = [(1, 2)] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) - autoroute!(ar, R) + autoroute!(ar, R, MARGIN) c = visualize_router_state(ar; wire_width=WW) @assert length(ar.net_wires) == 1 @@ -101,7 +102,7 @@ function example_parallel() nets = [(1, 4), (2, 5), (3, 6)] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) - routes = autoroute!(ar, R) + routes = autoroute!(ar, R, MARGIN) paths = Path.(routes, Ref(Paths.Trace(WW))) c = visualize_router_state(ar; wire_width=WW) @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" @@ -134,7 +135,7 @@ function example_crossing() nets = [(1, 4), (2, 3)] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) - routes = autoroute!(ar, R) + routes = autoroute!(ar, R, MARGIN) paths = Path.(routes, Ref(Paths.Trace(WW))) c = visualize_router_state(ar; wire_width=WW) @@ -177,13 +178,14 @@ function example_fanin_fanout() nets = [(1, 5), (2, 6), (3, 7), (4, 8)] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) - routes = autoroute!(ar, R) - paths = Path.(routes, Ref(Paths.Trace(WW))) + routes = autoroute!(ar, R, MARGIN) c = visualize_router_state(ar; wire_width=WW) @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" - # @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" - # @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" + paths = Path.(routes, Ref(Paths.Trace(WW))) + @show Intersect.prepared_intersections(paths) + @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" return c, ar end @@ -220,7 +222,7 @@ function example_multichannel_fanout() nets = [(1, 5), (2, 6), (3, 7), (4, 8)] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) - routes = autoroute!(ar, R) + routes = autoroute!(ar, R, MARGIN) paths = Path.(routes, Ref(Paths.Trace(WW))) c = visualize_router_state(ar; wire_width=WW) @@ -275,7 +277,7 @@ function example_grid() ] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) - autoroute!(ar, R) + autoroute!(ar, R, MARGIN) c = visualize_router_state(ar; wire_width=WW) @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" @@ -315,12 +317,12 @@ function example_angled() nets = [(1, 2), (3, 4)] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) - routes = autoroute!(ar, R) - paths = Path.(routes, Ref(Paths.Trace(WW))) - c = visualize_router_state(ar; wire_width=WW, rule=StraightAnd45(0.1)) + routes = autoroute!(ar, Paths.StraightAnd45(0.1), MARGIN) + c = visualize_router_state(ar; wire_width=WW) @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" @assert all(length.(ar.channel_tracks) .== 2) "Each channel needs two tracks" + paths = Path.(routes, Ref(Paths.Trace(WW))) @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" return c, ar end @@ -354,7 +356,7 @@ function example_dense() nets = [(i, i + 6) for i in 1:6] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) - routes = autoroute!(ar, R) + routes = autoroute!(ar, R, MARGIN) paths = Path.(routes, Ref(Paths.Trace(WW))) c = visualize_router_state(ar; wire_width=WW) @@ -395,29 +397,16 @@ function example_bspline() ar = Paths.ChannelRouter(nets, hooks, RouteChannel.(channels)) transition_rule = Paths.BSplineRouting(auto_speed=true, auto_curvature=true, endpoints_speed=1, endpoints_curvature=0) - autoroute!(ar, transition_rule) - rule = Paths.AutoChannelRouting( - ar.channels, - transition_rule, - 0.2, - ar - ) - c = visualize_router_state(ar; wire_width=WW, rule=transition_rule) - for i in 1:4 - pa = path_out(hooks[i]) - ar.net_paths[i] = pa - pa = ar.net_paths[i] - route!(pa, hooks[i+4].p, hooks[i+4].in_direction, rule, Paths.Trace(WW)) - render!(c, pa, GDSMeta(1)) - end + + routes = autoroute!(ar, transition_rule, 1.0) + c = visualize_router_state(ar, wire_width=WW) @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" - # prepared_intersections doesn't work (trying to differentiate through adapted_grid) - # @assert isempty(Intersect.prepared_intersections(ar.net_paths)) "No crossings" - # @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" + paths = Path.(routes, Ref(Paths.Trace(WW))) + @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" return c, ar end - # ── Example 10: Schematic integration ──────────────────────────────────────── # TODO: AutoChannelRouting with schematic workflow. # Requires components with hooks, route! calls, and schematic compilation. From 230a6f0d834605cda74405db26c624abe590479b Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 3 Apr 2026 17:02:00 +0200 Subject: [PATCH 10/32] Fix incorrect against_channel after track assignment --- src/paths/channel_autorouter.jl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index bd0a1ea21..23c7d0438 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -393,6 +393,10 @@ taken into account. Otherwise, the start and stop are at the centre line of the The interval is always a tuple with the lower bound as the first element. """ function interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=true) + return _swap(unsorted_interval(ar, ws; use_track)...) +end + +function unsorted_interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=true) start_channel, stop_channel = bounding_channels(ws) channel_idx = running_channel(ws) @@ -419,8 +423,8 @@ function interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=true) start_offset_proj = -segment_offset(ar, ws, s_start; use_wire_direction=false) / tan(α_ixn_start) stop_offset_proj = -segment_offset(ar, ws, s_stop; use_wire_direction=false) / tan(α_ixn_stop) # This is approximate on bending or tapered tracks - return _swap(s1 + (prev_offset_proj + start_offset_proj), - s2 - (next_offset_proj + stop_offset_proj)) + return s1 + (prev_offset_proj + start_offset_proj), + s2 - (next_offset_proj + stop_offset_proj) end """ @@ -664,7 +668,7 @@ function assign_tracks_matching!(ar, channel) push!(group, v) merged_groups[v] = group # Replace match with v as representative of merged group in L - pop!(L, matching[v]) # match must have been in L + matching[v] in L && pop!(L, matching[v]) # match must have been in L end v in R && pop!(R, v) end @@ -832,8 +836,9 @@ end function against_channel(ar, wireseg) channel_idx = running_channel(wireseg) start_channel, stop_channel = bounding_channels(wireseg) - s1 = pathlength_at_intersection(ar, channel_idx, start_channel) - s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) + s1, s2 = unsorted_interval(ar, wireseg) + # s1 = pathlength_at_intersection(ar, channel_idx, start_channel) + # s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) return s1 > s2 end From 2ccf7f0eae2260cbf4bff3099789d80ce2fda76d Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 3 Apr 2026 17:15:03 +0200 Subject: [PATCH 11/32] Fix set mutation during iteration --- src/paths/channel_autorouter.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 23c7d0438..4d7074be8 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -655,7 +655,7 @@ function assign_tracks_matching!(ar, channel) for (zone, nextzone) in zip(zones, [zones[2:end]..., Int[]]) # Extra empty zone at end # Merge nets terminating in current zone to left side if length(zones) > 1 - for v in active # v was in nextzone last round + for v in collect(active) # v was in nextzone last round if !(v in nextzone) # v terminates in current zone pop!(active, v) # no longer active push!(L, v) # add to left side From 9a5b77af50bb22adbbe74bd5f596752595bba199 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 3 Apr 2026 22:08:30 +0200 Subject: [PATCH 12/32] Fix iterator mutation bug, improve performance --- src/paths/channel_autorouter.jl | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 4d7074be8..b3b815759 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -14,7 +14,6 @@ import Graphs: inneighbors, neighbors, outneighbors, - dag_longest_path, maximal_cliques, yen_k_shortest_paths, dijkstra_shortest_paths, @@ -261,7 +260,7 @@ function build_channel_graph(pins, channels, T) end else # record intersection as edge in channel graph, with info in dict haskey(intersection_dict, (v1_idx, v2_idx)) && - error("Spaces $v1_idx and $v2_idx have multiple intersections") + error("Channels $v1_idx and $v2_idx have multiple intersections") dir1 = direction(channels[v1_idx][node1_idx].seg, s1) dir2 = direction(channels[v2_idx][node2_idx].seg, s2) s1 = pathlength_from_start(channels[v1_idx], node1_idx, s1) @@ -668,7 +667,7 @@ function assign_tracks_matching!(ar, channel) push!(group, v) merged_groups[v] = group # Replace match with v as representative of merged group in L - matching[v] in L && pop!(L, matching[v]) # match must have been in L + pop!(L, matching[v]) # match must have been in L end v in R && pop!(R, v) end @@ -682,16 +681,15 @@ function assign_tracks_matching!(ar, channel) end end # Remove all edges in merging graph, start fresh this iteration - for edge in edges(merging_graph) - rem_edge!(merging_graph, edge) - end + merging_graph = SimpleGraph(length(wiresegs_ascending)) # Add edges between left and right when they can be merged for l in L # Only use rightmost in any merged group haskey(merged_into, l) && continue for r in R if !segments_overlap(ar, wiresegs_ascending[l], wiresegs_ascending[r]) - mergeable = isempty(yen_k_shortest_paths(vcg, l, r).paths) && isempty(yen_k_shortest_paths(vcg, r, l).paths) + mergeable = dijkstra_shortest_paths(vcg, [l]).dists[r] == typemax(Int) && + dijkstra_shortest_paths(vcg, [r]).dists[l] == typemax(Int) mergeable && add_edge!(merging_graph, l, r) end end @@ -1035,7 +1033,8 @@ function autoroute!( margin, ar ) - return make_routes!(ar, rule) + routes = make_routes!(ar, rule) + return routes end ######## Modification From 328eb040bc8dfb51570bdf798a35cc4ee504aa18 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Sun, 5 Apr 2026 14:52:50 +0200 Subject: [PATCH 13/32] Find VCG shortest paths more efficiently --- src/paths/channel_autorouter.jl | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index b3b815759..429ec2e6f 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -683,14 +683,18 @@ function assign_tracks_matching!(ar, channel) # Remove all edges in merging graph, start fresh this iteration merging_graph = SimpleGraph(length(wiresegs_ascending)) # Add edges between left and right when they can be merged + high_to_low = topological_sort_by_dfs(vcg) + dists_from_r = Dict(r => dag_shortest_paths(vcg, high_to_low, r) for r in R) for l in L # Only use rightmost in any merged group haskey(merged_into, l) && continue + dists_from_l = dag_shortest_paths(vcg, high_to_low, l) for r in R + mergeable = dists_from_l[r] >= nv(vcg) && + dists_from_r[r][l] >= nv(vcg) + !mergeable && continue if !segments_overlap(ar, wiresegs_ascending[l], wiresegs_ascending[r]) - mergeable = dijkstra_shortest_paths(vcg, [l]).dists[r] == typemax(Int) && - dijkstra_shortest_paths(vcg, [r]).dists[l] == typemax(Int) - mergeable && add_edge!(merging_graph, l, r) + add_edge!(merging_graph, l, r) end end end @@ -831,6 +835,22 @@ function merge_segments!(zone_ig, vcg, ws1, ws2) return merge_vertices(vcg, [ws1, ws2]) # No in-place for directed graph end +function dag_shortest_paths(dag, v_sorted, s) + # p = zeros(Int, length(v_sorted)) + d = fill(nv(dag), nv(dag)) + d[s] = 0 + for i in findfirst(v -> v == s, v_sorted):nv(dag) + u = v_sorted[i] + for v in outneighbors(dag, u) + if d[v] > d[u] + 1 + d[v] = d[u] + 1 + # p[v] = u + end + end + end + return d +end + function against_channel(ar, wireseg) channel_idx = running_channel(wireseg) start_channel, stop_channel = bounding_channels(wireseg) From be1bd4ed727a330128f770db3c736c33c00d5f84 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Wed, 15 Apr 2026 15:44:43 +0200 Subject: [PATCH 14/32] Cleanup --- src/paths/channel_autorouter.jl | 113 +++----------------------------- test/autoroute_examples.jl | 29 ++++++++ test/test_channels.jl | 28 -------- 3 files changed, 39 insertions(+), 131 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 429ec2e6f..f357c55c3 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -15,11 +15,9 @@ import Graphs: neighbors, outneighbors, maximal_cliques, - yen_k_shortest_paths, dijkstra_shortest_paths, enumerate_paths, topological_sort_by_dfs -import LinearAlgebra: norm import SparseArrays: sparse import BipartiteMatching @@ -192,15 +190,6 @@ function direction_at_intersection(ar::ChannelRouter, return ixn_info[4] end -function width_at_intersection(ar::ChannelRouter{T}, - running_channel, - intersecting_channel) where {T} - - ixn_info = channel_intersection(ar, running_channel, intersecting_channel) - running_channel < intersecting_channel && return channel_width(ar, running_channel, ixn_info[1]) - return channel_width(ar, running_channel, ixn_info[2]) -end - segment_waypoint(ar::ChannelRouter, ws::TrackWireSegment) = ar.segment_waypoints[ws] _swap(x, y) = (y > x ? (x, y) : (y, x)) @@ -318,30 +307,6 @@ function segment_track(ar::ChannelRouter, ws::TrackWireSegment) return track_idx end -""" - segment_midpoint(ar::ChannelRouter, ws::TrackWireSegment) - -The midpoint of the segment `ws` between `bounding_channels(ws)`. - -If `ws` has been assigned a track, uses the segment along that track. -""" -function segment_midpoint(ar::ChannelRouter{T}, ws::TrackWireSegment) where {T} - channel_idx = running_channel(ws) - s0, s1 = interval(ar, ws) - s = (s0+s1)/2 - if is_pin(ar, channel_idx) - dir = pin_direction(ar, graphidx_to_pin(ar, channel_idx)) - return pin_coordinates(ar, graphidx_to_pin(ar, channel_idx)) + - s * Point(cos(dir), sin(dir)) - end - channel_midpoint = channel_coordinates(ar, channel_idx, s) - - offset_distance = segment_offset(ar, ws; use_wire_direction=false) - - channel_dir = channel_direction(ar, channel_idx, s) - return channel_midpoint + offset_distance * Point(-sin(channel_dir), cos(channel_dir)) -end - """ segment_direction(ar::ChannelRouter, ws::TrackWireSegment) @@ -353,11 +318,6 @@ function segment_direction(ar::ChannelRouter, ws::TrackWireSegment, s) return direction(seg, s) end -function segment_mid_direction(ar::ChannelRouter, ws::TrackWireSegment) - s0, s1 = interval(ar, ws) - return segment_direction(ar, ws, (s0+s1)/2) -end - function segment_offset(ar::ChannelRouter{T}, ws::TrackWireSegment, s...; use_wire_direction=true) where {T} channel_idx = running_channel(ws) is_pin(ar, channel_idx) && return zero(T) @@ -368,19 +328,6 @@ function segment_offset(ar::ChannelRouter{T}, ws::TrackWireSegment, s...; use_wi Paths.width(ar.channels[channel_idx].node.sty, s...), track_idx; reversed) end -""" - track_offset(ar::ChannelRouter, channel_idx, track_idx, s) - -The offset of the centerline of track `track_idx` in channel `channel_idx`, -measured at pathlength `s` in the channel. -""" -function track_offset(ar::ChannelRouter{T}, channel_idx, track_idx, s...) where {T} - n_tracks = length(channel_tracks(ar, channel_idx)) - w = Paths.width(ar.channels[channel_idx].node.sty, zero(T)) - spacing = w / (n_tracks + 1) - return spacing * ((1 + n_tracks) / 2 - track_idx) -end - """ interval(ar::ChannelRouter, ws::TrackWireSegment) @@ -516,11 +463,6 @@ function build_auxiliary_graph(ar::ChannelRouter{T}) where {T} end """ - shortest_path_between_pins(ar::ChannelRouter, pin_1::Int, pin_2::Int) - -A shortest path in the router's channel graph from `pin_1` to `pin_2`, minimizing -hop count (number of channel transitions). - shortest_path_between_pins(ar::ChannelRouter, pin_1::Int, pin_2::Int, aux::AuxiliaryGraph) A shortest path minimizing physical distance (sum of arclengths along channels between @@ -529,15 +471,6 @@ intersection points), using a precomputed [`AuxiliaryGraph`](@ref). In both cases, the returned path is a list of vertex indices `[pin_gidx, ch1, ..., chN, pin_gidx]`. """ -function shortest_path_between_pins(ar::ChannelRouter, p0::Int, p1::Int) - ys = yen_k_shortest_paths( - channel_graph(ar), - pin_to_graphidx(ar, p0), - pin_to_graphidx(ar, p1) - ) - return ys.paths[1] -end - function shortest_path_between_pins(ar::ChannelRouter, p0::Int, p1::Int, aux::AuxiliaryGraph) pin0_gidx = pin_to_graphidx(ar, p0) pin1_gidx = pin_to_graphidx(ar, p1) @@ -830,13 +763,7 @@ function best_matching!(merging_graph, vcg) ) end -function merge_segments!(zone_ig, vcg, ws1, ws2) - merge_vertices!(zone_ig, [ws1, ws2]) - return merge_vertices(vcg, [ws1, ws2]) # No in-place for directed graph -end - function dag_shortest_paths(dag, v_sorted, s) - # p = zeros(Int, length(v_sorted)) d = fill(nv(dag), nv(dag)) d[s] = 0 for i in findfirst(v -> v == s, v_sorted):nv(dag) @@ -844,7 +771,6 @@ function dag_shortest_paths(dag, v_sorted, s) for v in outneighbors(dag, u) if d[v] > d[u] + 1 d[v] = d[u] + 1 - # p[v] = u end end end @@ -852,11 +778,7 @@ function dag_shortest_paths(dag, v_sorted, s) end function against_channel(ar, wireseg) - channel_idx = running_channel(wireseg) - start_channel, stop_channel = bounding_channels(wireseg) s1, s2 = unsorted_interval(ar, wireseg) - # s1 = pathlength_at_intersection(ar, channel_idx, start_channel) - # s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) return s1 > s2 end @@ -1027,7 +949,7 @@ function reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) end """ - autoroute!(ar::ChannelRouter, rule; net_indices=eachindex(ar.net_pins), + autoroute!(ar::ChannelRouter, transition_rule; net_indices=eachindex(ar.net_pins), fixed_channel_paths::Dict{Int,Vector{Int}}=Dict()) Perform channel and track assigment, then make routes. @@ -1047,12 +969,7 @@ function autoroute!( reset_nets!(ar, net_indices=net_indices) assign_channels!(ar; net_indices=net_indices, fixed_paths=fixed_channel_paths) assign_tracks!(ar) - rule = Paths.AutoChannelRouting( - ar.channels, - transition_rule, - margin, - ar - ) + rule = AutoChannelRouting(ar, transition_rule, margin) routes = make_routes!(ar, rule) return routes end @@ -1144,13 +1061,6 @@ function pin_labels(ar::ChannelRouter) ] end -function waypoint_labels(ar::ChannelRouter) - return [ - ("$i", segment_waypoint(ar, segs[i]).p) for segs in ar.net_wires for - i = 1:length(segs) - ] -end - function channel_path(ar::ChannelRouter, channel_idx) return ar.channels[channel_idx].path end @@ -1171,6 +1081,10 @@ struct AutoChannelRouting{T <: Coordinate} <: AbstractChannelRouting transition_margin::T router::ChannelRouter{T} end + +function AutoChannelRouting(ar::ChannelRouter{T}, transition_rule, margin) where {T} + return AutoChannelRouting{T}(ar.channels, transition_rule, convert(T, margin), ar) +end entry_rules(r::AutoChannelRouting) = Iterators.repeated(r.transition_rule) exit_rule(r::AutoChannelRouting) = r.transition_rule @@ -1189,17 +1103,10 @@ function track_path_segment(ar::ChannelRouter{T}, ch::RouteChannel, pa::Path; ma wireseg = net_wire(ar, net_idx)[wireseg_idx] track_idx = segment_track(ar, wireseg) # Get the starting and ending pathlengths - # Not accounting for track offsets - # start_channel, stop_channel = bounding_channels(wireseg) - # wireseg_start = pathlength_at_intersection(ar, channel_idx, start_channel) - # wireseg_stop = pathlength_at_intersection(ar, channel_idx, stop_channel) - # Account for track offsets - s1, s2 = interval(ar, wireseg) - wireseg_start = against_channel(ar, wireseg) ? s2 : s1 - wireseg_stop = against_channel(ar, wireseg) ? s1 : s2 - - prev_width = zero(T)#width_at_intersection(ar, start_channel, channel_idx) - next_width = zero(T)#width_at_intersection(ar, stop_channel, channel_idx) + # Accounting for track offsets + wireseg_start, wireseg_stop = unsorted_interval(ar, wireseg) + prev_width = zero(T) # Autorouter just uses margin + next_width = zero(T) # Interval already takes into account actual neighbor track offsets so we don't need this precaution channel_section = segment_channel_section(ch, wireseg_start, wireseg_stop, prev_width, next_width; margin) # Return channel section segment offset by width according to track diff --git a/test/autoroute_examples.jl b/test/autoroute_examples.jl index 06204147d..dad10073c 100644 --- a/test/autoroute_examples.jl +++ b/test/autoroute_examples.jl @@ -407,6 +407,35 @@ function example_bspline() return c, ar end +# ── Example 9: Fan-out with 40 nets ────────────────────────────────── +# 40 nets must fan out with a ratio of 2 +function example_fanout40nets() + lx_outer = ly_outer = 10e6nm + lx_inner = ly_inner = 5e6nm + + fanout_space_bottom = Path(Point(-lx_outer/2, -(ly_inner/2 + (ly_outer - ly_inner)/4))) + straight!(fanout_space_bottom, lx_outer, Paths.Trace(0.8*(ly_outer - ly_inner)/4)) + + n_nets = 40 + x0s = range(-lx_outer/2, stop=lx_outer/2, length=n_nets+2)[2:end-1] + x1s = range(-lx_inner/2, stop=lx_inner/2, length=n_nets+2)[2:end-1] + p0s = [PointHook(x, -ly_outer/2, -90°) for x in x0s] + p1s = [PointHook(x, -ly_inner/2, 90°) for x in x1s] + mynets = [(i, i + n_nets) for i = 1:n_nets] + ar = ChannelRouter( + mynets, + vcat(p0s, p1s), + [RouteChannel(fanout_space_bottom)] + ) + routes = Paths.autoroute!(ar, Paths.StraightAnd90(10μm), 10μm) + c = Paths.visualize_router_state(ar, wire_width=1μm); + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + paths = Path.(routes, Ref(Paths.Trace(WW))) + @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert length(ar.channel_tracks[1]) <= 20 "Left and right halves share tracks" + return c, ar +end + # ── Example 10: Schematic integration ──────────────────────────────────────── # TODO: AutoChannelRouting with schematic workflow. # Requires components with hooks, route! calls, and schematic compilation. diff --git a/test/test_channels.jl b/test/test_channels.jl index ee0e513d5..94dd0c836 100644 --- a/test/test_channels.jl +++ b/test/test_channels.jl @@ -375,34 +375,6 @@ end return c end - function test_fanout() - lx_outer = ly_outer = 10e6nm - lx_inner = ly_inner = 5e6nm - - fanout_space_bottom = Path(Point(-lx_outer/2, -(ly_inner/2 + (ly_outer - ly_inner)/4))) - straight!(fanout_space_bottom, lx_outer, Paths.Trace(0.8*(ly_outer - ly_inner)/4)) - - n_nets = 40 - x0s = range(-lx_outer/2, stop=lx_outer/2, length=n_nets+2)[2:end-1] - x1s = range(-lx_inner/2, stop=lx_inner/2, length=n_nets+2)[2:end-1] - p0s = [PointHook(x, -ly_outer/2, -90°) for x in x0s] - p1s = [PointHook(x, -ly_inner/2, 90°) for x in x1s] - # n_nets=10 - mynets = [(i, i + n_nets) for i = 1:n_nets] - ar = ChannelRouter( - mynets, - vcat(p0s, p1s), - [RouteChannel(fanout_space_bottom)] - ) - - Paths.assign_channels!(ar) - Paths.assign_tracks!(ar) - - c = Paths.visualize_router_state(ar); - - save("autoroute_test.png", c, width=10DeviceLayout.Graphics.inch) - return c - end function test_grid_escape() # Grid of sites dx = 1.5 From 9089792ac7193d11b3958c9bd3f70c7bd3292e37 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Wed, 15 Apr 2026 17:00:43 +0200 Subject: [PATCH 15/32] Minor example and test changes --- src/paths/channel_autorouter.jl | 257 +++++++++++++++++++------------- src/paths/channels.jl | 2 +- src/paths/paths.jl | 5 +- test/autoroute_examples.jl | 126 ++++++++++------ test/test_channels.jl | 30 ++-- 5 files changed, 249 insertions(+), 171 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index f357c55c3..30e15590e 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -82,18 +82,14 @@ struct ChannelRouter{T <: Coordinate} net_paths::Vector{Path{T}} # [Internals] Persistent path objects to populate after routing end -"""" +""" ChannelRouter( nets::Vector{Tuple{Int, Int}}, pin_hooks::Vector{<:Hook}, channels::Vector{<:RouteChannel} ) """ -function ChannelRouter( - nets, - pin_hooks::Vector{<:Hook}, - channels::Vector{<:RouteChannel} -) +function ChannelRouter(nets, pin_hooks::Vector{<:Hook}, channels::Vector{<:RouteChannel}) T = promote_type(coordinatetype(pin_hooks), coordinatetype(channels)) net_wires = [NetWire() for i in eachindex(nets)] channel_segments = [TrackWireSegment[] for i in eachindex(channels)] @@ -124,7 +120,7 @@ function ChannelRouter(channels::Vector{RouteChannel{T}}) where {T} segment_waypoints = Dict{TrackWireSegment, PointHook{T}}() return ChannelRouter{T}( SimpleGraph(), - Tuple{Int,Int}[], + Tuple{Int, Int}[], NetWire[], PointHook{T}[], channels, @@ -166,9 +162,11 @@ is_pin(ar::ChannelRouter, graphidx) = graphidx > num_channels(ar) adjoining_channel(ar::ChannelRouter, pin) = neighbors(channel_graph(ar), pin_to_graphidx(ar, pin))[1] channel_intersection(ar, s1, s2) = ar.channel_intersections[_swap(s1, s2)] -function pathlength_at_intersection(ar::ChannelRouter{T}, +function pathlength_at_intersection( + ar::ChannelRouter{T}, running_channel, - intersecting_channel) where {T} + intersecting_channel +) where {T} # Intersecting channel is zero where a wire segment hits a pin if iszero(running_channel) || iszero(intersecting_channel) return zero(T) @@ -178,9 +176,7 @@ function pathlength_at_intersection(ar::ChannelRouter{T}, return ixn_info[2] end -function direction_at_intersection(ar::ChannelRouter, - running_channel, - intersecting_channel) +function direction_at_intersection(ar::ChannelRouter, running_channel, intersecting_channel) # Intersecting channel is zero where a wire segment hits a pin if iszero(running_channel) || iszero(intersecting_channel) return 0.0° @@ -193,15 +189,15 @@ end segment_waypoint(ar::ChannelRouter, ws::TrackWireSegment) = ar.segment_waypoints[ws] _swap(x, y) = (y > x ? (x, y) : (y, x)) -function _shared_vertex(edge_a::Tuple{Int,Int}, edge_b::Tuple{Int,Int}) +function _shared_vertex(edge_a::Tuple{Int, Int}, edge_b::Tuple{Int, Int}) a1, a2 = edge_a b1, b2 = edge_b a1 in (b1, b2) && return a1 a2 in (b1, b2) && return a2 - error("Edges $edge_a and $edge_b share no vertex") + return error("Edges $edge_a and $edge_b share no vertex") end -pathlength_from_start(channel, node, s) = pathlength(channel[1:node-1]) + s +pathlength_from_start(channel, node, s) = pathlength(channel[1:(node - 1)]) + s # Build graph with pins/channels as vertices and intersections as edges function build_channel_graph(pins, channels, T) @@ -210,7 +206,7 @@ function build_channel_graph(pins, channels, T) # Create segments extending from pins bbox = bounds(bounds(channels), bounds(Polygon(DeviceLayout.getp.(pins)))) - ray_length = max(DeviceLayout.width(bbox), DeviceLayout.height(bbox))*sqrt(2) + ray_length = max(DeviceLayout.width(bbox), DeviceLayout.height(bbox)) * sqrt(2) pin_rays = Path{T}[] for pin in pins path = Path{T}(pin.p, pin.in_direction) @@ -219,8 +215,8 @@ function build_channel_graph(pins, channels, T) end # Add edges for intersections between channels - intersections = DeviceLayout.Intersect.prepared_intersections( - [channels..., pin_rays...]) + intersections = + DeviceLayout.Intersect.prepared_intersections([channels..., pin_rays...]) pin_ixns = Dict{Int, Tuple{Int, IntersectionInfo{T}}}() for ixn in intersections location_1, location_2, p = ixn @@ -241,14 +237,13 @@ function build_channel_graph(pins, channels, T) s1 = pathlength_from_start(channels[v1_idx], node1_idx, s1) # Record intersection if it's the closest so far ixn_info = (s1, s2, dir1, dir2, p) - _, old_ixn_info = get(pin_ixns, v2_idx, - (v1_idx, ixn_info)) + _, old_ixn_info = get(pin_ixns, v2_idx, (v1_idx, ixn_info)) old_distance = old_ixn_info[2] if s2 <= old_distance pin_ixns[v2_idx] = (v1_idx, ixn_info) end else # record intersection as edge in channel graph, with info in dict - haskey(intersection_dict, (v1_idx, v2_idx)) && + haskey(intersection_dict, (v1_idx, v2_idx)) && error("Channels $v1_idx and $v2_idx have multiple intersections") dir1 = direction(channels[v1_idx][node1_idx].seg, s1) dir2 = direction(channels[v2_idx][node2_idx].seg, s2) @@ -261,9 +256,11 @@ function build_channel_graph(pins, channels, T) end end # Add min distance edge for each pin - for pin_idx in (length(channels)+1):(length(channels) + length(pins)) + for pin_idx = (length(channels) + 1):(length(channels) + length(pins)) orig_idx = pin_idx - length(channels) - !haskey(pin_ixns, pin_idx) && error("The ray from pin $(orig_idx) ($(pins[orig_idx])) does not intersect any channel") + !haskey(pin_ixns, pin_idx) && error( + "The ray from pin $(orig_idx) ($(pins[orig_idx])) does not intersect any channel" + ) channel_idx, ixn_info = pin_ixns[pin_idx] add_edge!(g, (channel_idx, pin_idx)) intersection_dict[(channel_idx, pin_idx)] = ixn_info @@ -318,14 +315,23 @@ function segment_direction(ar::ChannelRouter, ws::TrackWireSegment, s) return direction(seg, s) end -function segment_offset(ar::ChannelRouter{T}, ws::TrackWireSegment, s...; use_wire_direction=true) where {T} +function segment_offset( + ar::ChannelRouter{T}, + ws::TrackWireSegment, + s...; + use_wire_direction=true +) where {T} channel_idx = running_channel(ws) is_pin(ar, channel_idx) && return zero(T) track_idx = segment_track(ar, ws) isnothing(track_idx) && return zero(T) reversed = use_wire_direction && against_channel(ar, ws) - return track_section_offset(length(ar.channel_tracks[channel_idx]), - Paths.width(ar.channels[channel_idx].node.sty, s...), track_idx; reversed) + return track_section_offset( + length(ar.channel_tracks[channel_idx]), + Paths.width(ar.channels[channel_idx].node.sty, s...), + track_idx; + reversed + ) end """ @@ -345,7 +351,7 @@ end function unsorted_interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=true) start_channel, stop_channel = bounding_channels(ws) channel_idx = running_channel(ws) - + start_channel, stop_channel = bounding_channels(ws) s1 = pathlength_at_intersection(ar, channel_idx, start_channel) s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) @@ -363,14 +369,19 @@ function unsorted_interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=tr # Offset also depends neighbor track offsets α_ixn_start = (dir1 - start_dir) α_ixn_stop = (stop_dir - dir2) - prev_offset_proj = segment_offset(ar, prev(ar, ws), s_start; use_wire_direction=false) / sin(α_ixn_start) - next_offset_proj = segment_offset(ar, next(ar, ws), s_stop; use_wire_direction=false) / sin(α_ixn_stop) + prev_offset_proj = + segment_offset(ar, prev(ar, ws), s_start; use_wire_direction=false) / + sin(α_ixn_start) + next_offset_proj = + segment_offset(ar, next(ar, ws), s_stop; use_wire_direction=false) / sin(α_ixn_stop) # Offset *also* depends on this wire segment's offset at a non-90° intersection - start_offset_proj = -segment_offset(ar, ws, s_start; use_wire_direction=false) / tan(α_ixn_start) - stop_offset_proj = -segment_offset(ar, ws, s_stop; use_wire_direction=false) / tan(α_ixn_stop) + start_offset_proj = + -segment_offset(ar, ws, s_start; use_wire_direction=false) / tan(α_ixn_start) + stop_offset_proj = + -segment_offset(ar, ws, s_stop; use_wire_direction=false) / tan(α_ixn_stop) # This is approximate on bending or tapered tracks return s1 + (prev_offset_proj + start_offset_proj), - s2 - (next_offset_proj + stop_offset_proj) + s2 - (next_offset_proj + stop_offset_proj) end """ @@ -384,10 +395,7 @@ function next(ar::ChannelRouter, ws::TrackWireSegment) idx = findfirst(isequal(ws), segs) if idx == length(segs) final_pin_idx = pin_to_graphidx(ar, last(net_pins(ar, net_idx))) - return TrackWireSegment(net_idx, - final_pin_idx, - running_channel(ws), - 0) + return TrackWireSegment(net_idx, final_pin_idx, running_channel(ws), 0) end return segs[idx + 1] end @@ -403,10 +411,7 @@ function prev(ar::ChannelRouter, ws::TrackWireSegment) idx = findfirst(isequal(ws), segs) if idx == 1 first_pin_idx = pin_to_graphidx(ar, first(net_pins(ar, net_idx))) - return TrackWireSegment(net_idx, - first_pin_idx, - 0, - running_channel(ws)) + return TrackWireSegment(net_idx, first_pin_idx, 0, running_channel(ws)) end return segs[idx - 1] end @@ -423,8 +428,8 @@ function build_auxiliary_graph(ar::ChannelRouter{T}) where {T} n_aux = ne(g) # Map channel-graph edges to auxiliary vertices - aux_to_edge = Vector{Tuple{Int,Int}}(undef, n_aux) - edge_to_aux = Dict{Tuple{Int,Int}, Int}() + aux_to_edge = Vector{Tuple{Int, Int}}(undef, n_aux) + edge_to_aux = Dict{Tuple{Int, Int}, Int}() for (i, e) in enumerate(edges(g)) key = _swap(e.src, e.dst) aux_to_edge[i] = key @@ -436,25 +441,29 @@ function build_auxiliary_graph(ar::ChannelRouter{T}) where {T} I = Int[] J = Int[] V = T[] - for ch in 1:num_channels(ar) + for ch = 1:num_channels(ar) nbs = neighbors(g, ch) length(nbs) < 2 && continue # Collect (pathlength_along_ch, aux_vertex) for each intersection on this channel ch_ixns = Tuple{T, Int}[ - (pathlength_at_intersection(ar, ch, nb), edge_to_aux[_swap(ch, nb)]) - for nb in nbs + (pathlength_at_intersection(ar, ch, nb), edge_to_aux[_swap(ch, nb)]) for + nb in nbs ] sort!(ch_ixns, by=first) # Connect consecutive intersection points - for k in 1:(length(ch_ixns) - 1) + for k = 1:(length(ch_ixns) - 1) s1, aux1 = ch_ixns[k] s2, aux2 = ch_ixns[k + 1] add_edge!(aux_g, aux1, aux2) w = abs(s2 - s1) - push!(I, aux1); push!(J, aux2); push!(V, w) - push!(I, aux2); push!(J, aux1); push!(V, w) + push!(I, aux1) + push!(J, aux2) + push!(V, w) + push!(I, aux2) + push!(J, aux1) + push!(V, w) end end @@ -471,7 +480,12 @@ intersection points), using a precomputed [`AuxiliaryGraph`](@ref). In both cases, the returned path is a list of vertex indices `[pin_gidx, ch1, ..., chN, pin_gidx]`. """ -function shortest_path_between_pins(ar::ChannelRouter, p0::Int, p1::Int, aux::AuxiliaryGraph) +function shortest_path_between_pins( + ar::ChannelRouter, + p0::Int, + p1::Int, + aux::AuxiliaryGraph +) pin0_gidx = pin_to_graphidx(ar, p0) pin1_gidx = pin_to_graphidx(ar, p1) src_aux = aux.edge_to_aux[_swap(pin0_gidx, adjoining_channel(ar, p0))] @@ -485,9 +499,8 @@ function shortest_path_between_pins(ar::ChannelRouter, p0::Int, p1::Int, aux::Au # Skip consecutive duplicates: the aux path may traverse multiple intersections on # the same channel, which just means a longer segment on that channel. path = Int[pin0_gidx] - for i in 1:(length(aux_path) - 1) - ch = _shared_vertex(aux.aux_to_edge[aux_path[i]], - aux.aux_to_edge[aux_path[i + 1]]) + for i = 1:(length(aux_path) - 1) + ch = _shared_vertex(aux.aux_to_edge[aux_path[i]], aux.aux_to_edge[aux_path[i + 1]]) if ch != last(path) push!(path, ch) end @@ -623,8 +636,7 @@ function assign_tracks_matching!(ar, channel) haskey(merged_into, l) && continue dists_from_l = dag_shortest_paths(vcg, high_to_low, l) for r in R - mergeable = dists_from_l[r] >= nv(vcg) && - dists_from_r[r][l] >= nv(vcg) + mergeable = dists_from_l[r] >= nv(vcg) && dists_from_r[r][l] >= nv(vcg) !mergeable && continue if !segments_overlap(ar, wiresegs_ascending[l], wiresegs_ascending[r]) add_edge!(merging_graph, l, r) @@ -640,7 +652,7 @@ function assign_tracks_matching!(ar, channel) # The longest directed path gives a representative of each merged group where track height is max # But VCG may be a partial order so use topological sort high_to_low = topological_sort_by_dfs(vcg) # If vcg was acyclic to begin with, it is still acyclic - for v in 1:nv(vcg) + for v = 1:nv(vcg) if !haskey(merged_groups, v) merged_groups[v] = [v] end @@ -672,7 +684,7 @@ end function _same_tendency_at_boundary(ar, seg1, seg2) p1, n1 = bounding_channels(seg1) p2, n2 = bounding_channels(seg2) - shared_boundary = [2 - 1*(p1 == p2 || p1 == n2), 2 - 1*(p2 == p1 || p2 == n1)] + shared_boundary = [2 - 1 * (p1 == p2 || p1 == n2), 2 - 1 * (p2 == p1 || p2 == n1)] t1 = prev_next_tendency(ar, seg1) t2 = prev_next_tendency(ar, seg2) return t1[shared_boundary[1]] == t2[shared_boundary[2]] @@ -686,7 +698,7 @@ function best_matching!(merging_graph, vcg) working_vcg = copy(vcg) # Collect "deleted" vertices in edge_selection_graph # Vertices aren't labelled, just indexed, so we don't actually delete them - ignored = Set{Int}() + ignored = Set{Int}() # While working graphs have vertices, keep collecting edges to remove from merging graph while length(ignored) < nv(edge_selection_graph) # 1. Find nodes with no VCG ancestors, remove edges between them @@ -705,9 +717,10 @@ function best_matching!(merging_graph, vcg) # Remove edges between vertices with no ancestors # Then they will not be selected for removal from `merging_graph` no_ancestors = Int[] - for v in 1:nv(edge_selection_graph) + for v = 1:nv(edge_selection_graph) v in ignored && continue - if isempty(inneighbors(working_vcg, v)) || all([w in ignored for w in inneighbors(working_vcg, v)]) + if isempty(inneighbors(working_vcg, v)) || + all([w in ignored for w in inneighbors(working_vcg, v)]) # v has no surviving ancestors, remove edges to other such vertices for w in no_ancestors rem_edge!(edge_selection_graph, v, w) @@ -716,7 +729,7 @@ function best_matching!(merging_graph, vcg) end end # Find nodes with minimum number of edges - for v in 1:nv(edge_selection_graph) + for v = 1:nv(edge_selection_graph) v in ignored && continue nbs = neighbors(edge_selection_graph, v) if length(nbs) < min_neighbors @@ -759,14 +772,14 @@ function best_matching!(merging_graph, vcg) end # Any matching is feasible now that we've removed marked edges return BipartiteMatching.findmaxcardinalitybipartitematching( - BitMatrix(adjacency_matrix(merging_graph)) - ) + BitMatrix(adjacency_matrix(merging_graph)) + ) end function dag_shortest_paths(dag, v_sorted, s) d = fill(nv(dag), nv(dag)) d[s] = 0 - for i in findfirst(v -> v == s, v_sorted):nv(dag) + for i = findfirst(v -> v == s, v_sorted):nv(dag) u = v_sorted[i] for v in outneighbors(dag, u) if d[v] > d[u] + 1 @@ -792,7 +805,7 @@ function channel_problem_graphs(ar::ChannelRouter, channel) vcg = SimpleDiGraph(length(wiresegs_ascending)) # just a fresh graph for (idx1, seg1) in pairs(wiresegs_ascending) low1, high1 = interval(ar, seg1) - for (idx2, seg2) in collect(pairs(wiresegs_ascending))[idx1+1:end] + for (idx2, seg2) in collect(pairs(wiresegs_ascending))[(idx1 + 1):end] low2, high2 = interval(ar, seg2) # If there is no overlap, break and move on to next seg1 @@ -808,8 +821,16 @@ function channel_problem_graphs(ar::ChannelRouter, channel) (low1 == high1 && low2 == high2 && pt1 == pt2 && nt1 == nt2) && break low1_tend, high1_tend = against_channel(ar, seg1) ? (nt1, pt1) : (pt1, nt1) low2_tend, high2_tend = against_channel(ar, seg2) ? (nt2, pt2) : (pt2, nt2) - avoidable = is_avoidable(low1, high1, low2, high2, - low1_tend, high1_tend, low2_tend, high2_tend) + avoidable = is_avoidable( + low1, + high1, + low2, + high2, + low1_tend, + high1_tend, + low2_tend, + high2_tend + ) !avoidable && continue # Crossing is avoidable, so add a constraint @@ -824,7 +845,16 @@ function channel_problem_graphs(ar::ChannelRouter, channel) return vcg, zone_ig end -function is_avoidable(low1, high1, low2, high2, low1_tend, high1_tend, low2_tend, high2_tend) +function is_avoidable( + low1, + high1, + low2, + high2, + low1_tend, + high1_tend, + low2_tend, + high2_tend +) if high1 < high2 # 1 ____ # 2 ____ @@ -841,12 +871,15 @@ function is_avoidable(low1, high1, low2, high2, low1_tend, high1_tend, low2_tend ccw_order = [reverse(order[up]); order[down]] # Crossing is avoidable if same net has both endpoints adjacent # on the clock - avoidable = (ccw_order[1] == ccw_order[2] || - ccw_order[2] == ccw_order[3]) - # Also, if seg1 and seg2 have an endpoint at the same place, + avoidable = (ccw_order[1] == ccw_order[2] || ccw_order[2] == ccw_order[3]) + # Also, if seg1 and seg2 have an endpoint at the same place and don't make an X, # then crossing in this channel may be avoidable but depends on # other channel; assume other channel will agree - avoidable = (avoidable || (low1 == low2 || high1 == high2)) + depends = + ((low1 == low2) || high1 == high2) && + ((low1_tend == low2_tend) || (high1_tend == high2_tend)) + avoidable = (avoidable || depends) + return avoidable end # +1 if segment crosses over low track index in ws's channel @@ -857,7 +890,7 @@ function prev_next_tendency(ar, ws; use_wire_direction=true) s_along_start = pathlength_at_intersection(ar, start_channel, channel_idx) s1 = pathlength_at_intersection(ar, channel_idx, start_channel) s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) - s_along_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) + s_along_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) # Directions of bounding and running channels start_dir = direction_at_intersection(ar, start_channel, channel_idx) dir1 = direction_at_intersection(ar, channel_idx, start_channel) @@ -885,10 +918,7 @@ end Using channel and track assignments, create `Route`s for the nets in `net_indices`. """ -function make_routes!( - ar::ChannelRouter{T}, - rule -) where {T} +function make_routes!(ar::ChannelRouter{T}, rule) where {T} empty!(ar.net_routes) for idx_net in eachindex(ar.net_pins) p0, p1 = pin_coordinates.(ar, net_pins(ar, idx_net)) @@ -1008,10 +1038,7 @@ Pins are labeled as `P/N` where `P` is the pin index and `N` is the net index. Segment waypoints are marked with circles and numbered sequentially within each net. """ -function visualize_router_state( - ar::ChannelRouter{T}; - wire_width=0.1*oneunit(T) -) where {T} +function visualize_router_state(ar::ChannelRouter{T}; wire_width=0.1 * oneunit(T)) where {T} c = DeviceLayout.Cell{T}("track_viz") paths = Path.(ar.net_routes, Ref(Paths.Trace(wire_width))) @@ -1023,8 +1050,12 @@ function visualize_router_state( trlab = track_labels(ar, tracks) DeviceLayout.text!.(c, trlab, GDSMeta(4)) for pa in paths - for node in pa[1:end-1] - DeviceLayout.render!(c, DeviceLayout.Circle(1.5wire_width) + p1(node.seg), GDSMeta(5)) + for node in pa[1:(end - 1)] + DeviceLayout.render!( + c, + DeviceLayout.Circle(1.5wire_width) + p1(node.seg), + GDSMeta(5) + ) end end plab = pin_labels(ar) @@ -1035,9 +1066,7 @@ function visualize_router_state( end function track_paths(ar::ChannelRouter) - return [ - track_path(ar, s, c) for s = 1:num_channels(ar) for c = 1:num_tracks(ar, s) - ] + return [track_path(ar, s, c) for s = 1:num_channels(ar) for c = 1:num_tracks(ar, s)] end channel_paths(ar::ChannelRouter) = [channel_path(ar, s) for s = 1:num_channels(ar)] @@ -1067,10 +1096,12 @@ end function track_path(ar::ChannelRouter{T}, channel_idx, track_idx) where {T} n_tracks = length(channel_tracks(ar, channel_idx)) - seg = track_path_segment(n_tracks, - ar.channels[channel_idx].node, track_idx) + seg = track_path_segment(n_tracks, ar.channels[channel_idx].node, track_idx) w = Paths.width(ar.channels[channel_idx].node.sty, zero(T)) - pa = Path([Paths.Node(seg, Paths.Trace(0.9*w / (n_tracks+1)))]; name="$channel_idx:$track_idx") + pa = Path( + [Paths.Node(seg, Paths.Trace(0.9 * w / (n_tracks + 1)))]; + name="$channel_idx:$track_idx" + ) return pa end @@ -1089,16 +1120,29 @@ entry_rules(r::AutoChannelRouting) = Iterators.repeated(r.transition_rule) exit_rule(r::AutoChannelRouting) = r.transition_rule function track_path_segments(rule::AutoChannelRouting, pa::Path, _) - return [track_path_segment(rule.router, channel, pa; margin=rule.transition_margin) - for channel in rule.channels[channels_taken(rule.router, pa)]] + return [ + track_path_segment(rule.router, channel, pa; margin=rule.transition_margin) for + channel in rule.channels[channels_taken(rule.router, pa)] + ] end -function track_path_segment(ar::ChannelRouter{T}, ch::RouteChannel, pa::Path; margin=zero(T)) where {T} +function track_path_segment( + ar::ChannelRouter{T}, + ch::RouteChannel, + pa::Path; + margin=zero(T) +) where {T} # Get the track wire segment from the router # Assume there is exactly one wire segment belonging to this path in the channel # Channel node might have been converted to store in router, so just check start point/direction - channel_idx = findfirst(chn -> p0(chn.node.seg) ≈ p0(ch.path) && α0(chn.node.seg) == α0(ch.path), ar.channels) - net_idx = findfirst(pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), ar.pins[first.(ar.net_pins)]) + channel_idx = findfirst( + chn -> p0(chn.node.seg) ≈ p0(ch.path) && α0(chn.node.seg) == α0(ch.path), + ar.channels + ) + net_idx = findfirst( + pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), + ar.pins[first.(ar.net_pins)] + ) wireseg_idx = findfirst(ws -> running_channel(ws) == channel_idx, net_wire(ar, net_idx)) wireseg = net_wire(ar, net_idx)[wireseg_idx] track_idx = segment_track(ar, wireseg) @@ -1107,20 +1151,33 @@ function track_path_segment(ar::ChannelRouter{T}, ch::RouteChannel, pa::Path; ma wireseg_start, wireseg_stop = unsorted_interval(ar, wireseg) prev_width = zero(T) # Autorouter just uses margin next_width = zero(T) # Interval already takes into account actual neighbor track offsets so we don't need this precaution - channel_section = segment_channel_section(ch, - wireseg_start, wireseg_stop, prev_width, next_width; margin) + channel_section = segment_channel_section( + ch, + wireseg_start, + wireseg_stop, + prev_width, + next_width; + margin + ) # Return channel section segment offset by width according to track - return track_path_segment(length(channel_tracks(ar, channel_idx)), - channel_section, track_idx; reversed=against_channel(ar, wireseg)) + return track_path_segment( + length(channel_tracks(ar, channel_idx)), + channel_section, + track_idx; + reversed=against_channel(ar, wireseg) + ) end function channels_taken(ar::ChannelRouter, pa::Path) - net_idx = findfirst(pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), ar.pins[first.(ar.net_pins)]) + net_idx = findfirst( + pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), + ar.pins[first.(ar.net_pins)] + ) return [running_channel(wireseg) for wireseg in net_wire(ar, net_idx)] end function _update_with_graph!(rule::AutoChannelRouting, route_node, graph; kwargs...) - push!(rule.router.net_paths, route_node.component._path) + return push!(rule.router.net_paths, route_node.component._path) end function _update_with_plan!(rule::AutoChannelRouting{T}, route_node, sch) where {T} @@ -1134,4 +1191,4 @@ function _update_with_plan!(rule::AutoChannelRouting{T}, route_node, sch) where assign_channels!(rule.router) assign_tracks!(rule.router) end -end \ No newline at end of file +end diff --git a/src/paths/channels.jl b/src/paths/channels.jl index df943c49f..f37d07bce 100644 --- a/src/paths/channels.jl +++ b/src/paths/channels.jl @@ -57,7 +57,7 @@ function segment_channel_section( split( ch.node, [ - max(zero(T), wireseg_stop + margin + next_width / 2,) + max(zero(T), wireseg_stop + margin + next_width / 2) min(pathlength(ch.node.seg), wireseg_start - margin - prev_width / 2) ] )[2] diff --git a/src/paths/paths.jl b/src/paths/paths.jl index 0122f6ac8..1690e489c 100644 --- a/src/paths/paths.jl +++ b/src/paths/paths.jl @@ -710,7 +710,10 @@ Generic fallback, approximating a [`Paths.Segment`](@ref) using many [`Polygons.LineSegment`](@ref) objects. Returns a vector of `LineSegment`s. """ function line_segments(seg::Paths.Segment{T}) where {T} - return Polygons.segmentize(DeviceLayout.discretize_curve(seg, DeviceLayout.onenanometer(T)), false) + return Polygons.segmentize( + DeviceLayout.discretize_curve(seg, DeviceLayout.onenanometer(T)), + false + ) end """ diff --git a/test/autoroute_examples.jl b/test/autoroute_examples.jl index dad10073c..4f5b6ec8c 100644 --- a/test/autoroute_examples.jl +++ b/test/autoroute_examples.jl @@ -1,35 +1,53 @@ using DeviceLayout using FileIO -import .Paths: ChannelRouter, RouteChannel, AutoChannelRouting, autoroute!, visualize_router_state +import .Paths: + ChannelRouter, RouteChannel, AutoChannelRouting, autoroute!, visualize_router_state # ── Helpers ────────────────────────────────────────────────────────────────── -"Horizontal channel at height `y`, from `x0` to `x1`." +""" +Horizontal channel at height `y`, from `x0` to `x1`. +""" function hchannel(x0, x1, y; width=2.0) pa = Path(Float64(x0), Float64(y)) straight!(pa, Float64(x1 - x0), Paths.Trace(Float64(width))) return pa end -"Vertical channel at `x`, from `y0` to `y1`." +""" +Vertical channel at `x`, from `y0` to `y1`. +""" function vchannel(x, y0, y1; width=2.0) pa = Path(Float64(x), Float64(y0), α0=90°) straight!(pa, Float64(y1 - y0), Paths.Trace(Float64(width))) return pa end -"Diagonal channel from `(x0,y0)` at angle `α` for length `len`." +""" +Diagonal channel from `(x0,y0)` at angle `α` for length `len`. +""" function dchannel(x0, y0, α, len; width=2.0) pa = Path(Float64(x0), Float64(y0), α0=α) straight!(pa, Float64(len), Paths.Trace(Float64(width))) return pa end -"B-spline channel from `(x0,y0)` to `(x1, y1)` at angle `α` at endpoints." +""" +B-spline channel from `(x0,y0)` to `(x1, y1)` at angle `α` at endpoints. +""" function bchannel(x0, y0, α, x1, y1; width=2.0) pa = Path(Float64(x0), Float64(y0), α0=α) - bspline!(pa, [Point(x1, y1)], α, Paths.Trace(Float64(width)), auto_speed=true, auto_curvature=true, endpoints_speed=1, endpoints_curvature=0) + bspline!( + pa, + [Point(x1, y1)], + α, + Paths.Trace(Float64(width)), + auto_speed=true, + auto_curvature=true, + endpoints_speed=1, + endpoints_curvature=0 + ) return pa end @@ -59,7 +77,7 @@ function example_simple() channels = [hchannel(0, 10, 0)] hooks = [ bpin(2, -0.5), # pin 1: below channel, ray goes up - tpin(8, 0.5), # pin 2: above channel, ray goes down + tpin(8, 0.5) # pin 2: above channel, ray goes down ] nets = [(1, 2)] @@ -89,7 +107,7 @@ function example_parallel() vchannel(10, -1, 9), # v_right hchannel(-1, 11, 0), # h_bot hchannel(-1, 11, 4), # h_mid - hchannel(-1, 11, 8), # h_top + hchannel(-1, 11, 8) # h_top ] hooks = [ lpin(-0.5, 0), # p1: left, at h_bot level @@ -97,7 +115,7 @@ function example_parallel() lpin(-0.5, 8), # p3: left, at h_top level rpin(10.5, 0), # p4: right, at h_bot level rpin(10.5, 4), # p5: right, at h_mid level - rpin(10.5, 8), # p6: right, at h_top level + rpin(10.5, 8) # p6: right, at h_top level ] nets = [(1, 4), (2, 5), (3, 6)] @@ -123,13 +141,13 @@ function example_crossing() channels = [ vchannel(5, -1, 7), # v_mid hchannel(-1, 9, 0), # h_bot - hchannel(-1, 9, 6), # h_top + hchannel(-1, 9, 6) # h_top ] hooks = [ bpin(2, -0.5), # p1: below h_bot, left side bpin(8, -0.5), # p2: below h_bot, right side tpin(2, 6.5), # p3: above h_top, left side - tpin(8, 6.5), # p4: above h_top, right side + tpin(8, 6.5) # p4: above h_top, right side ] # Crossed: bottom-left↔top-right, bottom-right↔top-left nets = [(1, 4), (2, 3)] @@ -161,7 +179,7 @@ function example_fanin_fanout() channels = [ vchannel(0, -2, 11), # v_left hchannel(-2, 12, 7), # h_mid - vchannel(10, -2, 11), # v_right + vchannel(10, -2, 11) # v_right ] # Left pins clustered at y=3,4,5,6 — all cross v_left # Right pins spread at y=0,3,6,9 — all cross v_right @@ -173,7 +191,7 @@ function example_fanin_fanout() rpin(11, 0), # p5 rpin(11, 3), # p6 rpin(11, 6), # p7 - rpin(11, 9), # p8 + rpin(11, 9) # p8 ] nets = [(1, 5), (2, 6), (3, 7), (4, 8)] @@ -183,7 +201,6 @@ function example_fanin_fanout() @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" paths = Path.(routes, Ref(Paths.Trace(WW))) - @show Intersect.prepared_intersections(paths) @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" return c, ar @@ -205,7 +222,7 @@ function example_multichannel_fanout() hchannel(-2, 12, 3.5), # h_2 hchannel(-2, 12, 5.5), # h_3 hchannel(-2, 12, 7.5), # h_4 - vchannel(10, -2, 11), # v_right + vchannel(10, -2, 11) # v_right ] # Left pins clustered at y=3,4,5,6 — all cross v_left # Right pins spread at y=0,3,6,9 — all cross v_right @@ -217,7 +234,7 @@ function example_multichannel_fanout() rpin(11, 0), # p5 rpin(11, 3), # p6 rpin(11, 6), # p7 - rpin(11, 9), # p8 + rpin(11, 9) # p8 ] nets = [(1, 5), (2, 6), (3, 7), (4, 8)] @@ -260,7 +277,7 @@ function example_grid() hchannel(-2, 11, 0), hchannel(-2, 11, 3), hchannel(-2, 11, 6), - hchannel(-2, 11, 9), + hchannel(-2, 11, 9) ] hooks = [ lpin(-0.5, 3), # p1: left edge, at h2 level → adj to v1 @@ -268,12 +285,12 @@ function example_grid() bpin(1.5, -0.5), # p3: bottom edge → adj to h1 bpin(7.5, -0.5), # p4: bottom edge → adj to h1 lpin(-0.5, 9), # p5: left edge, at h4 level → adj to v1 - rpin(9.5, 0), # p6: right edge, at h1 level → adj to v4 + rpin(9.5, 0) # p6: right edge, at h1 level → adj to v4 ] nets = [ (1, 2), # left(y=3) → right(y=6): diagonal traverse (3, 5), # bottom(x=1.5) → left(y=9): corner path - (4, 6), # bottom(x=7.5) → right(y=0): short path + (4, 6) # bottom(x=7.5) → right(y=0): short path ] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) @@ -301,16 +318,16 @@ end function example_angled() channels = [ - dchannel(-1, 0, 45°, 10*sqrt(2); width=2.0), # d1: NE from (-1,0) + dchannel(-1, 0, 45°, 10 * sqrt(2); width=2.0), # d1: NE from (-1,0) hchannel(-2, 12, 3; width=2.0), # h1 (y=3) - dchannel(-1, 10, -45°, 10*sqrt(2); width=2.0), # d2: SE from (-1,10) + dchannel(-1, 10, -45°, 10 * sqrt(2); width=2.0) # d2: SE from (-1,10) ] # Pins offset so rays cross diagonal hooks = [ bpin(0, -2), # p1: below d1, left side bpin(8, -2), # p2: below d2, right side rpin(12, 1), # p4: above d2, right side - lpin(-2, 1), # p3: below d1, left side + lpin(-2, 1) # p3: below d1, left side ] # Net 1 goes left→right via h1 and diagonals # Net 2 goes right→left via h1 and diagonals @@ -344,16 +361,16 @@ function example_dense() vchannel(0, -2, 7), # v_left (idx 1) hchannel(-2, 10, -1), # h1 (idx 3) hchannel(-2, 10, 6), # h2 (idx 4) - vchannel(8, -2, 7), # v_right (idx 2) + vchannel(8, -2, 7) # v_right (idx 2) ] # 6 left pins, tightly spaced, all crossing v_left # 6 right pins, same y-positions, all crossing v_right ys = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] hooks = [ [lpin(-1, y) for y in ys]..., # p1-p6 - [rpin(9, y) for y in ys]..., # p7-p12 + [rpin(9, y) for y in ys]... # p7-p12 ] - nets = [(i, i + 6) for i in 1:6] + nets = [(i, i + 6) for i = 1:6] ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) routes = autoroute!(ar, R, MARGIN) @@ -379,7 +396,7 @@ function example_bspline() channels = [ bchannel(0, -2, 30°, 1, 12), # v_left bchannel(-1, 2, -30°, 12, 9), # h_mid - bchannel(10, -2, 30°, 11, 12), # v_right + bchannel(10, -2, 30°, 11, 12) # v_right ] # Left pins clustered at y=3,4,5,6 — all cross v_left # Right pins spread at y=0,3,6,9 — all cross v_right @@ -391,13 +408,18 @@ function example_bspline() rpin(14, 0), # p5 rpin(14, 3), # p6 rpin(14, 6), # p7 - rpin(14, 9), # p8 + rpin(14, 9) # p8 ] nets = [(1, 5), (2, 6), (3, 7), (4, 8)] ar = Paths.ChannelRouter(nets, hooks, RouteChannel.(channels)) - transition_rule = Paths.BSplineRouting(auto_speed=true, auto_curvature=true, endpoints_speed=1, endpoints_curvature=0) - + transition_rule = Paths.BSplineRouting( + auto_speed=true, + auto_curvature=true, + endpoints_speed=1, + endpoints_curvature=0 + ) + routes = autoroute!(ar, transition_rule, 1.0) c = visualize_router_state(ar, wire_width=WW) @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" @@ -413,22 +435,19 @@ function example_fanout40nets() lx_outer = ly_outer = 10e6nm lx_inner = ly_inner = 5e6nm - fanout_space_bottom = Path(Point(-lx_outer/2, -(ly_inner/2 + (ly_outer - ly_inner)/4))) - straight!(fanout_space_bottom, lx_outer, Paths.Trace(0.8*(ly_outer - ly_inner)/4)) + fanout_space_bottom = + Path(Point(-lx_outer / 2, -(ly_inner / 2 + (ly_outer - ly_inner) / 4))) + straight!(fanout_space_bottom, lx_outer, Paths.Trace(0.8 * (ly_outer - ly_inner) / 4)) n_nets = 40 - x0s = range(-lx_outer/2, stop=lx_outer/2, length=n_nets+2)[2:end-1] - x1s = range(-lx_inner/2, stop=lx_inner/2, length=n_nets+2)[2:end-1] - p0s = [PointHook(x, -ly_outer/2, -90°) for x in x0s] - p1s = [PointHook(x, -ly_inner/2, 90°) for x in x1s] + x0s = range(-lx_outer / 2, stop=lx_outer / 2, length=n_nets + 2)[2:(end - 1)] + x1s = range(-lx_inner / 2, stop=lx_inner / 2, length=n_nets + 2)[2:(end - 1)] + p0s = [PointHook(x, -ly_outer / 2, -90°) for x in x0s] + p1s = [PointHook(x, -ly_inner / 2, 90°) for x in x1s] mynets = [(i, i + n_nets) for i = 1:n_nets] - ar = ChannelRouter( - mynets, - vcat(p0s, p1s), - [RouteChannel(fanout_space_bottom)] - ) + ar = ChannelRouter(mynets, vcat(p0s, p1s), [RouteChannel(fanout_space_bottom)]) routes = Paths.autoroute!(ar, Paths.StraightAnd90(10μm), 10μm) - c = Paths.visualize_router_state(ar, wire_width=1μm); + c = Paths.visualize_router_state(ar, wire_width=1μm) @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" paths = Path.(routes, Ref(Paths.Trace(WW))) @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" @@ -447,15 +466,18 @@ end # ── Assembly ───────────────────────────────────────────────────────────────── -function run_all_examples(; save_gds=true) +function run_all_examples(; save_gds=true, save_png=true) examples = [ - "simple" => example_simple, - "parallel" => example_parallel, - "crossing" => example_crossing, - "fanout" => example_fanout, - "grid" => example_grid, - "angled" => example_angled, - "dense" => example_dense, + "simple" => example_simple, + "parallel" => example_parallel, + "crossing" => example_crossing, + "fanin_fanout" => example_fanin_fanout, + "multichannel_fanout" => example_multichannel_fanout, + "grid" => example_grid, + "angled" => example_angled, + "dense" => example_dense, + "bspline" => example_bspline, + "fanout40" => example_fanout40nets ] results = Pair{String, Tuple{Cell, ChannelRouter}}[] for (name, fn) in examples @@ -468,5 +490,11 @@ function run_all_examples(; save_gds=true) end @info "Saved $(length(results)) GDS files" end + if save_png + for (name, (c, _)) in results + save("autoroute_$name.png", c; spec_warnings=false) + end + @info "Saved $(length(results)) PNG files" + end return results end diff --git a/test/test_channels.jl b/test/test_channels.jl index 94dd0c836..1d5ae007b 100644 --- a/test/test_channels.jl +++ b/test/test_channels.jl @@ -293,7 +293,6 @@ end using .SchematicDrivenLayout function test_simple(; split=false) - mypins = [ Point(4.0, 3.0), Point(6.0, 3.0), @@ -324,10 +323,10 @@ end for y0 in [1.0, 5.0, 9.0] pa = Path(0.0, y0) if split && y0 == 5.0 - pa = Path(0.0, y0+0.7) + pa = Path(0.0, y0 + 0.7) straight!(pa, 10.0, Paths.Trace(1.0)) push!(space_paths, pa) - pa = Path(0.0, y0-0.7) + pa = Path(0.0, y0 - 0.7) straight!(pa, 10.0, Paths.Trace(1.0)) push!(space_paths, pa) continue @@ -338,12 +337,7 @@ end n_wires = 4 mynets = [(i, i + 4) for i = 1:n_wires] - ar = ChannelRouter( - mynets, - pins, - RouteChannel.(space_paths) - ) - + ar = ChannelRouter(mynets, pins, RouteChannel.(space_paths)) # # # # Split space demo # # # Cut space 2 in half @@ -369,7 +363,7 @@ end # rts = make_routes!(ar, rule) # paths = [Path(rt, Paths.Trace(0.1)) for rt in rts] - c = Paths.visualize_router_state(ar); + c = Paths.visualize_router_state(ar) save("autoroute_test.svg", c, width=10DeviceLayout.Graphics.inch) return c @@ -381,7 +375,7 @@ end dy = 1.5 nx = 20 ny = 20 - grid = [(ix, iy) for ix in 1:nx for iy in 1:ny] + grid = [(ix, iy) for ix = 1:nx for iy = 1:ny] sites = [Point(dx * (ix - 1), dy * (iy - 1)) for (ix, iy) in grid] site_size = 0.5 @@ -416,8 +410,8 @@ end return iy_delta > 0 ? pi / 2 : -pi / 2 end site_pin_pos = [ - site + 0.5site_size * Point(cos(d), sin(d)) - for (site, d) in zip(sites, site_dirs) + site + 0.5site_size * Point(cos(d), sin(d)) for + (site, d) in zip(sites, site_dirs) ] # PointHook in_direction points inward (away from channel); site_dirs point toward channel site_hooks = [PointHook(p, d + pi) for (p, d) in zip(site_pin_pos, site_dirs)] @@ -437,7 +431,7 @@ end # Pre-compute edge assignments and per-edge coordinate ranges edge_assignments = [edge_index(edir) for edir in edge_dirs] - pins_per_edge = [count(==(ei), edge_assignments) for ei in 1:4] + pins_per_edge = [count(==(ei), edge_assignments) for ei = 1:4] coord_span = (-dx / 2, -dx / 2 + nx * dx) coord_ranges = [range(coord_span..., length=n) for n in pins_per_edge] @@ -458,13 +452,9 @@ end # Assemble: each site pin connects to its corresponding edge pin all_hooks = [site_hooks; edge_hooks] n_wires = length(sites) - mynets = [(i, i + n_wires) for i in 1:n_wires] + mynets = [(i, i + n_wires) for i = 1:n_wires] - ar = ChannelRouter( - mynets, - all_hooks, - RouteChannel.(space_paths) - ) + ar = ChannelRouter(mynets, all_hooks, RouteChannel.(space_paths)) Paths.assign_channels!(ar) Paths.assign_tracks!(ar) From f7f4236ef2cb1f4f3fd552b8d6b71acf4e81c31a Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Wed, 15 Apr 2026 17:10:49 +0200 Subject: [PATCH 16/32] Add autorouter internals tests --- test/test_autorouter_internals.jl | 165 ++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 test/test_autorouter_internals.jl diff --git a/test/test_autorouter_internals.jl b/test/test_autorouter_internals.jl new file mode 100644 index 000000000..805dae079 --- /dev/null +++ b/test/test_autorouter_internals.jl @@ -0,0 +1,165 @@ +@testitem "Autorouter internals" setup = [CommonTestSetup] begin + import DeviceLayout.Paths: ChannelRouter, RouteChannel, autoroute! + # ── Helpers (shared with autoroute_examples.jl) ────────────────────────────── + + hchannel(x0, x1, y; width=2.0) = + let pa = Path(Float64(x0), Float64(y)) + straight!(pa, Float64(x1 - x0), Paths.Trace(Float64(width))) + pa + end + vchannel(x, y0, y1; width=2.0) = + let pa = Path(Float64(x), Float64(y0), α0=90°) + straight!(pa, Float64(y1 - y0), Paths.Trace(Float64(width))) + pa + end + + lpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 180°) + rpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 0°) + bpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 270°) + tpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 90°) + + """ + Build a ChannelRouter and run channel assignment only (no track assignment). + """ + function _route_channels(channels, hooks, nets) + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + Paths.assign_channels!(ar) + return ar + end + + # ── is_avoidable ───────────────────────────────────────────────────────────── + # Pure function: is_avoidable(low1, high1, low2, high2, lt1, ht1, lt2, ht2) + # Determines if a crossing between two overlapping segments can be resolved + # by choosing track order, based on the tendency (±1) at each endpoint. + + @testset "is_avoidable" begin + # Segments: [0,6] and [3,9], high1 < high2 → order = [1,2,1,2] + # All same tendency (all +1): not avoidable + @test Paths.is_avoidable(0, 6, 3, 9, 1, 1, 1, 1) == false + + # Alternating: seg1 enters up, exits down; seg2 enters down, exits up + # Tendencies: low1=+1, high1=-1, low2=-1, high2=+1 + @test Paths.is_avoidable(0, 6, 3, 9, 1, -1, -1, 1) == false + + # Seg1 both up, seg2 both down + @test Paths.is_avoidable(0, 6, 3, 9, 1, 1, -1, -1) == true + + # Contained case: high2 <= high1 → order = [1,2,2,1] + # All same tendency: avoidable + @test Paths.is_avoidable(0, 9, 3, 6, 1, 1, 1, 1) == true + # Contained, opposite + @test Paths.is_avoidable(0, 9, 3, 6, -1, -1, -1, -1) == true + + # Shared endpoint (low1 == low2): assumed avoidable depending on neighbor + @test Paths.is_avoidable(0, 6, 0, 9, 1, -1, -1, -1) == true + @test Paths.is_avoidable(0, 6, 0, 9, -1, 1, -1, 1) == true + # Crossing is still not avoidable if the entry/exit points make an X + @test Paths.is_avoidable(0, 6, 0, 9, 1, -1, -1, 1) == false + @test Paths.is_avoidable(0, 6, 0, 9, -1, 1, 1, -1) == false + + # Shared endpoint (high1 == high2): assumed avoidable + @test Paths.is_avoidable(0, 9, 3, 9, 1, -1, 1, 1) == true + end + + # ── segments_overlap: boundary case with tendency ──────────────────────────── + # The knock-knee relaxation (strict < instead of <=) should NOT apply when + # segments at a shared endpoint face the same direction. + + @testset "segments_overlap boundary" begin + # Crossing setup: 2H + 1V, two nets that cross in the vertical channel. + # v_mid assigned first so its track offsets propagate. + channels = [ + vchannel(5, -1, 7), # idx 1: v_mid + hchannel(-1, 9, 0), # idx 2: h_bot + hchannel(-1, 9, 6) # idx 3: h_top + ] + hooks = [ + bpin(2, -0.5), # p1: below h_bot, left + bpin(8, -0.5), # p2: below h_bot, right + tpin(2, 6.5), # p3: above h_top, left + tpin(8, 6.5) # p4: above h_top, right + ] + # Crossing: bottom-left↔top-right, bottom-right↔top-left + ar = _route_channels(channels, hooks, [(1, 4), (2, 3)]) + + # In h_bot (channel 2): two segments touching at the v_mid intersection (x=5) + segs_hbot = ar.channel_segments[2] + @test length(segs_hbot) == 2 + # These segments touch at x=5 with same tendency (both enter v_mid going up) + # → should be treated as overlapping (can't knock-knee) + @test Paths.segments_overlap(ar, segs_hbot[1], segs_hbot[2]) == true + + # In v_mid (channel 1): two segments fully overlapping (both span h_bot→h_top) + segs_vmid = ar.channel_segments[1] + @test length(segs_vmid) == 2 + @test Paths.segments_overlap(ar, segs_vmid[1], segs_vmid[2]) == true + end + + @testset "segments_overlap knock-knee" begin + # Parallel setup: two nets through a shared channel, NOT crossing. + # They should NOT overlap since knock-knee is possible + # (touching at boundary with opposite tendencies) + channels = [ + vchannel(5, -1, 10), # idx 1: v_mid + hchannel(-1, 9, 0), # idx 2: h_bot + hchannel(-1, 9, 6), # idx 3: h_mid + hchannel(-1, 9, 9) # idx 3: h_top + ] + hooks = [ + bpin(2, -0.5), # p1 + tpin(8, 6.5), # p2 + bpin(2, 4), # p3 + tpin(8, 9.5) # p4 + ] + # Parallel: bottom-left↔top-left, bottom-right↔top-right + ar = _route_channels(channels, hooks, [(1, 2), (3, 4)]) + + # In v_mid: both nets traverse it but one starts where the other ends + # And they come from opposite sides at that point + # Knock-knee relaxation says they don't overlap + segs_vmid = ar.channel_segments[1] + @test length(segs_vmid) == 2 + @test Paths.segments_overlap(ar, segs_vmid[1], segs_vmid[2]) == false + end + + # ── Track assignment results ───────────────────────────────────────────────── + # Verify that track counts match expectations after full routing. + + @testset "track assignment: crossing" begin + channels = [vchannel(5, -1, 7), hchannel(-1, 9, 0), hchannel(-1, 9, 6)] + hooks = [bpin(2, -0.5), bpin(8, -0.5), tpin(2, 6.5), tpin(8, 6.5)] + ar = ChannelRouter([(1, 4), (2, 3)], hooks, RouteChannel.(channels)) + autoroute!(ar, Paths.StraightAnd90(0.1), 0.1) + + # v_mid gets 2 tracks (full overlap) + @test length(ar.channel_tracks[1]) == 2 + # One horizontal channel gets 2 tracks (post-crossing overlap detected), + # the other gets 1 (pre-crossing, touching but not overlapping) + h_tracks = length(ar.channel_tracks[2]) + length(ar.channel_tracks[3]) + @test h_tracks == 3 + end + + @testset "track assignment: parallel" begin + channels = [ + vchannel(0, -1, 9), + vchannel(10, -1, 9), + hchannel(-1, 11, 0), + hchannel(-1, 11, 4), + hchannel(-1, 11, 8) + ] + hooks = [ + lpin(-0.5, 0), + lpin(-0.5, 4), + lpin(-0.5, 8), + rpin(10.5, 0), + rpin(10.5, 4), + rpin(10.5, 8) + ] + ar = ChannelRouter([(1, 4), (2, 5), (3, 6)], hooks, RouteChannel.(channels)) + autoroute!(ar, Paths.StraightAnd90(0.1), 0.1) + + @test all(length.(ar.net_wires) .> 0) + # No channel should need more than 1 track (non-crossing parallel routes) + @test all(length.(ar.channel_tracks) .<= 1) + end +end From 76ea73c1af8d56d1af45d4bebfbd950e245ff5a3 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 17 Apr 2026 15:40:46 +0200 Subject: [PATCH 17/32] Refactor for DL-independent channel routing core --- src/paths/channel_autorouter.jl | 782 ++--------------------------- src/paths/channel_routing_core.jl | 789 ++++++++++++++++++++++++++++++ src/paths/channels.jl | 21 - src/paths/paths.jl | 1 + test/autoroute_examples.jl | 8 +- test/test_channels.jl | 1 - 6 files changed, 841 insertions(+), 761 deletions(-) create mode 100644 src/paths/channel_routing_core.jl diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 30e15590e..131d91f9e 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -1,51 +1,9 @@ -# Original: Hashimoto and Stevens https://cs.baylor.edu/~maurer/CSI5346/originalCR.pdf -# VCG, HCG/Zone representation, doglegs, merging: Yoshimura and Kuh https://my.ece.utah.edu/~kalla/phy_des/yk.pdf -# Crossing-aware: Condrat, Kalla, Blair https://my.ece.utah.edu/~kalla/papers/condrat_crossing-aware_channel_routing_for_photonic_waveguides.pdf +# DeviceLayout-specific ChannelRouter implementation. +# Pure algorithmic core is in channel_routing_core.jl. -import Graphs: - SimpleGraph, - SimpleDiGraph, - nv, - ne, - add_edge!, - rem_edge!, - adjacency_matrix, - edges, - inneighbors, - neighbors, - outneighbors, - maximal_cliques, - dijkstra_shortest_paths, - enumerate_paths, - topological_sort_by_dfs -import SparseArrays: sparse -import BipartiteMatching - -# TrackWireSegment = (net, lengthwise channel, start vertex, end vertex) -# vertex is the channel index or, if the start/end is a pin, the graph index for that pin -struct TrackWireSegment - net_index::Int - running_channel::Int - start_vertex::Int - stop_vertex::Int -end - -bounding_channels(ws::TrackWireSegment) = (ws.start_vertex, ws.stop_vertex) -net_index(ws::TrackWireSegment) = ws.net_index -running_channel(ws::TrackWireSegment) = ws.running_channel - -const Track = Vector{TrackWireSegment} -const NetWire = Vector{TrackWireSegment} # pathlength 1, pathlength 2, dir1, dir2, intersection point const IntersectionInfo{T} = Tuple{T, T, typeof(1.0°), typeof(1.0°), Point{T}} -struct AuxiliaryGraph{T, M <: AbstractMatrix{T}} - graph::SimpleGraph{Int} - distmx::M - aux_to_edge::Vector{Tuple{Int, Int}} # aux vertex → original (u, v) edge - edge_to_aux::Dict{Tuple{Int, Int}, Int} # original (u, v) edge → aux vertex -end - """ ChannelRouter{T <: Coordinate} @@ -63,7 +21,7 @@ choosing which channels the net's wire passes through on its way from one pin to is "track assigment", which proceeds channel by channel. Wire segments are assigned to tracks within the channel such that wire segments in the same track do not overlap. """ -struct ChannelRouter{T <: Coordinate} +struct ChannelRouter{T <: Coordinate} <: AbstractChannelProblem{T} channel_graph::SimpleGraph{Int} # Channels/pins are vertices, intersections are edges net_pins::Vector{Tuple{Int, Int}} # Pairs of pins (indices in `pins`) net_wires::Vector{NetWire} # Wire segments connecting pins for each net @@ -133,6 +91,8 @@ function ChannelRouter(channels::Vector{RouteChannel{T}}) where {T} ) end +# ──── AbstractChannelProblem interface implementation ──────────────────────── + Base.broadcastable(x::ChannelRouter) = Ref(x) num_channels(ar::ChannelRouter) = length(ar.channels) num_nets(ar::ChannelRouter) = length(ar.net_pins) @@ -156,12 +116,9 @@ end channel_segments(ar::ChannelRouter, channel) = ar.channel_segments[channel] channel_tracks(ar::ChannelRouter, channel) = ar.channel_tracks[channel] num_tracks(ar::ChannelRouter, channel) = length(ar.channel_tracks[channel]) -pin_to_graphidx(ar::ChannelRouter, p::Int) = p + num_channels(ar) -graphidx_to_pin(ar::ChannelRouter, graphidx::Int) = graphidx - num_channels(ar) -is_pin(ar::ChannelRouter, graphidx) = graphidx > num_channels(ar) -adjoining_channel(ar::ChannelRouter, pin) = - neighbors(channel_graph(ar), pin_to_graphidx(ar, pin))[1] + channel_intersection(ar, s1, s2) = ar.channel_intersections[_swap(s1, s2)] + function pathlength_at_intersection( ar::ChannelRouter{T}, running_channel, @@ -179,24 +136,36 @@ end function direction_at_intersection(ar::ChannelRouter, running_channel, intersecting_channel) # Intersecting channel is zero where a wire segment hits a pin if iszero(running_channel) || iszero(intersecting_channel) - return 0.0° + return 0.0 end ixn_info = channel_intersection(ar, running_channel, intersecting_channel) - running_channel < intersecting_channel && return ixn_info[3] - return ixn_info[4] + angle_unitful = running_channel < intersecting_channel ? ixn_info[3] : ixn_info[4] + return rem2pi(uconvert(NoUnits, angle_unitful), RoundNearest) end segment_waypoint(ar::ChannelRouter, ws::TrackWireSegment) = ar.segment_waypoints[ws] -_swap(x, y) = (y > x ? (x, y) : (y, x)) -function _shared_vertex(edge_a::Tuple{Int, Int}, edge_b::Tuple{Int, Int}) - a1, a2 = edge_a - b1, b2 = edge_b - a1 in (b1, b2) && return a1 - a2 in (b1, b2) && return a2 - return error("Edges $edge_a and $edge_b share no vertex") +function segment_offset( + ar::ChannelRouter{T}, + ws::TrackWireSegment, + s...; + use_wire_direction=true +) where {T} + channel_idx = running_channel(ws) + is_pin(ar, channel_idx) && return zero(T) + track_idx = segment_track(ar, ws) + isnothing(track_idx) && return zero(T) + reversed = use_wire_direction && against_channel(ar, ws) + return track_section_offset( + length(ar.channel_tracks[channel_idx]), + Paths.width(ar.channels[channel_idx].node.sty, s...), + track_idx; + reversed + ) end +# ──── DL-dependent geometry functions ──────────────────────────────────────── + pathlength_from_start(channel, node, s) = pathlength(channel[1:(node - 1)]) + s # Build graph with pins/channels as vertices and intersections as edges @@ -292,18 +261,6 @@ function print_segments(ar::ChannelRouter, net) end end -""" - segment_track(ar::ChannelRouter, ws::TrackWireSegment) - -The track index of `ws`, or `nothing` if no track has been assigned. -""" -function segment_track(ar::ChannelRouter, ws::TrackWireSegment) - channel_idx = running_channel(ws) - tracks = channel_tracks(ar, channel_idx) - track_idx = findfirst((c) -> ws in c, tracks) - return track_idx -end - """ segment_direction(ar::ChannelRouter, ws::TrackWireSegment) @@ -315,669 +272,6 @@ function segment_direction(ar::ChannelRouter, ws::TrackWireSegment, s) return direction(seg, s) end -function segment_offset( - ar::ChannelRouter{T}, - ws::TrackWireSegment, - s...; - use_wire_direction=true -) where {T} - channel_idx = running_channel(ws) - is_pin(ar, channel_idx) && return zero(T) - track_idx = segment_track(ar, ws) - isnothing(track_idx) && return zero(T) - reversed = use_wire_direction && against_channel(ar, ws) - return track_section_offset( - length(ar.channel_tracks[channel_idx]), - Paths.width(ar.channels[channel_idx].node.sty, s...), - track_idx; - reversed - ) -end - -""" - interval(ar::ChannelRouter, ws::TrackWireSegment) - -A tuple `(start, stop)` of approximate channel pathlengths at which `ws` starts and stops. - -If tracks have been assigned to the previous or next segments, then the track offset is -taken into account. Otherwise, the start and stop are at the centre line of the intersecting channel. - -The interval is always a tuple with the lower bound as the first element. -""" -function interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=true) - return _swap(unsorted_interval(ar, ws; use_track)...) -end - -function unsorted_interval(ar::ChannelRouter, ws::TrackWireSegment; use_track=true) - start_channel, stop_channel = bounding_channels(ws) - channel_idx = running_channel(ws) - - start_channel, stop_channel = bounding_channels(ws) - s1 = pathlength_at_intersection(ar, channel_idx, start_channel) - s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) - (!use_track || is_pin(ar, channel_idx)) && return _swap(s1, s2) - # Could just do that for all cases - # But if we want to break ties we use offsets from previous/next segments - # Offset sign needs to take into account relative directions of segments in channels - s_start = pathlength_at_intersection(ar, start_channel, channel_idx) - s_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) - pt, nt = prev_next_tendency(ar, ws) - start_dir = direction_at_intersection(ar, start_channel, channel_idx) - dir1 = direction_at_intersection(ar, channel_idx, start_channel) - dir2 = direction_at_intersection(ar, channel_idx, stop_channel) - stop_dir = direction_at_intersection(ar, stop_channel, channel_idx) - # Offset also depends neighbor track offsets - α_ixn_start = (dir1 - start_dir) - α_ixn_stop = (stop_dir - dir2) - prev_offset_proj = - segment_offset(ar, prev(ar, ws), s_start; use_wire_direction=false) / - sin(α_ixn_start) - next_offset_proj = - segment_offset(ar, next(ar, ws), s_stop; use_wire_direction=false) / sin(α_ixn_stop) - # Offset *also* depends on this wire segment's offset at a non-90° intersection - start_offset_proj = - -segment_offset(ar, ws, s_start; use_wire_direction=false) / tan(α_ixn_start) - stop_offset_proj = - -segment_offset(ar, ws, s_stop; use_wire_direction=false) / tan(α_ixn_stop) - # This is approximate on bending or tapered tracks - return s1 + (prev_offset_proj + start_offset_proj), - s2 - (next_offset_proj + stop_offset_proj) -end - -""" - next(ar::ChannelRouter, ws::TrackWireSegment) - -The wire segment after `ws`, with the wire directed from the source to the destination pin. -""" -function next(ar::ChannelRouter, ws::TrackWireSegment) - net_idx = net_index(ws) - segs = net_wire(ar, net_idx) - idx = findfirst(isequal(ws), segs) - if idx == length(segs) - final_pin_idx = pin_to_graphidx(ar, last(net_pins(ar, net_idx))) - return TrackWireSegment(net_idx, final_pin_idx, running_channel(ws), 0) - end - return segs[idx + 1] -end - -""" - prev(ar::ChannelRouter, ws::TrackWireSegment) - -The wire segment before `ws`, with the wire directed from the source to the destination pin. -""" -function prev(ar::ChannelRouter, ws::TrackWireSegment) - net_idx = net_index(ws) - segs = net_wire(ar, net_idx) - idx = findfirst(isequal(ws), segs) - if idx == 1 - first_pin_idx = pin_to_graphidx(ar, first(net_pins(ar, net_idx))) - return TrackWireSegment(net_idx, first_pin_idx, 0, running_channel(ws)) - end - return segs[idx - 1] -end - -""" - build_auxiliary_graph(ar::ChannelRouter) - -Build an auxiliary graph where each vertex represents an intersection point (edge in the -channel graph) and edges connect consecutive intersections along the same channel, weighted -by the physical distance between them. -""" -function build_auxiliary_graph(ar::ChannelRouter{T}) where {T} - g = channel_graph(ar) - n_aux = ne(g) - - # Map channel-graph edges to auxiliary vertices - aux_to_edge = Vector{Tuple{Int, Int}}(undef, n_aux) - edge_to_aux = Dict{Tuple{Int, Int}, Int}() - for (i, e) in enumerate(edges(g)) - key = _swap(e.src, e.dst) - aux_to_edge[i] = key - edge_to_aux[key] = i - end - - # Build auxiliary graph: chain consecutive intersections per channel - aux_g = SimpleGraph(n_aux) - I = Int[] - J = Int[] - V = T[] - for ch = 1:num_channels(ar) - nbs = neighbors(g, ch) - length(nbs) < 2 && continue - - # Collect (pathlength_along_ch, aux_vertex) for each intersection on this channel - ch_ixns = Tuple{T, Int}[ - (pathlength_at_intersection(ar, ch, nb), edge_to_aux[_swap(ch, nb)]) for - nb in nbs - ] - sort!(ch_ixns, by=first) - - # Connect consecutive intersection points - for k = 1:(length(ch_ixns) - 1) - s1, aux1 = ch_ixns[k] - s2, aux2 = ch_ixns[k + 1] - add_edge!(aux_g, aux1, aux2) - w = abs(s2 - s1) - push!(I, aux1) - push!(J, aux2) - push!(V, w) - push!(I, aux2) - push!(J, aux1) - push!(V, w) - end - end - - distmx = sparse(I, J, V, n_aux, n_aux) - return AuxiliaryGraph(aux_g, distmx, aux_to_edge, edge_to_aux) -end - -""" - shortest_path_between_pins(ar::ChannelRouter, pin_1::Int, pin_2::Int, aux::AuxiliaryGraph) - -A shortest path minimizing physical distance (sum of arclengths along channels between -intersection points), using a precomputed [`AuxiliaryGraph`](@ref). - -In both cases, the returned path is a list of vertex indices -`[pin_gidx, ch1, ..., chN, pin_gidx]`. -""" -function shortest_path_between_pins( - ar::ChannelRouter, - p0::Int, - p1::Int, - aux::AuxiliaryGraph -) - pin0_gidx = pin_to_graphidx(ar, p0) - pin1_gidx = pin_to_graphidx(ar, p1) - src_aux = aux.edge_to_aux[_swap(pin0_gidx, adjoining_channel(ar, p0))] - dst_aux = aux.edge_to_aux[_swap(pin1_gidx, adjoining_channel(ar, p1))] - - ds = dijkstra_shortest_paths(aux.graph, src_aux, aux.distmx) - aux_path = enumerate_paths(ds, dst_aux) - isempty(aux_path) && error("No path between pins $p0 and $p1") - - # Convert aux vertices back to channel-graph vertex sequence. - # Skip consecutive duplicates: the aux path may traverse multiple intersections on - # the same channel, which just means a longer segment on that channel. - path = Int[pin0_gidx] - for i = 1:(length(aux_path) - 1) - ch = _shared_vertex(aux.aux_to_edge[aux_path[i]], aux.aux_to_edge[aux_path[i + 1]]) - if ch != last(path) - push!(path, ch) - end - end - push!(path, pin1_gidx) - return path -end - -""" - assign_channels!(ar::ChannelRouter) - -Performs channel assignment for `ar`. - -Finds a shortest path between pins minimizing physical distance along channels -(sum of arclengths between intersection points), using an auxiliary intersection-point -graph with Dijkstra's algorithm. -Does not currently take congestion, crossings, or channel capacity into account. -""" -function assign_channels!( - ar::ChannelRouter; - net_indices=eachindex(ar.net_pins), - fixed_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}() -) - aux = build_auxiliary_graph(ar) - for (idx_net, net) in zip(net_indices, ar.net_pins[net_indices]) - p0, p1 = net - path = if idx_net in keys(fixed_paths) - [ - pin_to_graphidx(ar, p0) - fixed_paths[idx_net] - pin_to_graphidx(ar, p1) - ] - else - shortest_path_between_pins(ar, p0, p1, aux) - end - ixns = [(path[i], path[i + 1]) for i = 1:(length(path) - 1)] - segs = [(ixns[i], ixns[i + 1]) for i = 1:(length(ixns) - 1)] - for (channel, seg) in zip(path[2:(end - 1)], segs) - ws = TrackWireSegment(idx_net, channel, first(seg[1]), last(seg[2])) - push!(net_wire(ar, idx_net), ws) - push!(channel_segments(ar, channel), ws) - end - end -end - -""" - assign_tracks!(ar::ChannelRouter) - -Performs track assigment for `ar`. -""" -function assign_tracks!(ar::ChannelRouter{T}) where {T} - # Order of channels will change results - # Generally you want early channels to be those with more pins adjoining them - # Or fewer non-pinned segments - # Because those channels will inform constraints on later channels - # For now the user can worry about that - for channel = 1:num_channels(ar) - assign_tracks_matching!(ar, channel) - end -end - -function merge_in_vcg!(vcg, v1, v2) - # Children of one become children of the other - for child in outneighbors(vcg, v1) # Directed neighbor v1 -> child - add_edge!(vcg, v2, child) - end - for child in outneighbors(vcg, v2) # Directed neighbor v2 -> child - add_edge!(vcg, v1, child) - end - # Parents of one become parents of the other - for parent in inneighbors(vcg, v1) # parent -> v1 - add_edge!(vcg, parent, v2) - end - for parent in inneighbors(vcg, v2) - add_edge!(vcg, parent, v1) - end -end - -function assign_tracks_matching!(ar, channel) - # Yoshimura and Kuh Algorithm #2 - # We will not check whether there are cycles in the vertical constraint graph, just assume - vcg, zone_ig = channel_problem_graphs(ar, channel) - zones = maximal_cliques(zone_ig) - isempty(zones) && return - # Need to sort zones left to right (by their minimal element) - sort!(zones, by=minimum) - # Same with wire segments, to follow same indexing as graphs - wiresegs_ascending = sort(channel_segments(ar, channel), by=(ws) -> interval(ar, ws)) - L = Set{Int}() - active = Set(zones[1]) - merged_groups = Dict{Int, Vector{Int}}() - merged_into = Dict{Int, Int}() # merged_into[x] = y means y was merged into x with y > x - merging_graph = SimpleGraph(length(wiresegs_ascending)) - matching = Dict{Int, Int}() - R = Set{Int}() - - for (zone, nextzone) in zip(zones, [zones[2:end]..., Int[]]) # Extra empty zone at end - # Merge nets terminating in current zone to left side - if length(zones) > 1 - for v in collect(active) # v was in nextzone last round - if !(v in nextzone) # v terminates in current zone - pop!(active, v) # no longer active - push!(L, v) # add to left side - if haskey(matching, v) - # Matched in previous round, so update VCG - merge_in_vcg!(vcg, matching[v], v) - # Record merge - merged_into[matching[v]] = v - group = get!(merged_groups, matching[v], Int[matching[v]]) - push!(group, v) - merged_groups[v] = group - # Replace match with v as representative of merged group in L - pop!(L, matching[v]) # match must have been in L - end - v in R && pop!(R, v) - end - end - end - # Add nets starting in next zone to right side - for v in nextzone - if !(v in active) - push!(R, v) - push!(active, v) - end - end - # Remove all edges in merging graph, start fresh this iteration - merging_graph = SimpleGraph(length(wiresegs_ascending)) - # Add edges between left and right when they can be merged - high_to_low = topological_sort_by_dfs(vcg) - dists_from_r = Dict(r => dag_shortest_paths(vcg, high_to_low, r) for r in R) - for l in L - # Only use rightmost in any merged group - haskey(merged_into, l) && continue - dists_from_l = dag_shortest_paths(vcg, high_to_low, l) - for r in R - mergeable = dists_from_l[r] >= nv(vcg) && dists_from_r[r][l] >= nv(vcg) - !mergeable && continue - if !segments_overlap(ar, wiresegs_ascending[l], wiresegs_ascending[r]) - add_edge!(merging_graph, l, r) - end - end - end - # Find max cardinality valid matching, removing edges as necessary - matching = best_matching!(merging_graph, vcg)[1] # Just the dict, not the indicator - end - # Assign merged groups to tracks according to VCG - tracks = channel_tracks(ar, channel) - # At the end of this process, segments are merged into layers in the VCG - # The longest directed path gives a representative of each merged group where track height is max - # But VCG may be a partial order so use topological sort - high_to_low = topological_sort_by_dfs(vcg) # If vcg was acyclic to begin with, it is still acyclic - for v = 1:nv(vcg) - if !haskey(merged_groups, v) - merged_groups[v] = [v] - end - end - assigned = Int[] - for v in high_to_low # high in vcg => low track index - # Create a track with `v` and all others merged with it - v in assigned && continue - push!(tracks, wiresegs_ascending[merged_groups[v]]) - append!(assigned, merged_groups[v]) - end -end - -function segments_overlap(ar, seg1, seg2) - low1, high1 = interval(ar, seg1) - low2, high2 = interval(ar, seg2) - if low1 <= low2 # segments are in ascending order - low2 < high1 && return true - low2 == high1 || return false - # Boundary case: check if knock-knee is actually possible - return _same_tendency_at_boundary(ar, seg1, seg2) - else # descending order, just reverse the roles - low1 < high2 && return true - low1 == high2 || return false - return _same_tendency_at_boundary(ar, seg2, seg1) - end -end - -function _same_tendency_at_boundary(ar, seg1, seg2) - p1, n1 = bounding_channels(seg1) - p2, n2 = bounding_channels(seg2) - shared_boundary = [2 - 1 * (p1 == p2 || p1 == n2), 2 - 1 * (p2 == p1 || p2 == n1)] - t1 = prev_next_tendency(ar, seg1) - t2 = prev_next_tendency(ar, seg2) - return t1[shared_boundary[1]] == t2[shared_boundary[2]] -end - -function best_matching!(merging_graph, vcg) - # Collect set of edges to remove - to_remove = Set{Tuple{Int, Int}}() - # Create a temporary copy to help find problematic edges - edge_selection_graph = copy(merging_graph) - working_vcg = copy(vcg) - # Collect "deleted" vertices in edge_selection_graph - # Vertices aren't labelled, just indexed, so we don't actually delete them - ignored = Set{Int}() - # While working graphs have vertices, keep collecting edges to remove from merging graph - while length(ignored) < nv(edge_selection_graph) - # 1. Find nodes with no VCG ancestors, remove edges between them - # 2. If there are nodes with no edges in edge_selection_graph, remove them from working graphs and go back to 1 - # 3. Now the node with the fewest edges has at least one edge - # That edge corresponds to a merging that would cause a problem in the VCG - # so then we mark it for removal from the merging graph - # and remove it from our working graphs - min_neighbors = nv(edge_selection_graph) - v_min_neighbors = 0 - orphans = true - while orphans - min_neighbors = nv(edge_selection_graph) - v_min_neighbors = 0 - orphans = false - # Remove edges between vertices with no ancestors - # Then they will not be selected for removal from `merging_graph` - no_ancestors = Int[] - for v = 1:nv(edge_selection_graph) - v in ignored && continue - if isempty(inneighbors(working_vcg, v)) || - all([w in ignored for w in inneighbors(working_vcg, v)]) - # v has no surviving ancestors, remove edges to other such vertices - for w in no_ancestors - rem_edge!(edge_selection_graph, v, w) - end - push!(no_ancestors, v) - end - end - # Find nodes with minimum number of edges - for v = 1:nv(edge_selection_graph) - v in ignored && continue - nbs = neighbors(edge_selection_graph, v) - if length(nbs) < min_neighbors - min_neighbors = length(nbs) - v_min_neighbors = v - end - # If any nodes have no edges, remove them and go back - if isempty(nbs) - min_neighbors = 0 - orphans = true - push!(ignored, v) - # No need to remove edges from edge_selection_graph because it doesn't have any - # "Removing" v from working VCG means merging edges betwen its parents and children - for parent in inneighbors(working_vcg, v) - for child in outneighbors(working_vcg, v) - add_edge!(working_vcg, parent, child) - end - end - end - end - end - # Remove a node with fewest edges, remove and collect those edges - if !iszero(v_min_neighbors) && min_neighbors > 0 - for nb in copy(neighbors(edge_selection_graph, v_min_neighbors)) # copy bc neighbors is changing in the loop - rem_edge!(edge_selection_graph, v_min_neighbors, nb) - push!(to_remove, (v_min_neighbors, nb)) - end - push!(ignored, v_min_neighbors) - # "Removing" v from working VCG means merging edges betwen its parents and children - for parent in inneighbors(working_vcg, v_min_neighbors) - for child in outneighbors(working_vcg, v_min_neighbors) - add_edge!(working_vcg, parent, child) - end - end - end - end - # Remove marked edges - for edge in to_remove - rem_edge!(merging_graph, edge...) - end - # Any matching is feasible now that we've removed marked edges - return BipartiteMatching.findmaxcardinalitybipartitematching( - BitMatrix(adjacency_matrix(merging_graph)) - ) -end - -function dag_shortest_paths(dag, v_sorted, s) - d = fill(nv(dag), nv(dag)) - d[s] = 0 - for i = findfirst(v -> v == s, v_sorted):nv(dag) - u = v_sorted[i] - for v in outneighbors(dag, u) - if d[v] > d[u] + 1 - d[v] = d[u] + 1 - end - end - end - return d -end - -function against_channel(ar, wireseg) - s1, s2 = unsorted_interval(ar, wireseg) - return s1 > s2 -end - -function channel_problem_graphs(ar::ChannelRouter, channel) - wiresegs_ascending = sort(channel_segments(ar, channel), by=(ws) -> interval(ar, ws)) - # Y&K zone representation as interval graph - # Edge between each pair of segments that overlap - zone_ig = SimpleGraph(length(wiresegs_ascending)) - # Condrat et al. VCG with avoidable crossings as constraints - # Not handled: constraints from vertically aligned pin positions - vcg = SimpleDiGraph(length(wiresegs_ascending)) # just a fresh graph - for (idx1, seg1) in pairs(wiresegs_ascending) - low1, high1 = interval(ar, seg1) - for (idx2, seg2) in collect(pairs(wiresegs_ascending))[(idx1 + 1):end] - low2, high2 = interval(ar, seg2) - - # If there is no overlap, break and move on to next seg1 - # Use >= instead of > to potentially allow knock-knees - low2 >= high1 && break # All subsequent seg2 have low2 >= high1 - # There is overlap, so add an edge to the interval graph - add_edge!(zone_ig, idx1, idx2) - # Now check if crossing is unavoidable - pt1, nt1 = prev_next_tendency(ar, seg1) - pt2, nt2 = prev_next_tendency(ar, seg2) - # If seg1 and seg2 enter and exit at the same place with same tendency, then crossing - # may be avoidable, but this channel can't say which goes on top yet - (low1 == high1 && low2 == high2 && pt1 == pt2 && nt1 == nt2) && break - low1_tend, high1_tend = against_channel(ar, seg1) ? (nt1, pt1) : (pt1, nt1) - low2_tend, high2_tend = against_channel(ar, seg2) ? (nt2, pt2) : (pt2, nt2) - avoidable = is_avoidable( - low1, - high1, - low2, - high2, - low1_tend, - high1_tend, - low2_tend, - high2_tend - ) - !avoidable && continue - - # Crossing is avoidable, so add a constraint - # Determine which goes on top based on the lower bound tendency of seg2 - # Is prev or next the lower bound? - # top is rightmost segment iff its lower bound tends towards higher (lower index) tracks - top = (idx1, idx2)[1 + (low2_tend == 1)] - bottom = (idx1, idx2)[2 - (low2_tend == 1)] - add_edge!(vcg, top, bottom) # VCG has edge from higher to lower tracks - end - end - return vcg, zone_ig -end - -function is_avoidable( - low1, - high1, - low2, - high2, - low1_tend, - high1_tend, - low2_tend, - high2_tend -) - if high1 < high2 - # 1 ____ - # 2 ____ - order = [1, 2, 1, 2] # low1 <= low2 < high1 < high2 - ordered_tendency = [low1_tend, low2_tend, high1_tend, high2_tend] - else - # 1 _____ - # 2 __ - order = [1, 2, 2, 1] # low1 <= low2 < high2 <= high1 - ordered_tendency = [low1_tend, low2_tend, high2_tend, high1_tend] - end - up = (ordered_tendency .== 1) - down = (!).(up) - ccw_order = [reverse(order[up]); order[down]] - # Crossing is avoidable if same net has both endpoints adjacent - # on the clock - avoidable = (ccw_order[1] == ccw_order[2] || ccw_order[2] == ccw_order[3]) - # Also, if seg1 and seg2 have an endpoint at the same place and don't make an X, - # then crossing in this channel may be avoidable but depends on - # other channel; assume other channel will agree - depends = - ((low1 == low2) || high1 == high2) && - ((low1_tend == low2_tend) || (high1_tend == high2_tend)) - avoidable = (avoidable || depends) - return avoidable -end - -# +1 if segment crosses over low track index in ws's channel -function prev_next_tendency(ar, ws; use_wire_direction=true) - channel_idx = running_channel(ws) - start_channel, stop_channel = bounding_channels(ws) - # Distances along bounding and running channels - s_along_start = pathlength_at_intersection(ar, start_channel, channel_idx) - s1 = pathlength_at_intersection(ar, channel_idx, start_channel) - s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) - s_along_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) - # Directions of bounding and running channels - start_dir = direction_at_intersection(ar, start_channel, channel_idx) - dir1 = direction_at_intersection(ar, channel_idx, start_channel) - dir2 = direction_at_intersection(ar, channel_idx, stop_channel) - stop_dir = direction_at_intersection(ar, stop_channel, channel_idx) - # Tendencies - ## +ve = wire makes CCW turns - ## But actual bends depend on direction of wires vs channels - ### Signs of angles made by channel intersections - sgn_bend1 = sign(rem2pi(uconvert(NoUnits, dir1 - start_dir), RoundNearest)) - sgn_bend2 = sign(rem2pi(uconvert(NoUnits, stop_dir - dir2), RoundNearest)) - !use_wire_direction && return (sgn_bend1, sgn_bend2) - ### Need to multiply according to direction in channel - ### Is prev upper-bounded by ws? Then it goes along with channel - sgn_start = s_along_start >= last(interval(ar, prev(ar, ws), use_track=false)) ? 1 : -1 - ### Is next upper-bounded by ws? Then it goes against channel - sgn_stop = s_along_stop >= last(interval(ar, next(ar, ws), use_track=false)) ? -1 : 1 - ### Bend signs get another -1 if ws runs opposite to its channel direction - ### But then tendency definition is reversed also - return (sgn_start * sgn_bend1, sgn_stop * sgn_bend2) -end - -""" - make_routes!(ar::ChannelRouter, rule; net_indices=eachindex(ar.net_pins)) - -Using channel and track assignments, create `Route`s for the nets in `net_indices`. -""" -function make_routes!(ar::ChannelRouter{T}, rule) where {T} - empty!(ar.net_routes) - for idx_net in eachindex(ar.net_pins) - p0, p1 = pin_coordinates.(ar, net_pins(ar, idx_net)) - α0, α1 = pin_direction.(ar, net_pins(ar, idx_net)) - rt = Route(rule, p0, p1, α0, α1 + pi) - push!(ar.net_routes, rt) - end - return ar.net_routes -end - -function _delete_segment!(ar, ws; reset_tracks=true, from_net=true) - # Deletes the wire segment `ws` from `ar`. - - # Delete the wire segment in channel_segments - s = running_channel(ws) - deleteat!(channel_segments(ar, s), findfirst(isequal(ws), channel_segments(ar, s))) - - # Delete the segment from its track - c_idx = segment_track(ar, ws) - if !isnothing(c_idx) - if reset_tracks # by default, reset all track assignments in this channel - empty!(channel_tracks(ar, s)) - else - deleteat!( - channel_tracks(ar, s)[c_idx], - findfirst(isequal(ws), channel_tracks(ar, s)[c_idx]) - ) - end - end - # Delete the segment waypoint - delete!(ar.segment_waypoints, ws) - - # if from_net, delete ws from net_wires - # We set from_net to false when looping over net segments so it's not changing under us - return from_net && deleteat!( - net_wire(ar, net_index(ws)), - findfirst(isequal(ws), net_wire(ar, net_index(ws))) - ) -end - -""" - reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) - -Resets the nets with `net_indices` to their unrouted state. - -If `reset_tracks` is `true`, then all channels used by nets being reset will also have their -track assignments removed. -""" -function reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) - for segs in net_wire.(ar, net_indices) - for ws in segs - # delete segment from the router - # don't delete it from net yet, we'll do that after this loop - _delete_segment!(ar, ws, reset_tracks=reset_tracks, from_net=false) - end - empty!(segs) - end -end - """ autoroute!(ar::ChannelRouter, transition_rule; net_indices=eachindex(ar.net_pins), fixed_channel_paths::Dict{Int,Vector{Int}}=Dict()) @@ -1025,6 +319,24 @@ function set_waypoint!( return ar.segment_waypoints[ws] = (new_point, dir) end +######## Route construction + +""" + make_routes!(ar::ChannelRouter, rule; net_indices=eachindex(ar.net_pins)) + +Using channel and track assignments, create `Route`s for the nets in `net_indices`. +""" +function make_routes!(ar::ChannelRouter{T}, rule) where {T} + empty!(ar.net_routes) + for idx_net in eachindex(ar.net_pins) + p0, p1 = pin_coordinates.(ar, net_pins(ar, idx_net)) + α0, α1 = pin_direction.(ar, net_pins(ar, idx_net)) + rt = Route(rule, p0, p1, α0, α1 + pi) + push!(ar.net_routes, rt) + end + return ar.net_routes +end + ######## Visualization """ diff --git a/src/paths/channel_routing_core.jl b/src/paths/channel_routing_core.jl new file mode 100644 index 000000000..24e85607c --- /dev/null +++ b/src/paths/channel_routing_core.jl @@ -0,0 +1,789 @@ +# Pure algorithmic core for channel routing. +# No DeviceLayout geometry types — communicates through AbstractChannelProblem interface. +# +# References: +# Original: Hashimoto and Stevens https://cs.baylor.edu/~maurer/CSI5346/originalCR.pdf +# VCG, HCG/Zone representation, doglegs, merging: Yoshimura and Kuh https://my.ece.utah.edu/~kalla/phy_des/yk.pdf +# Crossing-aware: Condrat, Kalla, Blair https://my.ece.utah.edu/~kalla/papers/condrat_crossing-aware_channel_routing_for_photonic_waveguides.pdf + +import Graphs: + SimpleGraph, + SimpleDiGraph, + nv, + ne, + add_edge!, + rem_edge!, + adjacency_matrix, + edges, + inneighbors, + neighbors, + outneighbors, + maximal_cliques, + dijkstra_shortest_paths, + enumerate_paths, + topological_sort_by_dfs +import SparseArrays: sparse +import BipartiteMatching + +# ──── Pure types ───────────────────────────────────────────────────────────── + +# TrackWireSegment = (net, lengthwise channel, start vertex, end vertex) +# vertex is the channel index or, if the start/end is a pin, the graph index for that pin +struct TrackWireSegment + net_index::Int + running_channel::Int + start_vertex::Int + stop_vertex::Int +end + +bounding_channels(ws::TrackWireSegment) = (ws.start_vertex, ws.stop_vertex) +net_index(ws::TrackWireSegment) = ws.net_index +running_channel(ws::TrackWireSegment) = ws.running_channel + +const Track = Vector{TrackWireSegment} +const NetWire = Vector{TrackWireSegment} + +struct AuxiliaryGraph{T, M <: AbstractMatrix{T}} + graph::SimpleGraph{Int} + distmx::M + aux_to_edge::Vector{Tuple{Int, Int}} # aux vertex → original (u, v) edge + edge_to_aux::Dict{Tuple{Int, Int}, Int} # original (u, v) edge → aux vertex +end + +# ──── Abstract interface ───────────────────────────────────────────────────── + +""" + AbstractChannelProblem{T} + +Abstract supertype for channel routing problems. + +Subtypes must implement the following interface functions so that the core +channel-routing algorithms can operate without knowledge of the underlying +geometry representation: + +- `channel_graph(prob)::SimpleGraph{Int}` +- `num_channels(prob)::Int` +- `num_nets(prob)::Int` +- `num_pins(prob)::Int` +- `net_pins(prob, net)::Tuple{Int,Int}` +- `net_wire(prob, net)::NetWire` +- `channel_segments(prob, ch)::Vector{TrackWireSegment}` +- `channel_tracks(prob, ch)::Vector{Track}` +- `num_tracks(prob, ch)::Int` +- `pathlength_at_intersection(prob, ch1, ch2)::T` +- `direction_at_intersection(prob, ch1, ch2)::Float64` (radians) +- `channel_width(prob, ch, s)::T` +- `is_pin(prob, idx)::Bool` +- `segment_offset(prob, ws, s...; use_wire_direction)::T` +""" +abstract type AbstractChannelProblem{T} end + +function channel_graph end +function num_channels end +function num_nets end +function num_pins end +function net_pins end +function net_wire end +function channel_segments end +function channel_tracks end +function num_tracks end +function pathlength_at_intersection end +function direction_at_intersection end +function channel_width end +function is_pin end +function segment_offset end + +# ──── Pure utility functions ───────────────────────────────────────────────── + +_swap(x, y) = (y > x ? (x, y) : (y, x)) + +function _shared_vertex(edge_a::Tuple{Int, Int}, edge_b::Tuple{Int, Int}) + a1, a2 = edge_a + b1, b2 = edge_b + a1 in (b1, b2) && return a1 + a2 in (b1, b2) && return a2 + return error("Edges $edge_a and $edge_b share no vertex") +end + +pin_to_graphidx(ar::AbstractChannelProblem, p::Int) = p + num_channels(ar) +graphidx_to_pin(ar::AbstractChannelProblem, graphidx::Int) = graphidx - num_channels(ar) +is_pin(ar::AbstractChannelProblem, graphidx) = graphidx > num_channels(ar) +adjoining_channel(ar::AbstractChannelProblem, pin) = + neighbors(channel_graph(ar), pin_to_graphidx(ar, pin))[1] + +# Offset coordinate or function for the section of track with given width +function track_section_offset( + n_tracks, + section_width::Number, + 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 + +# ──── Track/segment queries ────────────────────────────────────────────────── + +""" + segment_track(ar::AbstractChannelProblem, ws::TrackWireSegment) + +The track index of `ws`, or `nothing` if no track has been assigned. +""" +function segment_track(ar::AbstractChannelProblem, ws::TrackWireSegment) + channel_idx = running_channel(ws) + tracks = channel_tracks(ar, channel_idx) + track_idx = findfirst((c) -> ws in c, tracks) + return track_idx +end + +""" + next(ar::AbstractChannelProblem, ws::TrackWireSegment) + +The wire segment after `ws`, with the wire directed from the source to the destination pin. +""" +function next(ar::AbstractChannelProblem, ws::TrackWireSegment) + net_idx = net_index(ws) + segs = net_wire(ar, net_idx) + idx = findfirst(isequal(ws), segs) + if idx == length(segs) + final_pin_idx = pin_to_graphidx(ar, last(net_pins(ar, net_idx))) + return TrackWireSegment(net_idx, final_pin_idx, running_channel(ws), 0) + end + return segs[idx + 1] +end + +""" + prev(ar::AbstractChannelProblem, ws::TrackWireSegment) + +The wire segment before `ws`, with the wire directed from the source to the destination pin. +""" +function prev(ar::AbstractChannelProblem, ws::TrackWireSegment) + net_idx = net_index(ws) + segs = net_wire(ar, net_idx) + idx = findfirst(isequal(ws), segs) + if idx == 1 + first_pin_idx = pin_to_graphidx(ar, first(net_pins(ar, net_idx))) + return TrackWireSegment(net_idx, first_pin_idx, 0, running_channel(ws)) + end + return segs[idx - 1] +end + +function against_channel(ar, wireseg) + s1, s2 = unsorted_interval(ar, wireseg) + return s1 > s2 +end + +# ──── Interval computation ─────────────────────────────────────────────────── + +""" + interval(ar::AbstractChannelProblem, ws::TrackWireSegment) + +A tuple `(start, stop)` of approximate channel pathlengths at which `ws` starts and stops. + +If tracks have been assigned to the previous or next segments, then the track offset is +taken into account. Otherwise, the start and stop are at the centre line of the intersecting channel. + +The interval is always a tuple with the lower bound as the first element. +""" +function interval(ar::AbstractChannelProblem, ws::TrackWireSegment; use_track=true) + return _swap(unsorted_interval(ar, ws; use_track)...) +end + +function unsorted_interval(ar::AbstractChannelProblem, ws::TrackWireSegment; use_track=true) + start_channel, stop_channel = bounding_channels(ws) + channel_idx = running_channel(ws) + + start_channel, stop_channel = bounding_channels(ws) + s1 = pathlength_at_intersection(ar, channel_idx, start_channel) + s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) + (!use_track || is_pin(ar, channel_idx)) && return _swap(s1, s2) + # Could just do that for all cases + # But if we want to break ties we use offsets from previous/next segments + # Offset sign needs to take into account relative directions of segments in channels + s_start = pathlength_at_intersection(ar, start_channel, channel_idx) + s_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) + pt, nt = prev_next_tendency(ar, ws) + start_dir = direction_at_intersection(ar, start_channel, channel_idx) + dir1 = direction_at_intersection(ar, channel_idx, start_channel) + dir2 = direction_at_intersection(ar, channel_idx, stop_channel) + stop_dir = direction_at_intersection(ar, stop_channel, channel_idx) + # Offset also depends neighbor track offsets + α_ixn_start = (dir1 - start_dir) + α_ixn_stop = (stop_dir - dir2) + prev_offset_proj = + segment_offset(ar, prev(ar, ws), s_start; use_wire_direction=false) / + sin(α_ixn_start) + next_offset_proj = + segment_offset(ar, next(ar, ws), s_stop; use_wire_direction=false) / sin(α_ixn_stop) + # Offset *also* depends on this wire segment's offset at a non-90° intersection + start_offset_proj = + -segment_offset(ar, ws, s_start; use_wire_direction=false) / tan(α_ixn_start) + stop_offset_proj = + -segment_offset(ar, ws, s_stop; use_wire_direction=false) / tan(α_ixn_stop) + # This is approximate on bending or tapered tracks + return s1 + (prev_offset_proj + start_offset_proj), + s2 - (next_offset_proj + stop_offset_proj) +end + +# ──── Tendency computation ─────────────────────────────────────────────────── + +# +1 if segment crosses over low track index in ws's channel +function prev_next_tendency(ar, ws; use_wire_direction=true) + channel_idx = running_channel(ws) + start_channel, stop_channel = bounding_channels(ws) + # Distances along bounding and running channels + s_along_start = pathlength_at_intersection(ar, start_channel, channel_idx) + s1 = pathlength_at_intersection(ar, channel_idx, start_channel) + s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) + s_along_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) + # Directions of bounding and running channels (Float64 radians from interface) + start_dir = direction_at_intersection(ar, start_channel, channel_idx) + dir1 = direction_at_intersection(ar, channel_idx, start_channel) + dir2 = direction_at_intersection(ar, channel_idx, stop_channel) + stop_dir = direction_at_intersection(ar, stop_channel, channel_idx) + # Tendencies + ## +ve = wire makes CCW turns + ## But actual bends depend on direction of wires vs channels + ### Signs of angles made by channel intersections + sgn_bend1 = sign(rem2pi(dir1 - start_dir, RoundNearest)) + sgn_bend2 = sign(rem2pi(stop_dir - dir2, RoundNearest)) + !use_wire_direction && return (sgn_bend1, sgn_bend2) + ### Need to multiply according to direction in channel + ### Is prev upper-bounded by ws? Then it goes along with channel + sgn_start = s_along_start >= last(interval(ar, prev(ar, ws), use_track=false)) ? 1 : -1 + ### Is next upper-bounded by ws? Then it goes against channel + sgn_stop = s_along_stop >= last(interval(ar, next(ar, ws), use_track=false)) ? -1 : 1 + ### Bend signs get another -1 if ws runs opposite to its channel direction + ### But then tendency definition is reversed also + return (sgn_start * sgn_bend1, sgn_stop * sgn_bend2) +end + +# ──── Overlap and avoidability ─────────────────────────────────────────────── + +function segments_overlap(ar, seg1, seg2) + low1, high1 = interval(ar, seg1) + low2, high2 = interval(ar, seg2) + if low1 <= low2 # segments are in ascending order + low2 < high1 && return true + low2 == high1 || return false + # Boundary case: check if knock-knee is actually possible + return _same_tendency_at_boundary(ar, seg1, seg2) + else # descending order, just reverse the roles + low1 < high2 && return true + low1 == high2 || return false + return _same_tendency_at_boundary(ar, seg2, seg1) + end +end + +function _same_tendency_at_boundary(ar, seg1, seg2) + p1, n1 = bounding_channels(seg1) + p2, n2 = bounding_channels(seg2) + shared_boundary = [2 - 1 * (p1 == p2 || p1 == n2), 2 - 1 * (p2 == p1 || p2 == n1)] + t1 = prev_next_tendency(ar, seg1) + t2 = prev_next_tendency(ar, seg2) + return t1[shared_boundary[1]] == t2[shared_boundary[2]] +end + +function is_avoidable( + low1, + high1, + low2, + high2, + low1_tend, + high1_tend, + low2_tend, + high2_tend +) + if high1 < high2 + # 1 ____ + # 2 ____ + order = [1, 2, 1, 2] # low1 <= low2 < high1 < high2 + ordered_tendency = [low1_tend, low2_tend, high1_tend, high2_tend] + else + # 1 _____ + # 2 __ + order = [1, 2, 2, 1] # low1 <= low2 < high2 <= high1 + ordered_tendency = [low1_tend, low2_tend, high2_tend, high1_tend] + end + up = (ordered_tendency .== 1) + down = (!).(up) + ccw_order = [reverse(order[up]); order[down]] + # Crossing is avoidable if same net has both endpoints adjacent + # on the clock + avoidable = (ccw_order[1] == ccw_order[2] || ccw_order[2] == ccw_order[3]) + # Also, if seg1 and seg2 have an endpoint at the same place and don't make an X, + # then crossing in this channel may be avoidable but depends on + # other channel; assume other channel will agree + depends = + ((low1 == low2) || high1 == high2) && + ((low1_tend == low2_tend) || (high1_tend == high2_tend)) + avoidable = (avoidable || depends) + return avoidable +end + +# ──── Graph algorithms ─────────────────────────────────────────────────────── + +""" + build_auxiliary_graph(ar::AbstractChannelProblem) + +Build an auxiliary graph where each vertex represents an intersection point (edge in the +channel graph) and edges connect consecutive intersections along the same channel, weighted +by the physical distance between them. +""" +function build_auxiliary_graph(ar::AbstractChannelProblem{T}) where {T} + g = channel_graph(ar) + n_aux = ne(g) + + # Map channel-graph edges to auxiliary vertices + aux_to_edge = Vector{Tuple{Int, Int}}(undef, n_aux) + edge_to_aux = Dict{Tuple{Int, Int}, Int}() + for (i, e) in enumerate(edges(g)) + key = _swap(e.src, e.dst) + aux_to_edge[i] = key + edge_to_aux[key] = i + end + + # Build auxiliary graph: chain consecutive intersections per channel + aux_g = SimpleGraph(n_aux) + I = Int[] + J = Int[] + V = T[] + for ch = 1:num_channels(ar) + nbs = neighbors(g, ch) + length(nbs) < 2 && continue + + # Collect (pathlength_along_ch, aux_vertex) for each intersection on this channel + ch_ixns = Tuple{T, Int}[ + (pathlength_at_intersection(ar, ch, nb), edge_to_aux[_swap(ch, nb)]) for + nb in nbs + ] + sort!(ch_ixns, by=first) + + # Connect consecutive intersection points + for k = 1:(length(ch_ixns) - 1) + s1, aux1 = ch_ixns[k] + s2, aux2 = ch_ixns[k + 1] + add_edge!(aux_g, aux1, aux2) + w = abs(s2 - s1) + push!(I, aux1) + push!(J, aux2) + push!(V, w) + push!(I, aux2) + push!(J, aux1) + push!(V, w) + end + end + + distmx = sparse(I, J, V, n_aux, n_aux) + return AuxiliaryGraph(aux_g, distmx, aux_to_edge, edge_to_aux) +end + +""" + shortest_path_between_pins(ar::AbstractChannelProblem, pin_1::Int, pin_2::Int, aux::AuxiliaryGraph) + +A shortest path minimizing physical distance (sum of arclengths along channels between +intersection points), using a precomputed [`AuxiliaryGraph`](@ref). + +In both cases, the returned path is a list of vertex indices +`[pin_gidx, ch1, ..., chN, pin_gidx]`. +""" +function shortest_path_between_pins( + ar::AbstractChannelProblem, + p0::Int, + p1::Int, + aux::AuxiliaryGraph +) + pin0_gidx = pin_to_graphidx(ar, p0) + pin1_gidx = pin_to_graphidx(ar, p1) + src_aux = aux.edge_to_aux[_swap(pin0_gidx, adjoining_channel(ar, p0))] + dst_aux = aux.edge_to_aux[_swap(pin1_gidx, adjoining_channel(ar, p1))] + + ds = dijkstra_shortest_paths(aux.graph, src_aux, aux.distmx) + aux_path = enumerate_paths(ds, dst_aux) + isempty(aux_path) && error("No path between pins $p0 and $p1") + + # Convert aux vertices back to channel-graph vertex sequence. + # Skip consecutive duplicates: the aux path may traverse multiple intersections on + # the same channel, which just means a longer segment on that channel. + path = Int[pin0_gidx] + for i = 1:(length(aux_path) - 1) + ch = _shared_vertex(aux.aux_to_edge[aux_path[i]], aux.aux_to_edge[aux_path[i + 1]]) + if ch != last(path) + push!(path, ch) + end + end + push!(path, pin1_gidx) + return path +end + +# ──── Channel assignment ───────────────────────────────────────────────────── + +""" + assign_channels!(ar::AbstractChannelProblem) + +Performs channel assignment for `ar`. + +Finds a shortest path between pins minimizing physical distance along channels +(sum of arclengths between intersection points), using an auxiliary intersection-point +graph with Dijkstra's algorithm. +Does not currently take congestion, crossings, or channel capacity into account. +""" +function assign_channels!( + ar::AbstractChannelProblem; + net_indices=eachindex(ar.net_pins), + fixed_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}() +) + aux = build_auxiliary_graph(ar) + for (idx_net, net) in zip(net_indices, ar.net_pins[net_indices]) + p0, p1 = net + path = if idx_net in keys(fixed_paths) + [ + pin_to_graphidx(ar, p0) + fixed_paths[idx_net] + pin_to_graphidx(ar, p1) + ] + else + shortest_path_between_pins(ar, p0, p1, aux) + end + ixns = [(path[i], path[i + 1]) for i = 1:(length(path) - 1)] + segs = [(ixns[i], ixns[i + 1]) for i = 1:(length(ixns) - 1)] + for (channel, seg) in zip(path[2:(end - 1)], segs) + ws = TrackWireSegment(idx_net, channel, first(seg[1]), last(seg[2])) + push!(net_wire(ar, idx_net), ws) + push!(channel_segments(ar, channel), ws) + end + end +end + +# ──── Track assignment ─────────────────────────────────────────────────────── + +""" + assign_tracks!(ar::AbstractChannelProblem) + +Performs track assigment for `ar`. +""" +function assign_tracks!(ar::AbstractChannelProblem) + # Order of channels will change results + # Generally you want early channels to be those with more pins adjoining them + # Or fewer non-pinned segments + # Because those channels will inform constraints on later channels + # For now the user can worry about that + for channel = 1:num_channels(ar) + assign_tracks_matching!(ar, channel) + end +end + +function merge_in_vcg!(vcg, v1, v2) + # Children of one become children of the other + for child in outneighbors(vcg, v1) # Directed neighbor v1 -> child + add_edge!(vcg, v2, child) + end + for child in outneighbors(vcg, v2) # Directed neighbor v2 -> child + add_edge!(vcg, v1, child) + end + # Parents of one become parents of the other + for parent in inneighbors(vcg, v1) # parent -> v1 + add_edge!(vcg, parent, v2) + end + for parent in inneighbors(vcg, v2) + add_edge!(vcg, parent, v1) + end +end + +function assign_tracks_matching!(ar, channel) + # Yoshimura and Kuh Algorithm #2 + # We will not check whether there are cycles in the vertical constraint graph, just assume + vcg, zone_ig = channel_problem_graphs(ar, channel) + zones = maximal_cliques(zone_ig) + isempty(zones) && return + # Need to sort zones left to right (by their minimal element) + sort!(zones, by=minimum) + # Same with wire segments, to follow same indexing as graphs + wiresegs_ascending = sort(channel_segments(ar, channel), by=(ws) -> interval(ar, ws)) + L = Set{Int}() + active = Set(zones[1]) + merged_groups = Dict{Int, Vector{Int}}() + merged_into = Dict{Int, Int}() # merged_into[x] = y means y was merged into x with y > x + merging_graph = SimpleGraph(length(wiresegs_ascending)) + matching = Dict{Int, Int}() + R = Set{Int}() + + for (zone, nextzone) in zip(zones, [zones[2:end]..., Int[]]) # Extra empty zone at end + # Merge nets terminating in current zone to left side + if length(zones) > 1 + for v in collect(active) # v was in nextzone last round + if !(v in nextzone) # v terminates in current zone + pop!(active, v) # no longer active + push!(L, v) # add to left side + if haskey(matching, v) + # Matched in previous round, so update VCG + merge_in_vcg!(vcg, matching[v], v) + # Record merge + merged_into[matching[v]] = v + group = get!(merged_groups, matching[v], Int[matching[v]]) + push!(group, v) + merged_groups[v] = group + # Replace match with v as representative of merged group in L + pop!(L, matching[v]) # match must have been in L + end + v in R && pop!(R, v) + end + end + end + # Add nets starting in next zone to right side + for v in nextzone + if !(v in active) + push!(R, v) + push!(active, v) + end + end + # Remove all edges in merging graph, start fresh this iteration + merging_graph = SimpleGraph(length(wiresegs_ascending)) + # Add edges between left and right when they can be merged + high_to_low = topological_sort_by_dfs(vcg) + dists_from_r = Dict(r => dag_shortest_paths(vcg, high_to_low, r) for r in R) + for l in L + # Only use rightmost in any merged group + haskey(merged_into, l) && continue + dists_from_l = dag_shortest_paths(vcg, high_to_low, l) + for r in R + mergeable = dists_from_l[r] >= nv(vcg) && dists_from_r[r][l] >= nv(vcg) + !mergeable && continue + if !segments_overlap(ar, wiresegs_ascending[l], wiresegs_ascending[r]) + add_edge!(merging_graph, l, r) + end + end + end + # Find max cardinality valid matching, removing edges as necessary + matching = best_matching!(merging_graph, vcg)[1] # Just the dict, not the indicator + end + # Assign merged groups to tracks according to VCG + tracks = channel_tracks(ar, channel) + # At the end of this process, segments are merged into layers in the VCG + # The longest directed path gives a representative of each merged group where track height is max + # But VCG may be a partial order so use topological sort + high_to_low = topological_sort_by_dfs(vcg) # If vcg was acyclic to begin with, it is still acyclic + for v = 1:nv(vcg) + if !haskey(merged_groups, v) + merged_groups[v] = [v] + end + end + assigned = Int[] + for v in high_to_low # high in vcg => low track index + # Create a track with `v` and all others merged with it + v in assigned && continue + push!(tracks, wiresegs_ascending[merged_groups[v]]) + append!(assigned, merged_groups[v]) + end +end + +function best_matching!(merging_graph, vcg) + # Collect set of edges to remove + to_remove = Set{Tuple{Int, Int}}() + # Create a temporary copy to help find problematic edges + edge_selection_graph = copy(merging_graph) + working_vcg = copy(vcg) + # Collect "deleted" vertices in edge_selection_graph + # Vertices aren't labelled, just indexed, so we don't actually delete them + ignored = Set{Int}() + # While working graphs have vertices, keep collecting edges to remove from merging graph + while length(ignored) < nv(edge_selection_graph) + # 1. Find nodes with no VCG ancestors, remove edges between them + # 2. If there are nodes with no edges in edge_selection_graph, remove them from working graphs and go back to 1 + # 3. Now the node with the fewest edges has at least one edge + # That edge corresponds to a merging that would cause a problem in the VCG + # so then we mark it for removal from the merging graph + # and remove it from our working graphs + min_neighbors = nv(edge_selection_graph) + v_min_neighbors = 0 + orphans = true + while orphans + min_neighbors = nv(edge_selection_graph) + v_min_neighbors = 0 + orphans = false + # Remove edges between vertices with no ancestors + # Then they will not be selected for removal from `merging_graph` + no_ancestors = Int[] + for v = 1:nv(edge_selection_graph) + v in ignored && continue + if isempty(inneighbors(working_vcg, v)) || + all([w in ignored for w in inneighbors(working_vcg, v)]) + # v has no surviving ancestors, remove edges to other such vertices + for w in no_ancestors + rem_edge!(edge_selection_graph, v, w) + end + push!(no_ancestors, v) + end + end + # Find nodes with minimum number of edges + for v = 1:nv(edge_selection_graph) + v in ignored && continue + nbs = neighbors(edge_selection_graph, v) + if length(nbs) < min_neighbors + min_neighbors = length(nbs) + v_min_neighbors = v + end + # If any nodes have no edges, remove them and go back + if isempty(nbs) + min_neighbors = 0 + orphans = true + push!(ignored, v) + # No need to remove edges from edge_selection_graph because it doesn't have any + # "Removing" v from working VCG means merging edges betwen its parents and children + for parent in inneighbors(working_vcg, v) + for child in outneighbors(working_vcg, v) + add_edge!(working_vcg, parent, child) + end + end + end + end + end + # Remove a node with fewest edges, remove and collect those edges + if !iszero(v_min_neighbors) && min_neighbors > 0 + for nb in copy(neighbors(edge_selection_graph, v_min_neighbors)) # copy bc neighbors is changing in the loop + rem_edge!(edge_selection_graph, v_min_neighbors, nb) + push!(to_remove, (v_min_neighbors, nb)) + end + push!(ignored, v_min_neighbors) + # "Removing" v from working VCG means merging edges betwen its parents and children + for parent in inneighbors(working_vcg, v_min_neighbors) + for child in outneighbors(working_vcg, v_min_neighbors) + add_edge!(working_vcg, parent, child) + end + end + end + end + # Remove marked edges + for edge in to_remove + rem_edge!(merging_graph, edge...) + end + # Any matching is feasible now that we've removed marked edges + return BipartiteMatching.findmaxcardinalitybipartitematching( + BitMatrix(adjacency_matrix(merging_graph)) + ) +end + +function dag_shortest_paths(dag, v_sorted, s) + d = fill(nv(dag), nv(dag)) + d[s] = 0 + for i = findfirst(v -> v == s, v_sorted):nv(dag) + u = v_sorted[i] + for v in outneighbors(dag, u) + if d[v] > d[u] + 1 + d[v] = d[u] + 1 + end + end + end + return d +end + +function channel_problem_graphs(ar::AbstractChannelProblem, channel) + wiresegs_ascending = sort(channel_segments(ar, channel), by=(ws) -> interval(ar, ws)) + # Y&K zone representation as interval graph + # Edge between each pair of segments that overlap + zone_ig = SimpleGraph(length(wiresegs_ascending)) + # Condrat et al. VCG with avoidable crossings as constraints + # Not handled: constraints from vertically aligned pin positions + vcg = SimpleDiGraph(length(wiresegs_ascending)) # just a fresh graph + for (idx1, seg1) in pairs(wiresegs_ascending) + low1, high1 = interval(ar, seg1) + for (idx2, seg2) in collect(pairs(wiresegs_ascending))[(idx1 + 1):end] + low2, high2 = interval(ar, seg2) + + # If there is no overlap, break and move on to next seg1 + # Use >= instead of > to potentially allow knock-knees + low2 >= high1 && break # All subsequent seg2 have low2 >= high1 + # There is overlap, so add an edge to the interval graph + add_edge!(zone_ig, idx1, idx2) + # Now check if crossing is unavoidable + pt1, nt1 = prev_next_tendency(ar, seg1) + pt2, nt2 = prev_next_tendency(ar, seg2) + # If seg1 and seg2 enter and exit at the same place with same tendency, then crossing + # may be avoidable, but this channel can't say which goes on top yet + (low1 == high1 && low2 == high2 && pt1 == pt2 && nt1 == nt2) && break + low1_tend, high1_tend = against_channel(ar, seg1) ? (nt1, pt1) : (pt1, nt1) + low2_tend, high2_tend = against_channel(ar, seg2) ? (nt2, pt2) : (pt2, nt2) + avoidable = is_avoidable( + low1, + high1, + low2, + high2, + low1_tend, + high1_tend, + low2_tend, + high2_tend + ) + !avoidable && continue + + # Crossing is avoidable, so add a constraint + # Determine which goes on top based on the lower bound tendency of seg2 + # Is prev or next the lower bound? + # top is rightmost segment iff its lower bound tends towards higher (lower index) tracks + top = (idx1, idx2)[1 + (low2_tend == 1)] + bottom = (idx1, idx2)[2 - (low2_tend == 1)] + add_edge!(vcg, top, bottom) # VCG has edge from higher to lower tracks + end + end + return vcg, zone_ig +end + +# ──── Segment deletion / reset ─────────────────────────────────────────────── + +function _delete_segment!(ar, ws; reset_tracks=true, from_net=true) + # Deletes the wire segment `ws` from `ar`. + + # Delete the wire segment in channel_segments + s = running_channel(ws) + deleteat!(channel_segments(ar, s), findfirst(isequal(ws), channel_segments(ar, s))) + + # Delete the segment from its track + c_idx = segment_track(ar, ws) + if !isnothing(c_idx) + if reset_tracks # by default, reset all track assignments in this channel + empty!(channel_tracks(ar, s)) + else + deleteat!( + channel_tracks(ar, s)[c_idx], + findfirst(isequal(ws), channel_tracks(ar, s)[c_idx]) + ) + end + end + # Delete the segment waypoint + delete!(ar.segment_waypoints, ws) + + # if from_net, delete ws from net_wires + # We set from_net to false when looping over net segments so it's not changing under us + return from_net && deleteat!( + net_wire(ar, net_index(ws)), + findfirst(isequal(ws), net_wire(ar, net_index(ws))) + ) +end + +""" + reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) + +Resets the nets with `net_indices` to their unrouted state. + +If `reset_tracks` is `true`, then all channels used by nets being reset will also have their +track assignments removed. +""" +function reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) + for segs in net_wire.(ar, net_indices) + for ws in segs + # delete segment from the router + # don't delete it from net yet, we'll do that after this loop + _delete_segment!(ar, ws, reset_tracks=reset_tracks, from_net=false) + end + empty!(segs) + end +end diff --git a/src/paths/channels.jl b/src/paths/channels.jl index f37d07bce..388953c46 100644 --- a/src/paths/channels.jl +++ b/src/paths/channels.jl @@ -74,27 +74,6 @@ function track_path_segment(n_tracks, channel_section, track_idx; reversed=false ) 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} diff --git a/src/paths/paths.jl b/src/paths/paths.jl index 1690e489c..07a5ce214 100644 --- a/src/paths/paths.jl +++ b/src/paths/paths.jl @@ -764,6 +764,7 @@ include("segments/bspline_optimization.jl") include("routes.jl") +include("channel_routing_core.jl") include("channels.jl") include("channel_autorouter.jl") diff --git a/test/autoroute_examples.jl b/test/autoroute_examples.jl index 4f5b6ec8c..9b3c8d2aa 100644 --- a/test/autoroute_examples.jl +++ b/test/autoroute_examples.jl @@ -1,4 +1,4 @@ -using DeviceLayout +using DeviceLayout, .PreferredUnits using FileIO import .Paths: @@ -255,7 +255,7 @@ end # # v1 v2 v3 v4 # | | | | -# p5 → ════╪════╪════╪════╪════ ← p6 h4 (y=9) +# p5 → ════╪════╪════╪════╪════ ← p6 h4 (y=9) # | | | | # ════╪════╪════╪════╪════ h3 (y=6) # | | | | @@ -486,13 +486,13 @@ function run_all_examples(; save_gds=true, save_png=true) end if save_gds for (name, (c, _)) in results - save("autoroute_$name.gds", c; spec_warnings=false) + save("autoroute_$(name).gds", c; spec_warnings=false) end @info "Saved $(length(results)) GDS files" end if save_png for (name, (c, _)) in results - save("autoroute_$name.png", c; spec_warnings=false) + save("autoroute_$(name).png", c; spec_warnings=false) end @info "Saved $(length(results)) PNG files" end diff --git a/test/test_channels.jl b/test/test_channels.jl index 1d5ae007b..e8054117d 100644 --- a/test/test_channels.jl +++ b/test/test_channels.jl @@ -465,6 +465,5 @@ end end # Runs without error test_simple() - test_fanout() test_grid_escape() end From cf78054e3b56ddad95968a870bf2bcd14aacb0d5 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 17 Apr 2026 16:20:46 +0200 Subject: [PATCH 18/32] Add routing summary diagnostic and verbose autoroute option --- src/paths/channel_autorouter.jl | 40 +++++++++++++++++++++++++++---- src/paths/channel_routing_core.jl | 25 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 131d91f9e..683564cf4 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -273,8 +273,7 @@ function segment_direction(ar::ChannelRouter, ws::TrackWireSegment, s) end """ - autoroute!(ar::ChannelRouter, transition_rule; net_indices=eachindex(ar.net_pins), - fixed_channel_paths::Dict{Int,Vector{Int}}=Dict()) + autoroute!(ar::ChannelRouter, transition_rule, margin; net_indices, fixed_channel_paths, verbose) Perform channel and track assigment, then make routes. @@ -282,19 +281,37 @@ Routes only the nets in `net_indices`. If the net is already routed, it is reset for a net can be specified in `fixed_channel_paths` by the indices of the channels the route takes. For example, `fixed_channel_paths=Dict(1 => [2, 4, 1, 5])` will force net 1 to be routed from its source pin, through channels 2, 4, 1, 5 in order, then to its destination pin. + +If `verbose=true`, prints a summary of routing results including net count and track usage. """ function autoroute!( ar::ChannelRouter, transition_rule, margin; net_indices=eachindex(ar.net_pins), - fixed_channel_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}() + fixed_channel_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}(), + verbose=false ) reset_nets!(ar, net_indices=net_indices) assign_channels!(ar; net_indices=net_indices, fixed_paths=fixed_channel_paths) assign_tracks!(ar) rule = AutoChannelRouting(ar, transition_rule, margin) routes = make_routes!(ar, rule) + + if verbose + n_routed = count(!isempty, ar.net_wires[collect(net_indices)]) + n_total = length(net_indices) + max_tracks = maximum(num_tracks(ar, ch) for ch in 1:num_channels(ar)) + @info "Autorouting complete" nets_routed=n_routed nets_total=n_total max_tracks_per_channel=max_tracks + for idx in net_indices + for (seg_i, ws) in enumerate(net_wire(ar, idx)) + if isnothing(segment_track(ar, ws)) + @warn "Net $idx segment $seg_i: no track assigned" channel=running_channel(ws) + end + end + end + end + return routes end @@ -497,9 +514,24 @@ function _update_with_plan!(rule::AutoChannelRouting{T}, route_node, sch) where push!(rule.router.pins, hooks(route_node.component).p0) push!(rule.router.pins, hooks(route_node.component).p1) push!(rule.router.net_pins, (pin_idx, pin_idx + 1)) + push!(rule.router.net_wires, NetWire()) # If all paths have been added, go ahead and run autorouting if length(rule.router.net_pins) == length(rule.router.net_paths) - build_channel_graph(rule.router.pins, getproperty.(rule.router.channels, :path), T) + g, ixns = build_channel_graph( + rule.router.pins, + getproperty.(rule.router.channels, :path), + T + ) + # Populate the router's graph and intersection dict in-place + # (ChannelRouter is immutable, but its mutable fields can be mutated) + ar_g = rule.router.channel_graph + for _ in 1:(nv(g) - nv(ar_g)) + add_vertex!(ar_g) + end + for e in edges(g) + add_edge!(ar_g, e.src, e.dst) + end + merge!(rule.router.channel_intersections, ixns) assign_channels!(rule.router) assign_tracks!(rule.router) end diff --git a/src/paths/channel_routing_core.jl b/src/paths/channel_routing_core.jl index 24e85607c..34ee9956d 100644 --- a/src/paths/channel_routing_core.jl +++ b/src/paths/channel_routing_core.jl @@ -12,6 +12,7 @@ import Graphs: nv, ne, add_edge!, + add_vertex!, rem_edge!, adjacency_matrix, edges, @@ -787,3 +788,27 @@ function reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) empty!(segs) end end + +# ──── Diagnostics ──────────────────────────────────────────────────────────── + +""" + routing_summary(ar::AbstractChannelProblem) + +Print a per-net summary of routing results: pin pair, channel path, and track assignments. + +Segments with unassigned tracks are flagged. +""" +function routing_summary(ar::AbstractChannelProblem) + for idx = 1:num_nets(ar) + wire = net_wire(ar, idx) + pins = net_pins(ar, idx) + channels_used = [running_channel(ws) for ws in wire] + tracks = [segment_track(ar, ws) for ws in wire] + has_unassigned = any(isnothing, tracks) + println( + "Net $idx: pins $(pins[1])→$(pins[2]), $(length(wire)) segments, " * + "channels $channels_used, tracks $tracks" * + (has_unassigned ? " [UNASSIGNED TRACKS]" : "") + ) + end +end From 03cf786ac224ba7ff5a6ba5b2023f160c1bb86d7 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 17 Apr 2026 16:45:09 +0200 Subject: [PATCH 19/32] Route validation, rerouting convenience + docs, tests --- src/paths/channel_autorouter.jl | 25 +++++++++++-- src/paths/channel_routing_core.jl | 50 ++++++++++++++++++++++--- test/test_autorouter_internals.jl | 61 +++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 9 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 683564cf4..a0433c3a5 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -292,9 +292,7 @@ function autoroute!( fixed_channel_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}(), verbose=false ) - reset_nets!(ar, net_indices=net_indices) - assign_channels!(ar; net_indices=net_indices, fixed_paths=fixed_channel_paths) - assign_tracks!(ar) + affected_nets = reroute_nets!(ar, net_indices; fixed_paths=fixed_channel_paths) rule = AutoChannelRouting(ar, transition_rule, margin) routes = make_routes!(ar, rule) @@ -354,6 +352,27 @@ function make_routes!(ar::ChannelRouter{T}, rule) where {T} return ar.net_routes end +""" + validate_routes(ar::ChannelRouter{T}, sty; atol=onenanometer(T)) where {T} + +Construct `Path` objects from the router's `Route`s and check whether each one +reaches its destination point and angle. + +Returns a `BitVector` of length `num_nets(ar)` where `true` means the route +produced valid geometry. When a route fails, the corresponding `@error` from +`route!()` is still emitted to the logging system. +""" +function validate_routes(ar::ChannelRouter{T}, sty; atol=DeviceLayout.onenanometer(T)) where {T} + success = trues(num_nets(ar)) + for (idx, rt) in enumerate(ar.net_routes) + pa = Path(rt.p0, α0=rt.α0) + ok = route!(pa, rt.p1, rt.α1, rt.rule, sty; + waypoints=rt.waypoints, waydirs=rt.waydirs, atol=atol) + success[idx] = something(ok, false) # route! currently returns nothing on failure + end + return success +end + ######## Visualization """ diff --git a/src/paths/channel_routing_core.jl b/src/paths/channel_routing_core.jl index 34ee9956d..3f454ce2a 100644 --- a/src/paths/channel_routing_core.jl +++ b/src/paths/channel_routing_core.jl @@ -471,7 +471,10 @@ end """ assign_tracks!(ar::AbstractChannelProblem) -Performs track assigment for `ar`. +Performs track assignment for all channels in `ar`. + +Track assignment operates on **all** channels, not a subset. Re-running after modifying +a single net will reassign tracks globally in every channel that contains wire segments. """ function assign_tracks!(ar::AbstractChannelProblem) # Order of channels will change results @@ -775,8 +778,10 @@ end Resets the nets with `net_indices` to their unrouted state. -If `reset_tracks` is `true`, then all channels used by nets being reset will also have their -track assignments removed. +If `reset_tracks` is `true` (the default), then **all** track assignments in every channel +that contained a deleted segment are cleared — not just the tracks for the deleted nets. +This affects other nets sharing those channels. This is intentional: track assignment must be +globally consistent within a channel. """ function reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) for segs in net_wire.(ar, net_indices) @@ -789,26 +794,59 @@ function reset_nets!(ar; net_indices=eachindex(ar.net_pins), reset_tracks=true) end end +""" + reroute_nets!(ar::AbstractChannelProblem, net_indices; fixed_paths=Dict{Int,Vector{Int}}()) + +Reset and re-route specific nets, then reassign tracks globally. + +Returns the set of all net indices affected by the track reassignment (i.e., nets that +shared a channel with any re-routed net). This set always includes `net_indices` and may +include additional nets whose track assignments changed as a side effect. + +See also [`reset_nets!`](@ref), [`assign_channels!`](@ref), [`assign_tracks!`](@ref). +""" +function reroute_nets!( + ar::AbstractChannelProblem, + net_indices; + fixed_paths::Dict{Int, Vector{Int}}=Dict{Int, Vector{Int}}() +) + # Identify all nets sharing channels with the target nets (for caller awareness) + affected_nets = Set{Int}(net_indices) + for idx in net_indices + for ws in net_wire(ar, idx) + for other_ws in channel_segments(ar, running_channel(ws)) + push!(affected_nets, net_index(other_ws)) + end + end + end + + reset_nets!(ar; net_indices=net_indices, reset_tracks=true) + assign_channels!(ar; net_indices=net_indices, fixed_paths) + assign_tracks!(ar) + return affected_nets +end + # ──── Diagnostics ──────────────────────────────────────────────────────────── """ - routing_summary(ar::AbstractChannelProblem) + routing_summary([io::IO,] ar::AbstractChannelProblem) Print a per-net summary of routing results: pin pair, channel path, and track assignments. Segments with unassigned tracks are flagged. """ -function routing_summary(ar::AbstractChannelProblem) +function routing_summary(io::IO, ar::AbstractChannelProblem) for idx = 1:num_nets(ar) wire = net_wire(ar, idx) pins = net_pins(ar, idx) channels_used = [running_channel(ws) for ws in wire] tracks = [segment_track(ar, ws) for ws in wire] has_unassigned = any(isnothing, tracks) - println( + println(io, "Net $idx: pins $(pins[1])→$(pins[2]), $(length(wire)) segments, " * "channels $channels_used, tracks $tracks" * (has_unassigned ? " [UNASSIGNED TRACKS]" : "") ) end end +routing_summary(ar::AbstractChannelProblem) = routing_summary(stdout, ar) diff --git a/test/test_autorouter_internals.jl b/test/test_autorouter_internals.jl index 805dae079..caa1cc48d 100644 --- a/test/test_autorouter_internals.jl +++ b/test/test_autorouter_internals.jl @@ -162,4 +162,65 @@ # No channel should need more than 1 track (non-crossing parallel routes) @test all(length.(ar.channel_tracks) .<= 1) end + + # ── routing_summary ────────────────────────────────────────────────────── + @testset "routing_summary" begin + channels = [vchannel(5, -1, 7), hchannel(-1, 9, 0), hchannel(-1, 9, 6)] + hooks = [bpin(2, -0.5), bpin(8, -0.5), tpin(2, 6.5), tpin(8, 6.5)] + ar = ChannelRouter([(1, 4), (2, 3)], hooks, RouteChannel.(channels)) + autoroute!(ar, Paths.StraightAnd90(0.1), 0.1) + + output = sprint(Paths.routing_summary, ar) + @test occursin("Net 1:", output) + @test occursin("Net 2:", output) + @test !occursin("UNASSIGNED", output) + end + + # ── validate_routes ────────────────────────────────────────────────────── + @testset "validate_routes" begin + channels = [vchannel(5, -1, 7), hchannel(-1, 9, 0), hchannel(-1, 9, 6)] + hooks = [bpin(2, -0.5), bpin(8, -0.5), tpin(2, 6.5), tpin(8, 6.5)] + ar = ChannelRouter([(1, 4), (2, 3)], hooks, RouteChannel.(channels)) + autoroute!(ar, Paths.StraightAnd90(0.1), 0.1) + + ok = Paths.validate_routes(ar, Paths.Trace(0.05)) + @test ok isa BitVector + @test length(ok) == 2 + @test all(ok) + end + + # ── verbose autoroute! ─────────────────────────────────────────────────── + @testset "verbose autoroute!" begin + channels = [hchannel(0, 10, 0)] + hooks = [bpin(2, -0.5), tpin(8, 0.5)] + ar = ChannelRouter([(1, 2)], hooks, RouteChannel.(channels)) + + # verbose=true should not error and should produce log output + routes = autoroute!(ar, Paths.StraightAnd90(0.1), 0.1; verbose=true) + @test length(routes) == 1 + @test all(length.(ar.net_wires) .> 0) + end + + # ── reroute_nets! ──────────────────────────────────────────────────────── + @testset "reroute_nets!" begin + channels = [vchannel(5, -1, 7), hchannel(-1, 9, 0), hchannel(-1, 9, 6)] + hooks = [bpin(2, -0.5), bpin(8, -0.5), tpin(2, 6.5), tpin(8, 6.5)] + ar = ChannelRouter([(1, 4), (2, 3)], hooks, RouteChannel.(channels)) + autoroute!(ar, Paths.StraightAnd90(0.1), 0.1) + + # Record original track assignments + orig_tracks_net1 = [Paths.segment_track(ar, ws) for ws in Paths.net_wire(ar, 1)] + @test all(!isnothing, orig_tracks_net1) + + # Reroute net 1 with a fixed channel path + affected = Paths.reroute_nets!(ar, [1]; fixed_paths=Dict(1 => [2, 1, 3])) + @test 1 in affected + @test 2 in affected # net 2 shares all channels with net 1 + + # Net 1 should still be routed + @test length(Paths.net_wire(ar, 1)) > 0 + # Track assignments should still be valid + new_tracks_net1 = [Paths.segment_track(ar, ws) for ws in Paths.net_wire(ar, 1)] + @test all(!isnothing, new_tracks_net1) + end end From d09167eb2c37a5d60732e540239a33e5ec3805f3 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 17 Apr 2026 17:32:47 +0200 Subject: [PATCH 20/32] Ignore epsilon self-intersections in test --- src/paths/channels.jl | 2 +- test/autoroute_examples.jl | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/paths/channels.jl b/src/paths/channels.jl index 388953c46..f52e01543 100644 --- a/src/paths/channels.jl +++ b/src/paths/channels.jl @@ -156,7 +156,7 @@ function _route!( sty; waypoints ) - push!(p, Node(resolve_offset(track_path_seg), sty)) + push!(p, Node(resolve_offset(track_path_seg), sty), reconcile=false) p[end - 1].next = p[end] p[end].prev = p[end - 1] # Note `auto_curvature` BSpline uses curvature from end of previous segment diff --git a/test/autoroute_examples.jl b/test/autoroute_examples.jl index 9b3c8d2aa..419df6e9d 100644 --- a/test/autoroute_examples.jl +++ b/test/autoroute_examples.jl @@ -64,6 +64,13 @@ tpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 90°) const R = Paths.StraightAnd90(0.1) const WW = 0.05 const MARGIN = 0.1 +# Autoroute can produce epsilon overlap between end of one segment and start of another +# Filter prepared_intersections to only inter-path crossings (different path indices) +function inter_path_intersections(paths) + return filter(Intersect.prepared_intersections(paths)) do ixn + ixn[1][1] != ixn[2][1] + end +end # ── Example 1: Simple ──────────────────────────────────────────────────────── # 1 horizontal channel, 2 pins, 1 net. @@ -123,7 +130,7 @@ function example_parallel() routes = autoroute!(ar, R, MARGIN) paths = Path.(routes, Ref(Paths.Trace(WW))) c = visualize_router_state(ar; wire_width=WW) - @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert isempty(inter_path_intersections(paths)) "No crossings" @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" return c, ar end @@ -162,7 +169,7 @@ function example_crossing() @assert length(ar.channel_tracks[1]) >= 2 "Crossing nets need multiple tracks in shared channel" # Due to assignment order, only one horizontal channel has two tracks @assert length(ar.channel_tracks[2]) + length(ar.channel_tracks[3]) == 3 "Crossing nets need multiple horizontal tracks only when they overlap due to vertical assignment" - @assert length(Intersect.prepared_intersections(paths)) == 1 "Exactly one crossing" + @assert length(inter_path_intersections(paths)) == 1 "Exactly one crossing" return c, ar end @@ -201,7 +208,7 @@ function example_fanin_fanout() @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" paths = Path.(routes, Ref(Paths.Trace(WW))) - @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert isempty(inter_path_intersections(paths)) "No crossings" @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" return c, ar end @@ -245,7 +252,7 @@ function example_multichannel_fanout() @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" @assert all(length.(ar.channel_tracks[2:5]) .== 1) "Each net should use its nearest horizontal channel" - @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert isempty(inter_path_intersections(paths)) "No crossings" return c, ar end @@ -340,7 +347,7 @@ function example_angled() @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" @assert all(length.(ar.channel_tracks) .== 2) "Each channel needs two tracks" paths = Path.(routes, Ref(Paths.Trace(WW))) - @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert isempty(inter_path_intersections(paths)) "No crossings" return c, ar end @@ -379,7 +386,7 @@ function example_dense() @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" @assert all(length.(ar.channel_tracks) .== 3) "Requires 3 tracks on each channel" - @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert isempty(inter_path_intersections(paths)) "No crossings" return c, ar end @@ -424,7 +431,7 @@ function example_bspline() c = visualize_router_state(ar, wire_width=WW) @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" paths = Path.(routes, Ref(Paths.Trace(WW))) - @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert isempty(inter_path_intersections(paths)) "No crossings" @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" return c, ar end @@ -450,7 +457,7 @@ function example_fanout40nets() c = Paths.visualize_router_state(ar, wire_width=1μm) @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" paths = Path.(routes, Ref(Paths.Trace(WW))) - @assert isempty(Intersect.prepared_intersections(paths)) "No crossings" + @assert isempty(inter_path_intersections(paths)) "No crossings" @assert length(ar.channel_tracks[1]) <= 20 "Left and right halves share tracks" return c, ar end From e1311db918648b3dd0199d4f8dbd714a4f3f3e10 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Mon, 20 Apr 2026 15:15:28 +0200 Subject: [PATCH 21/32] Move autoroute examples to examples/ and add to docs --- docs/src/examples/autorouter.md | 142 ++++++++++++++++++ .../ChannelAutorouter/ChannelAutorouter.jl | 74 +++++---- 2 files changed, 178 insertions(+), 38 deletions(-) create mode 100644 docs/src/examples/autorouter.md rename test/autoroute_examples.jl => examples/ChannelAutorouter/ChannelAutorouter.jl (90%) diff --git a/docs/src/examples/autorouter.md b/docs/src/examples/autorouter.md new file mode 100644 index 000000000..900fd9649 --- /dev/null +++ b/docs/src/examples/autorouter.md @@ -0,0 +1,142 @@ +# Channel Autorouter + +The channel autorouter connects pairs of pins by routing wires through a user-defined network of channels. Routing proceeds in two steps: **channel assignment** (choosing which channels each net's wire passes through) and **track assignment** (assigning non-overlapping tracks within each channel). + +The full code for these examples can be found [in `examples/ChannelAutorouter/ChannelAutorouter.jl` in the DeviceLayout.jl repository](https://github.com/aws-cqc/DeviceLayout.jl/blob/main/examples/ChannelAutorouter/ChannelAutorouter.jl). + +```@example autorouter +using DeviceLayout, FileIO +include("../../../examples/ChannelAutorouter/ChannelAutorouter.jl") +using .ChannelAutorouter +nothing # hide +``` + +## Simple + +One horizontal channel, two pins, one net. The simplest possible autorouting scenario. + +```@example autorouter +c, ar = ChannelAutorouter.example_simple() +save("autoroute_simple.png", c); nothing # hide +``` + +```@raw html + +``` + +## Parallel + +Three nets routed left-to-right at matching heights through a grid of two vertical and three horizontal channels. No crossings needed. + +```@example autorouter +c, ar = ChannelAutorouter.example_parallel() +save("autoroute_parallel.png", c); nothing # hide +``` + +```@raw html + +``` + +## Crossing + +Two nets that must cross in a shared vertical channel, forcing the router to assign multiple tracks. + +```@example autorouter +c, ar = ChannelAutorouter.example_crossing() +save("autoroute_crossing.png", c); nothing # hide +``` + +```@raw html + +``` + +## Fan-in / fan-out + +Clustered pins on one side, spread-out pins on the other, routed through a single shared horizontal channel. The router assigns multiple tracks to accommodate the asymmetric spacing. + +```@example autorouter +c, ar = ChannelAutorouter.example_fanin_fanout() +save("autoroute_fanin_fanout.png", c); nothing # hide +``` + +```@raw html + +``` + +## Multichannel fan-out + +Same topology as fan-in/fan-out, but with a dedicated horizontal channel per net. Each net uses exactly one track. + +```@example autorouter +c, ar = ChannelAutorouter.example_multichannel_fanout() +save("autoroute_multichannel_fanout.png", c); nothing # hide +``` + +```@raw html + +``` + +## Grid + +A 4×4 grid of horizontal and vertical channels with pins on different edges. Nets take multi-hop paths through the grid. + +```@example autorouter +c, ar = ChannelAutorouter.example_grid() +save("autoroute_grid.png", c); nothing # hide +``` + +```@raw html + +``` + +## Angled channels + +Non-Manhattan channels: two 45° diagonals crossing a horizontal channel. The router handles arbitrary channel geometry. + +```@example autorouter +c, ar = ChannelAutorouter.example_angled() +save("autoroute_angled.png", c); nothing # hide +``` + +```@raw html + +``` + +## Dense + +Six nets sharing just two horizontal and two vertical channels. Forces three tracks per channel. + +```@example autorouter +c, ar = ChannelAutorouter.example_dense() +save("autoroute_dense.png", c); nothing # hide +``` + +```@raw html + +``` + +## B-spline channels + +Curved channels using B-spline geometry with `BSplineRouting` transitions. Same fan-in/fan-out topology as above but with non-straight channels. + +```@example autorouter +c, ar = ChannelAutorouter.example_bspline() +save("autoroute_bspline.png", c); nothing # hide +``` + +```@raw html + +``` + +## 40-net fan-out + +40 nets fan out through a single wide channel from an inner row of pins to an outer row with twice the spacing. + +```@example autorouter +c, ar = ChannelAutorouter.example_fanout40() +save("autoroute_fanout40.png", c); nothing # hide +``` + +```@raw html + +``` diff --git a/test/autoroute_examples.jl b/examples/ChannelAutorouter/ChannelAutorouter.jl similarity index 90% rename from test/autoroute_examples.jl rename to examples/ChannelAutorouter/ChannelAutorouter.jl index 419df6e9d..983d678d0 100644 --- a/test/autoroute_examples.jl +++ b/examples/ChannelAutorouter/ChannelAutorouter.jl @@ -1,3 +1,11 @@ +""" +Channel autorouter examples demonstrating various routing scenarios. + +Each `example_*` function returns `(cell::Cell, router::ChannelRouter)`. +`run_all_examples()` runs all examples and optionally saves GDS/PNG output. +""" +module ChannelAutorouter + using DeviceLayout, .PreferredUnits using FileIO @@ -213,7 +221,7 @@ function example_fanin_fanout() return c, ar end -# ── Example 5: Fan-out ─────────────────────────────────────────────────────── +# ── Example 5: Multichannel fan-out ────────────────────────────────────────── # Left pins clustered (simulating component outputs), right pins spread out. # 2 vertical + 4 horizontal channels. # @@ -390,15 +398,10 @@ function example_dense() return c, ar end -# ── Example 9: Fan-in/fan-out with B-splines ────────────────────────────────── -# Left and right pins spread out, must fan in/out asymmetrically to horizontal channel -# 2 quasi-vertical + 1 quasi-horizontal channels. -# -# v_left v_right -# p4 → (-0.5, 6) ║ ║ (10.5, 9) ← p8 h4 (y=9) -# p3 → (-0.5, 5) ║════════════════════║ (10.5, 6) ← p7 h3 (y=6) -# p2 → (-0.5, 4) ║ ║ (10.5, 3) ← p6 h2 (y=3) -# p1 → (-0.5, 3) ║ ║ (10.5, 0) ← p5 h1 (y=0) +# ── Example 9: B-spline channels ───────────────────────────────────────────── +# Curved channels using B-spline geometry. Same fan-in/fan-out topology as +# example 4 but with non-straight channels and BSplineRouting transitions. + function example_bspline() channels = [ bchannel(0, -2, 30°, 1, 12), # v_left @@ -436,9 +439,10 @@ function example_bspline() return c, ar end -# ── Example 9: Fan-out with 40 nets ────────────────────────────────── -# 40 nets must fan out with a ratio of 2 -function example_fanout40nets() +# ── Example 10: 40-net fan-out ─────────────────────────────────────────────── +# 40 nets fan out through a single wide channel from inner to outer pin rows. + +function example_fanout40() lx_outer = ly_outer = 10e6nm lx_inner = ly_inner = 5e6nm @@ -462,46 +466,40 @@ function example_fanout40nets() return c, ar end -# ── Example 10: Schematic integration ──────────────────────────────────────── -# TODO: AutoChannelRouting with schematic workflow. -# Requires components with hooks, route! calls, and schematic compilation. -# Placeholder for now. - -# function example_schematic() -# ... -# end - # ── Assembly ───────────────────────────────────────────────────────────────── -function run_all_examples(; save_gds=true, save_png=true) - examples = [ - "simple" => example_simple, - "parallel" => example_parallel, - "crossing" => example_crossing, - "fanin_fanout" => example_fanin_fanout, - "multichannel_fanout" => example_multichannel_fanout, - "grid" => example_grid, - "angled" => example_angled, - "dense" => example_dense, - "bspline" => example_bspline, - "fanout40" => example_fanout40nets - ] +const ALL_EXAMPLES = [ + "simple" => example_simple, + "parallel" => example_parallel, + "crossing" => example_crossing, + "fanin_fanout" => example_fanin_fanout, + "multichannel_fanout" => example_multichannel_fanout, + "grid" => example_grid, + "angled" => example_angled, + "dense" => example_dense, + "bspline" => example_bspline, + "fanout40" => example_fanout40 +] + +function run_all_examples(; save_gds=true, save_png=true, dir=@__DIR__) results = Pair{String, Tuple{Cell, ChannelRouter}}[] - for (name, fn) in examples + for (name, fn) in ALL_EXAMPLES @info "Running $name..." push!(results, name => fn()) end if save_gds for (name, (c, _)) in results - save("autoroute_$(name).gds", c; spec_warnings=false) + save(joinpath(dir, "autoroute_$(name).gds"), c; spec_warnings=false) end @info "Saved $(length(results)) GDS files" end if save_png for (name, (c, _)) in results - save("autoroute_$(name).png", c; spec_warnings=false) + save(joinpath(dir, "autoroute_$(name).png"), c; spec_warnings=false) end @info "Saved $(length(results)) PNG files" end return results end + +end # module From 4ade74c6dc01abe2d7615a1d45eb0f3f0932985f Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Tue, 21 Apr 2026 13:33:05 +0200 Subject: [PATCH 22/32] Add schematic example --- Project.toml | 3 - docs/make.jl | 3 +- docs/src/examples/autorouter.md | 2 + .../ChannelAutorouter/ChannelAutorouter.jl | 47 ++++++++++++- src/hooks.jl | 7 ++ src/paths/channel_autorouter.jl | 69 +------------------ src/schematics/routes.jl | 33 ++++++++- test/test_examples.jl | 11 +++ 8 files changed, 101 insertions(+), 74 deletions(-) diff --git a/Project.toml b/Project.toml index 74f92cac5..8657f02fb 100644 --- a/Project.toml +++ b/Project.toml @@ -87,8 +87,5 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" -[preferences.DeviceLayout] -units = "PreferMicrons" - [targets] test = ["Aqua", "Test", "TestItemRunner"] diff --git a/docs/make.jl b/docs/make.jl index 1975611a4..353514929 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -71,7 +71,8 @@ makedocs( "Examples" => [ "ExamplePDK" => "examples/examplepdk.md", "Quantum Processor" => "examples/qpu17.md", - "Single-Transmon Simulation" => "examples/singletransmon.md" + "Single-Transmon Simulation" => "examples/singletransmon.md", + "Channel Autorouter" => "examples/autorouter.md" ], "Reference" => [ "Overview" => "reference/index.md", diff --git a/docs/src/examples/autorouter.md b/docs/src/examples/autorouter.md index 900fd9649..78b42c298 100644 --- a/docs/src/examples/autorouter.md +++ b/docs/src/examples/autorouter.md @@ -50,6 +50,8 @@ save("autoroute_crossing.png", c); nothing # hide ``` +There is also a variant of this example using the schematic routing interface. + ## Fan-in / fan-out Clustered pins on one side, spread-out pins on the other, routed through a single shared horizontal channel. The router assigns multiple tracks to accommodate the asymmetric spacing. diff --git a/examples/ChannelAutorouter/ChannelAutorouter.jl b/examples/ChannelAutorouter/ChannelAutorouter.jl index 983d678d0..bac7758b5 100644 --- a/examples/ChannelAutorouter/ChannelAutorouter.jl +++ b/examples/ChannelAutorouter/ChannelAutorouter.jl @@ -6,7 +6,7 @@ Each `example_*` function returns `(cell::Cell, router::ChannelRouter)`. """ module ChannelAutorouter -using DeviceLayout, .PreferredUnits +using DeviceLayout, .PreferredUnits, .SchematicDrivenLayout using FileIO import .Paths: @@ -466,6 +466,48 @@ function example_fanout40() return c, ar end +# ── Example 11: Schematic interface ─────────────────────────────────────────── +# Same as `example_crossing` but using the schematic interface to set up the routing problem. + +function example_crossing_schematic() + channels = RouteChannel.([ + vchannel(5mm, -1mm, 7mm, width=2.0mm), # v_mid + hchannel(-1mm, 9mm, 0mm, width=2.0mm), # h_bot + hchannel(-1mm, 9mm, 6mm, width=2.0mm) # h_top + ]) + + hooks = [ + bpin(2mm, -0.5mm), # p1: below h_bot, left side + bpin(8mm, -0.5mm), # p2: below h_bot, right side + tpin(2mm, 6.5mm), # p3: above h_top, left side + tpin(8mm, 6.5mm) # p4: above h_top, right side + ] + # Crossed: bottom-left↔top-right, bottom-right↔top-left + nets = [(1, 4), (2, 3)] + # Set up schematic + g = SchematicGraph("example") + # Start/end components + comps = [Spacer(; p1=h.p) for h in hooks] + comp_nodes = add_node!.(g, comps) + ar = ChannelRouter(channels) + rule = AutoChannelRouting(ar, Paths.StraightAnd90(MARGIN*1mm), MARGIN*1mm) + r1 = route!(g, rule, comp_nodes[1]=>:p1_south, comp_nodes[4]=>:p1_north, Paths.Trace(WW*1mm), GDSMeta()) + r2 = route!(g, rule, comp_nodes[2]=>:p1_south, comp_nodes[3]=>:p1_north, Paths.Trace(WW*1mm), GDSMeta()) + + sch = plan(g) + paths = [SchematicDrivenLayout.path(r1), SchematicDrivenLayout.path(r2)] + c = Cell(sch.coordinate_system) + # c = visualize_router_state(ar; wire_width=WW) + + @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" + # Both nets traverse v_mid, so it needs ≥2 tracks + @assert length(ar.channel_tracks[1]) >= 2 "Crossing nets need multiple tracks in shared channel" + # Due to assignment order, only one horizontal channel has two tracks + @assert length(ar.channel_tracks[2]) + length(ar.channel_tracks[3]) == 3 "Crossing nets need multiple horizontal tracks only when they overlap due to vertical assignment" + @assert length(inter_path_intersections(paths)) == 1 "Exactly one crossing" + return c, ar +end + # ── Assembly ───────────────────────────────────────────────────────────────── const ALL_EXAMPLES = [ @@ -478,7 +520,8 @@ const ALL_EXAMPLES = [ "angled" => example_angled, "dense" => example_dense, "bspline" => example_bspline, - "fanout40" => example_fanout40 + "fanout40" => example_fanout40, + "crossing_schematic" => example_crossing_schematic, ] function run_all_examples(; save_gds=true, save_png=true, dir=@__DIR__) diff --git a/src/hooks.jl b/src/hooks.jl index 78e2dd65f..454d29bd8 100644 --- a/src/hooks.jl +++ b/src/hooks.jl @@ -20,6 +20,9 @@ struct PointHook{T} <: Hook{T} end PointHook(p, in_direction) = PointHook{eltype(p)}(p, in_direction) # Otherwise can't infer if in_direction is in degrees PointHook(x, y, in_direction) = PointHook(Point(promote(x, y)...), in_direction) +Base.convert(::Type{PointHook{T}}, h::PointHook{T}) where {T} = h +Base.convert(::Type{PointHook{T}}, h::PointHook{S}) where {T, S} = + PointHook{T}(h.p, h.in_direction) """ in_direction(h::Hook) @@ -111,6 +114,10 @@ HandedPointHook(p0::Point, in_direction, rh::Bool) = HandedPointHook(x0::Coordinate, y0::Coordinate, in_direction, right_handed=true) = HandedPointHook(PointHook(x0, y0, in_direction), right_handed) +Base.convert(::Type{HandedPointHook{T}}, h::HandedPointHook{T}) where {T} = h +Base.convert(::Type{HandedPointHook{T}}, h::HandedPointHook{S}) where {T, S} = + HandedPointHook{T}(PointHook{T}(h.h.p, h.h.in_direction), h.right_handed) + function transformation(h1::HandedPointHook, h2::HandedPointHook) f = transformation(h1.h, h2.h) h1.right_handed == h2.right_handed && return f diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index a0433c3a5..93af1bfdf 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -34,10 +34,7 @@ struct ChannelRouter{T <: Coordinate} <: AbstractChannelProblem{T} channel_segments::Vector{Vector{TrackWireSegment}} # Vector of vectors of all tracks in each channel [where each track is a vector of wire segments] channel_tracks::Vector{Vector{Track}} - # Waypoints for each segment (used for visualizing router state) - segment_waypoints::Dict{TrackWireSegment, PointHook{T}} net_routes::Vector{Route{T}} - net_paths::Vector{Path{T}} # [Internals] Persistent path objects to populate after routing end """ @@ -52,7 +49,6 @@ function ChannelRouter(nets, pin_hooks::Vector{<:Hook}, channels::Vector{<:Route net_wires = [NetWire() for i in eachindex(nets)] channel_segments = [TrackWireSegment[] for i in eachindex(channels)] channel_tracks = [Track[] for i in eachindex(channels)] - segment_waypoints = Dict{TrackWireSegment, PointHook{T}}() pins = [PointHook{T}(pin.p, pin.in_direction + 180°) for pin in pin_hooks] # Build channel graphs with full paths to avoid compound operations channel_paths = [ch.path for ch in channels] @@ -66,16 +62,13 @@ function ChannelRouter(nets, pin_hooks::Vector{<:Hook}, channels::Vector{<:Route ixns, channel_segments, channel_tracks, - segment_waypoints, - Route{T}[], - [Path{T}() for net in nets] + Route{T}[] ) end function ChannelRouter(channels::Vector{RouteChannel{T}}) where {T} channel_segments = [TrackWireSegment[] for i in eachindex(channels)] channel_tracks = [Track[] for i in eachindex(channels)] - segment_waypoints = Dict{TrackWireSegment, PointHook{T}}() return ChannelRouter{T}( SimpleGraph(), Tuple{Int, Int}[], @@ -85,9 +78,7 @@ function ChannelRouter(channels::Vector{RouteChannel{T}}) where {T} Dict{Tuple{Int, Int}, IntersectionInfo{T}}(), channel_segments, channel_tracks, - segment_waypoints, - Route{T}[], - Path{T}[] + Route{T}[] ) end @@ -143,8 +134,6 @@ function direction_at_intersection(ar::ChannelRouter, running_channel, intersect return rem2pi(uconvert(NoUnits, angle_unitful), RoundNearest) end -segment_waypoint(ar::ChannelRouter, ws::TrackWireSegment) = ar.segment_waypoints[ws] - function segment_offset( ar::ChannelRouter{T}, ws::TrackWireSegment, @@ -255,7 +244,6 @@ function print_segments(ar::ChannelRouter, net) Segment $i: Runs along Channel $(running_channel(ws)), Track $(segment_track(ar, ws)) From $(channel_names[1]) to $(channel_names[2]) - Through waypoint $(segment_waypoint(ar, ws)[1]) at $(segment_waypoint(ar, ws)[2]) """ ) end @@ -313,27 +301,6 @@ function autoroute!( return routes end -######## Modification - -""" - set_waypoint!(ar::ChannelRouter, net_idx, seg_idx, new_point) - set_waypoint!(ar::ChannelRouter, net_idx, seg_idx, new_point, new_direction) - -Sets the waypoint for the segment at `seg_idx` in net `net_idx` to `new_point`. - -A `new_direction` can also be specified for advanced usage. -""" -function set_waypoint!( - ar::ChannelRouter, - net_idx, - seg_idx, - new_point, - dir=segment_waypoints(ar, net_wire(ar, net_idx)[seg_idx])[2] -) - ws = net_wire(ar, net_idx)[seg_idx] - return ar.segment_waypoints[ws] = (new_point, dir) -end - ######## Route construction """ @@ -523,35 +490,3 @@ function channels_taken(ar::ChannelRouter, pa::Path) ) return [running_channel(wireseg) for wireseg in net_wire(ar, net_idx)] end - -function _update_with_graph!(rule::AutoChannelRouting, route_node, graph; kwargs...) - return push!(rule.router.net_paths, route_node.component._path) -end - -function _update_with_plan!(rule::AutoChannelRouting{T}, route_node, sch) where {T} - pin_idx = length(rule.router.pins) + 1 - push!(rule.router.pins, hooks(route_node.component).p0) - push!(rule.router.pins, hooks(route_node.component).p1) - push!(rule.router.net_pins, (pin_idx, pin_idx + 1)) - push!(rule.router.net_wires, NetWire()) - # If all paths have been added, go ahead and run autorouting - if length(rule.router.net_pins) == length(rule.router.net_paths) - g, ixns = build_channel_graph( - rule.router.pins, - getproperty.(rule.router.channels, :path), - T - ) - # Populate the router's graph and intersection dict in-place - # (ChannelRouter is immutable, but its mutable fields can be mutated) - ar_g = rule.router.channel_graph - for _ in 1:(nv(g) - nv(ar_g)) - add_vertex!(ar_g) - end - for e in edges(g) - add_edge!(ar_g, e.src, e.dst) - end - merge!(rule.router.channel_intersections, ixns) - assign_channels!(rule.router) - assign_tracks!(rule.router) - end -end diff --git a/src/schematics/routes.jl b/src/schematics/routes.jl index 481151d44..0784a5c6a 100644 --- a/src/schematics/routes.jl +++ b/src/schematics/routes.jl @@ -38,7 +38,7 @@ RouteComponent( sty::Paths.Style, meta::Meta ) where {T} = RouteComponent{T}(name, r, gw, [sty], meta) -hooks(rc::RouteComponent) = (p0=PointHook(rc.r.p0, rc.r.α0), p1=PointHook(rc.r.p1, rc.r.α1)) +hooks(rc::RouteComponent) = (p0=PointHook(rc.r.p0, rc.r.α0), p1=PointHook(rc.r.p1, rc.r.α1 + 180°)) function redecorate!(path::Path, sty::Paths.Style) end function redecorate!(path::Path, sty::Paths.DecoratedStyle) @@ -268,3 +268,34 @@ function _update_with_plan!(rule::Paths.SingleChannelRouting, route_node, sch) global_node = trans(rule.channel.node) return rule.global_channel = Paths.RouteChannel(Path([global_node])) end + +# AutoChannelRouting +# No update when `route!` is called on graph +# Only when planning +function _update_with_plan!(rule::Paths.AutoChannelRouting{T}, route_node, sch) where {T} + pin_idx = length(rule.router.pins) + 1 + push!(rule.router.pins, hooks(route_node.component).p0) + push!(rule.router.pins, hooks(route_node.component).p1) + push!(rule.router.net_pins, (pin_idx, pin_idx + 1)) + push!(rule.router.net_wires, Paths.NetWire()) + # If all paths have been added, go ahead and run autorouting + if length(rule.router.net_pins) == length(rule.router.net_paths) + g, ixns = Paths.build_channel_graph( + rule.router.pins, + getproperty.(rule.router.channels, :path), + T + ) + # Populate the router's graph and intersection dict in-place + # (ChannelRouter is immutable, but its mutable fields can be mutated) + ar_g = rule.router.channel_graph + for _ in 1:(Paths.nv(g) - Paths.nv(ar_g)) + Paths.add_vertex!(ar_g) + end + for e in edges(g) + Paths.add_edge!(ar_g, e.src, e.dst) + end + merge!(rule.router.channel_intersections, ixns) + Paths.assign_channels!(rule.router) + Paths.assign_tracks!(rule.router) + end +end diff --git a/test/test_examples.jl b/test/test_examples.jl index 0d04eb313..a84306afd 100644 --- a/test/test_examples.jl +++ b/test/test_examples.jl @@ -1,3 +1,14 @@ +@testitem "Channel Autorouter Examples" setup = [CommonTestSetup] begin + include("../examples/ChannelAutorouter/ChannelAutorouter.jl") + for (name, fn) in ChannelAutorouter.ALL_EXAMPLES + @testset "$name" begin + c, ar = fn() + @test c isa DeviceLayout.Cell + @test all(length.(ar.net_wires) .> 0) + end + end +end + @testitem "ExamplePDK" setup = [CommonTestSetup] begin include("../examples/DemoQPU17/DemoQPU17.jl") @time "Total" schematic, artwork = DemoQPU17.qpu17_demo(dir=tdir) From 96bbdd9dfb385247fa82027a3dc8b3201c14b7ba Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Tue, 21 Apr 2026 16:10:15 +0200 Subject: [PATCH 23/32] Fix tests and remove more dead code --- examples/ChannelAutorouter/ChannelAutorouter.jl | 2 +- src/paths/channel_routing_core.jl | 2 -- src/schematics/routes.jl | 8 +++++--- test/test_aqua.jl | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/ChannelAutorouter/ChannelAutorouter.jl b/examples/ChannelAutorouter/ChannelAutorouter.jl index bac7758b5..491107375 100644 --- a/examples/ChannelAutorouter/ChannelAutorouter.jl +++ b/examples/ChannelAutorouter/ChannelAutorouter.jl @@ -495,7 +495,7 @@ function example_crossing_schematic() r2 = route!(g, rule, comp_nodes[2]=>:p1_south, comp_nodes[3]=>:p1_north, Paths.Trace(WW*1mm), GDSMeta()) sch = plan(g) - paths = [SchematicDrivenLayout.path(r1), SchematicDrivenLayout.path(r2)] + paths = [SchematicDrivenLayout.path(r1.component), SchematicDrivenLayout.path(r2.component)] c = Cell(sch.coordinate_system) # c = visualize_router_state(ar; wire_width=WW) diff --git a/src/paths/channel_routing_core.jl b/src/paths/channel_routing_core.jl index 3f454ce2a..fc16a5512 100644 --- a/src/paths/channel_routing_core.jl +++ b/src/paths/channel_routing_core.jl @@ -762,8 +762,6 @@ function _delete_segment!(ar, ws; reset_tracks=true, from_net=true) ) end end - # Delete the segment waypoint - delete!(ar.segment_waypoints, ws) # if from_net, delete ws from net_wires # We set from_net to false when looping over net segments so it's not changing under us diff --git a/src/schematics/routes.jl b/src/schematics/routes.jl index 0784a5c6a..f6071da73 100644 --- a/src/schematics/routes.jl +++ b/src/schematics/routes.jl @@ -270,16 +270,18 @@ function _update_with_plan!(rule::Paths.SingleChannelRouting, route_node, sch) end # AutoChannelRouting -# No update when `route!` is called on graph +function _update_with_graph!(rule::Paths.AutoChannelRouting, route_node, graph; kwargs...) + push!(rule.router.net_wires, Paths.NetWire()) # So we know how many routes to expect in planning + return +end # Only when planning function _update_with_plan!(rule::Paths.AutoChannelRouting{T}, route_node, sch) where {T} pin_idx = length(rule.router.pins) + 1 push!(rule.router.pins, hooks(route_node.component).p0) push!(rule.router.pins, hooks(route_node.component).p1) push!(rule.router.net_pins, (pin_idx, pin_idx + 1)) - push!(rule.router.net_wires, Paths.NetWire()) # If all paths have been added, go ahead and run autorouting - if length(rule.router.net_pins) == length(rule.router.net_paths) + if length(rule.router.net_pins) == length(rule.router.net_wires) g, ixns = Paths.build_channel_graph( rule.router.pins, getproperty.(rule.router.channels, :path), diff --git a/test/test_aqua.jl b/test/test_aqua.jl index 186c7c7ee..543893796 100644 --- a/test/test_aqua.jl +++ b/test/test_aqua.jl @@ -3,7 +3,7 @@ # Everything but stdlib should have compat versions Aqua.test_deps_compat( DeviceLayout, - ignore=[:Dates, :LinearAlgebra, :Logging, :Random, :UUIDs], + ignore=[:Dates, :LinearAlgebra, :Logging, :Random, :UUIDs, :SparseArrays], check_extras=(; ignore=[:Test]) ) # We define ForwardDiff.extract_derivative with Unitful.Quantity; ignore that one From 46c130d81fd2ff59b7401bce292548a59a614f30 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Thu, 23 Apr 2026 19:28:49 +0200 Subject: [PATCH 24/32] Cleanup redundant channels field and fuzzy lookup --- src/paths/channel_autorouter.jl | 35 ++++++++++++--------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 93af1bfdf..2043c8d12 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -422,42 +422,37 @@ end ######## Actually doing the path construction struct AutoChannelRouting{T <: Coordinate} <: AbstractChannelRouting - channels::Vector{RouteChannel{T}} + router::ChannelRouter{T} transition_rule::RouteRule transition_margin::T - router::ChannelRouter{T} end -function AutoChannelRouting(ar::ChannelRouter{T}, transition_rule, margin) where {T} - return AutoChannelRouting{T}(ar.channels, transition_rule, convert(T, margin), ar) +function AutoChannelRouting(channels::Vector{RouteChannel{T}}, transition_rule, margin) where {T} + return AutoChannelRouting{T}(ChannelRouter(channels), transition_rule, convert(T, margin)) end entry_rules(r::AutoChannelRouting) = Iterators.repeated(r.transition_rule) exit_rule(r::AutoChannelRouting) = r.transition_rule function track_path_segments(rule::AutoChannelRouting, pa::Path, _) + net_idx = findfirst( + pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), + rule.router.pins[first.(rule.router.net_pins)] + ) return [ - track_path_segment(rule.router, channel, pa; margin=rule.transition_margin) for - channel in rule.channels[channels_taken(rule.router, pa)] + track_path_segment(rule.router, channel, net_idx; margin=rule.transition_margin) for + channel in channels_taken(rule.router, net_idx) ] end function track_path_segment( ar::ChannelRouter{T}, - ch::RouteChannel, - pa::Path; + channel_idx::Int, + net_idx::Int; margin=zero(T) ) where {T} + ch = ar.channels[channel_idx] # Get the track wire segment from the router # Assume there is exactly one wire segment belonging to this path in the channel - # Channel node might have been converted to store in router, so just check start point/direction - channel_idx = findfirst( - chn -> p0(chn.node.seg) ≈ p0(ch.path) && α0(chn.node.seg) == α0(ch.path), - ar.channels - ) - net_idx = findfirst( - pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), - ar.pins[first.(ar.net_pins)] - ) wireseg_idx = findfirst(ws -> running_channel(ws) == channel_idx, net_wire(ar, net_idx)) wireseg = net_wire(ar, net_idx)[wireseg_idx] track_idx = segment_track(ar, wireseg) @@ -483,10 +478,6 @@ function track_path_segment( ) end -function channels_taken(ar::ChannelRouter, pa::Path) - net_idx = findfirst( - pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), - ar.pins[first.(ar.net_pins)] - ) +function channels_taken(ar::ChannelRouter, net_idx::Int) return [running_channel(wireseg) for wireseg in net_wire(ar, net_idx)] end From 129c61535d1ce6829bb28b9c0dd5661d7e9722c2 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 24 Apr 2026 14:43:01 +0200 Subject: [PATCH 25/32] Cleanup --- docs/src/examples/autorouter.md | 10 +++++----- examples/ChannelAutorouter/ChannelAutorouter.jl | 8 ++++---- src/paths/channel_autorouter.jl | 8 +++++++- src/paths/channel_routing_core.jl | 4 +--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/src/examples/autorouter.md b/docs/src/examples/autorouter.md index 78b42c298..ae60cd3ff 100644 --- a/docs/src/examples/autorouter.md +++ b/docs/src/examples/autorouter.md @@ -130,15 +130,15 @@ save("autoroute_bspline.png", c); nothing # hide ``` -## 40-net fan-out +## 100-net fan-out -40 nets fan out through a single wide channel from an inner row of pins to an outer row with twice the spacing. +100 nets fan out through a single wide channel from an inner row of pins to an outer row with twice the spacing. ```@example autorouter -c, ar = ChannelAutorouter.example_fanout40() -save("autoroute_fanout40.png", c); nothing # hide +c, ar = ChannelAutorouter.example_fanout100() +save("autoroute_fanout100.svg", c); nothing # hide ``` ```@raw html - + ``` diff --git a/examples/ChannelAutorouter/ChannelAutorouter.jl b/examples/ChannelAutorouter/ChannelAutorouter.jl index 491107375..eddd6f24c 100644 --- a/examples/ChannelAutorouter/ChannelAutorouter.jl +++ b/examples/ChannelAutorouter/ChannelAutorouter.jl @@ -442,7 +442,7 @@ end # ── Example 10: 40-net fan-out ─────────────────────────────────────────────── # 40 nets fan out through a single wide channel from inner to outer pin rows. -function example_fanout40() +function example_fanout100() lx_outer = ly_outer = 10e6nm lx_inner = ly_inner = 5e6nm @@ -450,7 +450,7 @@ function example_fanout40() Path(Point(-lx_outer / 2, -(ly_inner / 2 + (ly_outer - ly_inner) / 4))) straight!(fanout_space_bottom, lx_outer, Paths.Trace(0.8 * (ly_outer - ly_inner) / 4)) - n_nets = 40 + n_nets = 100 x0s = range(-lx_outer / 2, stop=lx_outer / 2, length=n_nets + 2)[2:(end - 1)] x1s = range(-lx_inner / 2, stop=lx_inner / 2, length=n_nets + 2)[2:(end - 1)] p0s = [PointHook(x, -ly_outer / 2, -90°) for x in x0s] @@ -462,7 +462,7 @@ function example_fanout40() @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" paths = Path.(routes, Ref(Paths.Trace(WW))) @assert isempty(inter_path_intersections(paths)) "No crossings" - @assert length(ar.channel_tracks[1]) <= 20 "Left and right halves share tracks" + @assert length(ar.channel_tracks[1]) <= n_nets/2 "Left and right halves share tracks" return c, ar end @@ -520,7 +520,7 @@ const ALL_EXAMPLES = [ "angled" => example_angled, "dense" => example_dense, "bspline" => example_bspline, - "fanout40" => example_fanout40, + "fanout100" => example_fanout100, "crossing_schematic" => example_crossing_schematic, ] diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 2043c8d12..0a7e0fba1 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -427,6 +427,9 @@ struct AutoChannelRouting{T <: Coordinate} <: AbstractChannelRouting transition_margin::T end +function AutoChannelRouting(router::ChannelRouter{T}, transition_rule, margin) where {T} + return AutoChannelRouting{T}(router, transition_rule, convert(T, margin)) +end function AutoChannelRouting(channels::Vector{RouteChannel{T}}, transition_rule, margin) where {T} return AutoChannelRouting{T}(ChannelRouter(channels), transition_rule, convert(T, margin)) end @@ -434,10 +437,13 @@ entry_rules(r::AutoChannelRouting) = Iterators.repeated(r.transition_rule) exit_rule(r::AutoChannelRouting) = r.transition_rule function track_path_segments(rule::AutoChannelRouting, pa::Path, _) - net_idx = findfirst( + matching_idx = findall( pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), rule.router.pins[first.(rule.router.net_pins)] ) + isempty(matching_idx) && error("Could not find start pin corresponding to path $(pa.name) starting at $(p0(pa)).") + length(matching_idx) > 1 && error("Multiple nets $(matching_idx) including $(pa.name) start at $(p0(pa)). AutoChannelRouting requires distinct starting pins.") + net_idx = only(matching_idx) return [ track_path_segment(rule.router, channel, net_idx; margin=rule.transition_margin) for channel in channels_taken(rule.router, net_idx) diff --git a/src/paths/channel_routing_core.jl b/src/paths/channel_routing_core.jl index fc16a5512..ece295b32 100644 --- a/src/paths/channel_routing_core.jl +++ b/src/paths/channel_routing_core.jl @@ -242,10 +242,8 @@ end function prev_next_tendency(ar, ws; use_wire_direction=true) channel_idx = running_channel(ws) start_channel, stop_channel = bounding_channels(ws) - # Distances along bounding and running channels + # Distances along bounding channels s_along_start = pathlength_at_intersection(ar, start_channel, channel_idx) - s1 = pathlength_at_intersection(ar, channel_idx, start_channel) - s2 = pathlength_at_intersection(ar, channel_idx, stop_channel) s_along_stop = pathlength_at_intersection(ar, stop_channel, channel_idx) # Directions of bounding and running channels (Float64 radians from interface) start_dir = direction_at_intersection(ar, start_channel, channel_idx) From 2a1442e465c641b3826921c881cef0e2d0c6e309 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Fri, 24 Apr 2026 14:43:39 +0200 Subject: [PATCH 26/32] Run formatter --- src/paths/channel_autorouter.jl | 58 ++++++++++++++++++++++--------- src/paths/channel_routing_core.jl | 38 +++++++++----------- src/schematics/routes.jl | 5 +-- test/test_autorouter_internals.jl | 4 +-- 4 files changed, 63 insertions(+), 42 deletions(-) diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 0a7e0fba1..90cc4e0c6 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -239,13 +239,11 @@ function print_segments(ar::ChannelRouter, net) is_pin(ar, s1) ? "Pin $(graphidx_to_pin(ar, s1))" : "Channel $s1", is_pin(ar, s2) ? "Pin $(graphidx_to_pin(ar, s2))" : "Channel $s2" ] - println( - """ - Segment $i: - Runs along Channel $(running_channel(ws)), Track $(segment_track(ar, ws)) - From $(channel_names[1]) to $(channel_names[2]) - """ - ) + println(""" + Segment $i: + Runs along Channel $(running_channel(ws)), Track $(segment_track(ar, ws)) + From $(channel_names[1]) to $(channel_names[2]) + """) end end @@ -287,12 +285,14 @@ function autoroute!( if verbose n_routed = count(!isempty, ar.net_wires[collect(net_indices)]) n_total = length(net_indices) - max_tracks = maximum(num_tracks(ar, ch) for ch in 1:num_channels(ar)) - @info "Autorouting complete" nets_routed=n_routed nets_total=n_total max_tracks_per_channel=max_tracks + max_tracks = maximum(num_tracks(ar, ch) for ch = 1:num_channels(ar)) + @info "Autorouting complete" nets_routed = n_routed nets_total = n_total max_tracks_per_channel = + max_tracks for idx in net_indices for (seg_i, ws) in enumerate(net_wire(ar, idx)) if isnothing(segment_track(ar, ws)) - @warn "Net $idx segment $seg_i: no track assigned" channel=running_channel(ws) + @warn "Net $idx segment $seg_i: no track assigned" channel = + running_channel(ws) end end end @@ -329,12 +329,24 @@ Returns a `BitVector` of length `num_nets(ar)` where `true` means the route produced valid geometry. When a route fails, the corresponding `@error` from `route!()` is still emitted to the logging system. """ -function validate_routes(ar::ChannelRouter{T}, sty; atol=DeviceLayout.onenanometer(T)) where {T} +function validate_routes( + ar::ChannelRouter{T}, + sty; + atol=DeviceLayout.onenanometer(T) +) where {T} success = trues(num_nets(ar)) for (idx, rt) in enumerate(ar.net_routes) pa = Path(rt.p0, α0=rt.α0) - ok = route!(pa, rt.p1, rt.α1, rt.rule, sty; - waypoints=rt.waypoints, waydirs=rt.waydirs, atol=atol) + ok = route!( + pa, + rt.p1, + rt.α1, + rt.rule, + sty; + waypoints=rt.waypoints, + waydirs=rt.waydirs, + atol=atol + ) success[idx] = something(ok, false) # route! currently returns nothing on failure end return success @@ -430,8 +442,16 @@ end function AutoChannelRouting(router::ChannelRouter{T}, transition_rule, margin) where {T} return AutoChannelRouting{T}(router, transition_rule, convert(T, margin)) end -function AutoChannelRouting(channels::Vector{RouteChannel{T}}, transition_rule, margin) where {T} - return AutoChannelRouting{T}(ChannelRouter(channels), transition_rule, convert(T, margin)) +function AutoChannelRouting( + channels::Vector{RouteChannel{T}}, + transition_rule, + margin +) where {T} + return AutoChannelRouting{T}( + ChannelRouter(channels), + transition_rule, + convert(T, margin) + ) end entry_rules(r::AutoChannelRouting) = Iterators.repeated(r.transition_rule) exit_rule(r::AutoChannelRouting) = r.transition_rule @@ -441,8 +461,12 @@ function track_path_segments(rule::AutoChannelRouting, pa::Path, _) pin -> pin.p ≈ p0(pa) && isapprox_angle(in_direction(pin), α0(pa)), rule.router.pins[first.(rule.router.net_pins)] ) - isempty(matching_idx) && error("Could not find start pin corresponding to path $(pa.name) starting at $(p0(pa)).") - length(matching_idx) > 1 && error("Multiple nets $(matching_idx) including $(pa.name) start at $(p0(pa)). AutoChannelRouting requires distinct starting pins.") + isempty(matching_idx) && error( + "Could not find start pin corresponding to path $(pa.name) starting at $(p0(pa))." + ) + length(matching_idx) > 1 && error( + "Multiple nets $(matching_idx) including $(pa.name) start at $(p0(pa)). AutoChannelRouting requires distinct starting pins." + ) net_idx = only(matching_idx) return [ track_path_segment(rule.router, channel, net_idx; margin=rule.transition_margin) for diff --git a/src/paths/channel_routing_core.jl b/src/paths/channel_routing_core.jl index ece295b32..f2ab1aba1 100644 --- a/src/paths/channel_routing_core.jl +++ b/src/paths/channel_routing_core.jl @@ -62,20 +62,20 @@ Subtypes must implement the following interface functions so that the core channel-routing algorithms can operate without knowledge of the underlying geometry representation: -- `channel_graph(prob)::SimpleGraph{Int}` -- `num_channels(prob)::Int` -- `num_nets(prob)::Int` -- `num_pins(prob)::Int` -- `net_pins(prob, net)::Tuple{Int,Int}` -- `net_wire(prob, net)::NetWire` -- `channel_segments(prob, ch)::Vector{TrackWireSegment}` -- `channel_tracks(prob, ch)::Vector{Track}` -- `num_tracks(prob, ch)::Int` -- `pathlength_at_intersection(prob, ch1, ch2)::T` -- `direction_at_intersection(prob, ch1, ch2)::Float64` (radians) -- `channel_width(prob, ch, s)::T` -- `is_pin(prob, idx)::Bool` -- `segment_offset(prob, ws, s...; use_wire_direction)::T` + - `channel_graph(prob)::SimpleGraph{Int}` + - `num_channels(prob)::Int` + - `num_nets(prob)::Int` + - `num_pins(prob)::Int` + - `net_pins(prob, net)::Tuple{Int,Int}` + - `net_wire(prob, net)::NetWire` + - `channel_segments(prob, ch)::Vector{TrackWireSegment}` + - `channel_tracks(prob, ch)::Vector{Track}` + - `num_tracks(prob, ch)::Int` + - `pathlength_at_intersection(prob, ch1, ch2)::T` + - `direction_at_intersection(prob, ch1, ch2)::Float64` (radians) + - `channel_width(prob, ch, s)::T` + - `is_pin(prob, idx)::Bool` + - `segment_offset(prob, ws, s...; use_wire_direction)::T` """ abstract type AbstractChannelProblem{T} end @@ -113,12 +113,7 @@ adjoining_channel(ar::AbstractChannelProblem, pin) = neighbors(channel_graph(ar), pin_to_graphidx(ar, pin))[1] # Offset coordinate or function for the section of track with given width -function track_section_offset( - n_tracks, - section_width::Number, - track_idx; - reversed=false -) +function track_section_offset(n_tracks, section_width::Number, track_idx; reversed=false) # (spacing) * number of tracks away from middle track sgn = reversed ? -1 : 1 spacing = section_width / (n_tracks + 1) @@ -838,7 +833,8 @@ function routing_summary(io::IO, ar::AbstractChannelProblem) channels_used = [running_channel(ws) for ws in wire] tracks = [segment_track(ar, ws) for ws in wire] has_unassigned = any(isnothing, tracks) - println(io, + println( + io, "Net $idx: pins $(pins[1])→$(pins[2]), $(length(wire)) segments, " * "channels $channels_used, tracks $tracks" * (has_unassigned ? " [UNASSIGNED TRACKS]" : "") diff --git a/src/schematics/routes.jl b/src/schematics/routes.jl index f6071da73..2d594e7ad 100644 --- a/src/schematics/routes.jl +++ b/src/schematics/routes.jl @@ -38,7 +38,8 @@ RouteComponent( sty::Paths.Style, meta::Meta ) where {T} = RouteComponent{T}(name, r, gw, [sty], meta) -hooks(rc::RouteComponent) = (p0=PointHook(rc.r.p0, rc.r.α0), p1=PointHook(rc.r.p1, rc.r.α1 + 180°)) +hooks(rc::RouteComponent) = + (p0=PointHook(rc.r.p0, rc.r.α0), p1=PointHook(rc.r.p1, rc.r.α1 + 180°)) function redecorate!(path::Path, sty::Paths.Style) end function redecorate!(path::Path, sty::Paths.DecoratedStyle) @@ -290,7 +291,7 @@ function _update_with_plan!(rule::Paths.AutoChannelRouting{T}, route_node, sch) # Populate the router's graph and intersection dict in-place # (ChannelRouter is immutable, but its mutable fields can be mutated) ar_g = rule.router.channel_graph - for _ in 1:(Paths.nv(g) - Paths.nv(ar_g)) + for _ = 1:(Paths.nv(g) - Paths.nv(ar_g)) Paths.add_vertex!(ar_g) end for e in edges(g) diff --git a/test/test_autorouter_internals.jl b/test/test_autorouter_internals.jl index caa1cc48d..7ce3b3f0b 100644 --- a/test/test_autorouter_internals.jl +++ b/test/test_autorouter_internals.jl @@ -19,8 +19,8 @@ tpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 90°) """ - Build a ChannelRouter and run channel assignment only (no track assignment). - """ + Build a ChannelRouter and run channel assignment only (no track assignment). + """ function _route_channels(channels, hooks, nets) ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) Paths.assign_channels!(ar) From f174eda750464368a7f404a8772d840536689a33 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Mon, 27 Apr 2026 13:33:20 +0200 Subject: [PATCH 27/32] Autorouter concept docs --- docs/make.jl | 1 + docs/src/concepts/channel_autorouter.md | 152 ++++++++++++++++++++++++ docs/src/concepts/index.md | 1 + docs/src/concepts/routes.md | 2 + docs/src/examples/autorouter.md | 4 +- docs/src/reference/path_api.md | 2 + src/paths/channel_autorouter.jl | 37 ++++++ 7 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 docs/src/concepts/channel_autorouter.md diff --git a/docs/make.jl b/docs/make.jl index 353514929..4cf0237c6 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -60,6 +60,7 @@ makedocs( "Texts" => "concepts/texts.md", "Paths" => "concepts/paths.md", "Routes" => "concepts/routes.md", + "Channel Autorouter" => "concepts/channel_autorouter.md", "Rendering and Export" => "concepts/render.md", "Solid Models (3D Geometry)" => "concepts/solidmodels.md", "Schematic-Driven Design" => "concepts/schematic_driven_design.md", diff --git a/docs/src/concepts/channel_autorouter.md b/docs/src/concepts/channel_autorouter.md new file mode 100644 index 000000000..d7ab2a818 --- /dev/null +++ b/docs/src/concepts/channel_autorouter.md @@ -0,0 +1,152 @@ +# Channel Autorouter + +The channel autorouter connects pairs of pins by choosing paths through a user-defined +network of [`Paths.RouteChannel`](@ref)s. It is the multi-net, multi-channel +counterpart to the [`Paths.SingleChannelRouting`](@ref) rule described in +[Routes](./routes.md#Channel-routing): rather than having the user assign each route a +channel and a track by hand, the autorouter decides which channels each wire passes +through and which track it occupies within each channel. + +!!! warning + + Autorouting solutions may change between minor DeviceLayout.jl versions. Only breaking + changes to the autorouter API, not its output, will force major version bumps. + Autorouting is under active development, so upcoming minor versions are especially + likely to see changes in solutions for fixed routing problems. + +## Problem setup + +A channel-routing problem is described by three pieces of data: + + - **Channels**: a list of [`Paths.RouteChannel`](@ref)s. Channels can be straight, + curved, tapered, or composite — any `Path` that is valid as a `RouteChannel` is + valid as an autorouter channel. Channels intersect where their centerlines cross; + the autorouter precomputes these intersections and treats them as the graph along + which wires may travel. + - **Pins**: a list of `PointHook`s giving the location and direction of + each pin. A pin's ray along its direction must hit a channel; the first + channel it hits becomes the pin's entry or exit channel. + - **Nets**: pairs `(i, j)` of pin indices that should be connected. + +The autorouter then attempts to connect each net by routing a wire from its source +pin, through a sequence of channels, to its destination pin. + +## How routing works + +Routing happens in two stages: + + 1. **Channel assignment** picks, for each net, the sequence of channels its wire will + traverse. This is a shortest-path search in a graph whose vertices are channels and + pins and whose edges are channel intersections, weighted by physical distance + between successive intersections along each channel. Nets are processed one at a + time. Channel assignment does **not** currently take into account crossings, + congestion, or channel capacity. + 2. **Track assignment** proceeds channel by channel. Within a channel, every wire + segment occupies an interval of arclength between its entry and exit. Segments + whose intervals overlap must be placed on distinct tracks. The autorouter builds a + vertical-constraint graph (which segment must be "above" which, given the crossings + that occur entering or leaving the channel at the interval endpoints) and picks a + feasible track order that uses as few tracks as possible, merging non-overlapping + segments onto shared tracks when compatible. + +The algorithms follow the classical channel-routing literature (Hashimoto & Stevens; +Yoshimura & Kuh), with a crossing-aware variant due to Condrat, Kalla, & Blair to +handle channels shared by nets with avoidable crossings. See [References](#References) below. + +Once channels and tracks have been assigned, the autorouter builds a +[`Paths.Route`](@ref) for each net. The +`Route`'s rule is a `Paths.AutoChannelRouting` that uses a user-supplied +**transition rule** (for the short legs between pins and channels, and between +adjacent channels) and a **margin** (how much room to leave for bends at each +transition). The transition rule is typically [`Paths.StraightAnd90`](@ref), +[`Paths.StraightAnd45`](@ref), or [`Paths.BSplineRouting`](@ref). + +## Geometry-level usage + +At the geometry level, construct a `Paths.ChannelRouter` from nets, pins, and +channels, then call `Paths.autoroute!`: + +```julia +using DeviceLayout, .PreferredUnits +import DeviceLayout.Paths: + ChannelRouter, RouteChannel, autoroute!, visualize_router_state + +# Two horizontal channels and one vertical channel, with crossed nets +channels = RouteChannel.([ + (p = Path(5.0, -1.0; α0 = 90°); straight!(p, 8.0, Paths.Trace(2.0)); p), + (p = Path(-1.0, 0.0); straight!(p, 10.0, Paths.Trace(2.0)); p), + (p = Path(-1.0, 6.0); straight!(p, 10.0, Paths.Trace(2.0)); p) +]) + +hooks = [ + PointHook(Point(2.0, -0.5), 270°), # below h_bot + PointHook(Point(8.0, -0.5), 270°), # below h_bot + PointHook(Point(2.0, 6.5), 90°), # above h_top + PointHook(Point(8.0, 6.5), 90°) # above h_top +] +nets = [(1, 4), (2, 3)] # crossed + +ar = ChannelRouter(nets, hooks, channels) +routes = autoroute!(ar, Paths.StraightAnd90(0.1), 0.1) # transition rule, margin + +c = visualize_router_state(ar; wire_width = 0.05) +``` + +The returned `routes` are [`Paths.Route`](@ref) values; convert any of them to a drawn +path with `Path(route, style)`. `visualize_router_state` returns a `Cell` that overlays +the channels, tracks, pin labels, and route geometry — useful for debugging an +unexpected assignment. To check whether every route actually reaches its destination, +call `Paths.validate_routes`. + +Worked examples for common topologies (parallel, crossing, fan-in/fan-out, grid, +angled, B-spline, dense, many-net fan-out) live in the +[Channel Autorouter examples page](@ref channel-autorouter-examples). + +## Schematic-level usage + +At the schematic level, the autorouter is exposed as a [`Paths.RouteRule`](@ref): +construct a `Paths.AutoChannelRouting` from a `ChannelRouter` (or directly from a +vector of channels) and use it as the rule in +[`route!`](@ref route!(::SchematicDrivenLayout.SchematicGraph, ::Paths.RouteRule, ::Pair{SchematicDrivenLayout.ComponentNode, Symbol}, ::Pair{SchematicDrivenLayout.ComponentNode, Symbol}, ::Any, ::Any)). +Every route that shares a channel set should use the **same** rule instance, so that +the underlying router sees all nets: + +```julia +ar = ChannelRouter(channels) +rule = Paths.AutoChannelRouting(ar, Paths.StraightAnd90(0.1mm), 0.1mm) + +route!(g, rule, node1 => :port_a, node2 => :port_b, Paths.Trace(5μm), meta) +route!(g, rule, node3 => :port_a, node4 => :port_b, Paths.Trace(5μm), meta) + +sch = plan(g) +``` + +Pin positions and directions are pulled from the component hooks at each route's +endpoints during `plan`, and channel assignment + track assignment run once these have +been set for the last route that uses `rule`. Schematic-level autorouting does not allow the user to +pre-assign tracks—while [`Paths.SingleChannelRouting`](@ref) uses the `track` keyword in `route!` +(defaulting to incrementing by 1 for each new route), `AutoChannelRouting` makes all +track assignments automatically during `plan`. + +Unlike with `SingleChannelRouting`, the autorouter does not look for channels in the +schematic to find their global coordinates. Channels must be provided to the autorouter in global coordinates. Channels may still be added to a schematic, but this has no effect on routing. + +## Limitations + + - A pin's outward ray must hit exactly one channel. If no channel is in the ray's + path, or the pin points the wrong way, graph construction fails. + - Channels are assumed not to self-intersect; self-intersections are ignored with an + `@info` message. + - Two channels may intersect at most once. Re-entering the same channel pair is not + supported. + - Every net in a single `AutoChannelRouting` rule must start at a distinct pin. Nets + that share a source pin should be split into separate rules (or merged upstream). + - Track assignment does not care about proximity of slightly misaligned tracks + entering from different channels or pins; only exact alignment of entry/exit segments + and topologically avoidable crossings constrain track assignment. + +## References + + - Hashimoto & Stevens, ["Wire routing by optimizing channel assignment within large apertures"](https://cs.baylor.edu/~maurer/CSI5346/originalCR.pdf), *DAC '71: Proceedings of the 8th Design Automation Workshop* (1971). + - Yoshimura & Kuh, ["Efficient Algorithms for Channel Routing"](https://my.ece.utah.edu/~kalla/phy_des/yk.pdf), *IEEE Transactions on Computer-Aided Design of Integrated Circuits and Systems* (1982). + - Condrat, Kalla, & Blair, ["Crossing-aware Channel Routing for Photonic Waveguides"](https://my.ece.utah.edu/~kalla/papers/condrat_crossing-aware_channel_routing_for_photonic_waveguides.pdf), 2013 IEEE 56th International Midwest Symposium on Circuits and Systems (MWSCAS) (2013). diff --git a/docs/src/concepts/index.md b/docs/src/concepts/index.md index 51e6970b3..035465875 100644 --- a/docs/src/concepts/index.md +++ b/docs/src/concepts/index.md @@ -15,6 +15,7 @@ How DeviceLayout handles geometric objects and their metadata. - [Texts](texts.md): Text elements as geometric entities - [Paths](paths.md): Transmission lines and other path-based geometry - [Routes](routes.md): Defining Paths implicitly based on routing rules +- [Channel Autorouter](channel_autorouter.md): Multi-net routing through a channel network - [Rendering and File Export](render.md): How geometry becomes output data - [Solid Models](solidmodels.md): 3D geometry and meshing diff --git a/docs/src/concepts/routes.md b/docs/src/concepts/routes.md index c8bce99b5..c3ada0df6 100644 --- a/docs/src/concepts/routes.md +++ b/docs/src/concepts/routes.md @@ -150,6 +150,8 @@ nothing; # hide ``` +For multi-net routing through a network of channels, where the router decides which channels each wire passes through and which track it occupies, see [Concepts: Channel Autorouter](./channel_autorouter.md). + ## Routing in Schematics We can add `Route`s between components in a schematic using [`route!(g::SchematicGraph, ...)`](@ref route!(::SchematicDrivenLayout.SchematicGraph, ::Paths.RouteRule, ::Pair{SchematicDrivenLayout.ComponentNode, Symbol}, ::Pair{SchematicDrivenLayout.ComponentNode, Symbol}, ::Any, ::Any)), creating flexible connections that are only resolved after floorplanning has determined the positions of the components to be connected. (For more about the schematic workflow, see [Concepts: Schematic-Driven Design](./schematic_driven_design.md)) diff --git a/docs/src/examples/autorouter.md b/docs/src/examples/autorouter.md index ae60cd3ff..d3cd3c8cc 100644 --- a/docs/src/examples/autorouter.md +++ b/docs/src/examples/autorouter.md @@ -1,6 +1,6 @@ -# Channel Autorouter +# [Channel Autorouter](@id channel-autorouter-examples) -The channel autorouter connects pairs of pins by routing wires through a user-defined network of channels. Routing proceeds in two steps: **channel assignment** (choosing which channels each net's wire passes through) and **track assignment** (assigning non-overlapping tracks within each channel). +The channel autorouter connects pairs of pins by routing wires through a user-defined network of channels. Routing proceeds in two steps: **channel assignment** (choosing which channels each net's wire passes through) and **track assignment** (assigning non-overlapping tracks within each channel). See [Concepts: Channel Autorouter](./../concepts/channel_autorouter.md) for a conceptual overview. The full code for these examples can be found [in `examples/ChannelAutorouter/ChannelAutorouter.jl` in the DeviceLayout.jl repository](https://github.com/aws-cqc/DeviceLayout.jl/blob/main/examples/ChannelAutorouter/ChannelAutorouter.jl). diff --git a/docs/src/reference/path_api.md b/docs/src/reference/path_api.md index b7b50aa18..5c4f7d858 100644 --- a/docs/src/reference/path_api.md +++ b/docs/src/reference/path_api.md @@ -114,6 +114,8 @@ Paths.CompoundRouteRule Paths.SingleChannelRouting Paths.RouteChannel + Paths.ChannelRouter + Paths.AutoChannelRouting ``` ### Route drawing diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 90cc4e0c6..9e02da76f 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -43,6 +43,16 @@ end pin_hooks::Vector{<:Hook}, channels::Vector{<:RouteChannel} ) + +Construct a `ChannelRouter` from `nets`, `pin_hooks`, and `channels`. + +`nets` is a list of `(i, j)` pairs of indices into `pin_hooks`; each pair specifies +a wire to be routed from pin `i` to pin `j`. The pin `Hook`s supply the entry/exit +positions and directions; each pin's outward ray (opposite its `in_direction`) must +strike one of `channels` for graph construction to succeed. + +Channel intersections are computed eagerly during construction. Pass the result to +[`autoroute!`](@ref) to actually assign channels and tracks. """ function ChannelRouter(nets, pin_hooks::Vector{<:Hook}, channels::Vector{<:RouteChannel}) T = promote_type(coordinatetype(pin_hooks), coordinatetype(channels)) @@ -66,6 +76,15 @@ function ChannelRouter(nets, pin_hooks::Vector{<:Hook}, channels::Vector{<:Route ) end +""" + ChannelRouter(channels::Vector{RouteChannel{T}}) where {T} + +Construct an empty `ChannelRouter` containing `channels` but no pins or nets. + +Nets and pins are added later — typically by constructing an [`AutoChannelRouting`](@ref) +rule around this router and calling `route!` on a schematic graph, which discovers pins +from the schematic's hooks at `plan` time. +""" function ChannelRouter(channels::Vector{RouteChannel{T}}) where {T} channel_segments = [TrackWireSegment[] for i in eachindex(channels)] channel_tracks = [Track[] for i in eachindex(channels)] @@ -433,6 +452,24 @@ function track_path(ar::ChannelRouter{T}, channel_idx, track_idx) where {T} end ######## Actually doing the path construction +""" + struct AutoChannelRouting{T <: Coordinate} <: AbstractChannelRouting + AutoChannelRouting(router::ChannelRouter, transition_rule::RouteRule, margin) + AutoChannelRouting(channels::Vector{<:RouteChannel}, transition_rule::RouteRule, margin) + +A `RouteRule` that delegates routing decisions to a shared [`ChannelRouter`](@ref). + +At the schematic level, every `route!` call that should share channel and track +assignments must use the **same** `AutoChannelRouting` instance, so that the +underlying `router` sees all nets before channel and track assignment run during +`plan`. The `channels`/`transition_rule`/`margin` form constructs a fresh empty +`ChannelRouter` internally and is convenient when nets are populated from the schematic. + +`transition_rule` routes the short legs between a pin and its first channel, and +between adjacent channels. It is typically [`Paths.StraightAnd90`](@ref), +[`Paths.StraightAnd45`](@ref), or [`Paths.BSplineRouting`](@ref). `margin` +reserves arclength at each end of a channel segment for the transition bends. +""" struct AutoChannelRouting{T <: Coordinate} <: AbstractChannelRouting router::ChannelRouter{T} transition_rule::RouteRule From f20bf766b12be4c677a72febc36dc279e4ebb3ec Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Mon, 27 Apr 2026 15:31:07 +0200 Subject: [PATCH 28/32] Use bipartite shape for best matching instead of dense symmetric adjacency --- src/paths/channel_routing_core.jl | 23 ++++++++++++++++------- test/test_autorouter_internals.jl | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/paths/channel_routing_core.jl b/src/paths/channel_routing_core.jl index f2ab1aba1..cd4cdf8ba 100644 --- a/src/paths/channel_routing_core.jl +++ b/src/paths/channel_routing_core.jl @@ -14,7 +14,7 @@ import Graphs: add_edge!, add_vertex!, rem_edge!, - adjacency_matrix, + has_edge, edges, inneighbors, neighbors, @@ -562,7 +562,7 @@ function assign_tracks_matching!(ar, channel) end end # Find max cardinality valid matching, removing edges as necessary - matching = best_matching!(merging_graph, vcg)[1] # Just the dict, not the indicator + matching = best_matching!(merging_graph, vcg, L, R) end # Assign merged groups to tracks according to VCG tracks = channel_tracks(ar, channel) @@ -584,7 +584,7 @@ function assign_tracks_matching!(ar, channel) end end -function best_matching!(merging_graph, vcg) +function best_matching!(merging_graph, vcg, L, R) # Collect set of edges to remove to_remove = Set{Tuple{Int, Int}}() # Create a temporary copy to help find problematic edges @@ -664,10 +664,19 @@ function best_matching!(merging_graph, vcg) for edge in to_remove rem_edge!(merging_graph, edge...) end - # Any matching is feasible now that we've removed marked edges - return BipartiteMatching.findmaxcardinalitybipartitematching( - BitMatrix(adjacency_matrix(merging_graph)) - ) + # Any matching is feasible now that we've removed marked edges. + # Rows are R so the returned row→col dict is keyed by R, matching how the + # caller looks up `matching[v]` when v crosses from R into L next iteration. + R_vec = collect(R) + L_vec = collect(L) + sort!(R_vec) # For determinism + sort!(L_vec) + bip = falses(length(R_vec), length(L_vec)) + for (i, r) in pairs(R_vec), (j, l) in pairs(L_vec) + bip[i, j] = has_edge(merging_graph, r, l) + end + row_to_col, _ = BipartiteMatching.findmaxcardinalitybipartitematching(BitMatrix(bip)) + return Dict{Int, Int}(R_vec[i] => L_vec[j] for (i, j) in row_to_col) end function dag_shortest_paths(dag, v_sorted, s) diff --git a/test/test_autorouter_internals.jl b/test/test_autorouter_internals.jl index 7ce3b3f0b..d349a98c3 100644 --- a/test/test_autorouter_internals.jl +++ b/test/test_autorouter_internals.jl @@ -223,4 +223,24 @@ new_tracks_net1 = [Paths.segment_track(ar, ws) for ws in Paths.net_wire(ar, 1)] @test all(!isnothing, new_tracks_net1) end + + # ── best_matching! bipartite shape ─────────────────────────────────────── + @testset "best_matching! bipartite shape" begin + import Graphs: SimpleGraph, SimpleDiGraph, add_edge! + + # 4 vertices, L={1,2}, R={3,4}, edges (1,3), (1,4), (2,3). + # True max bipartite matching has 2 R→L pairs (e.g. 3→2, 4→1). + g = SimpleGraph(4) + add_edge!(g, 1, 3) + add_edge!(g, 1, 4) + add_edge!(g, 2, 3) + # Empty VCG preserves all merging edges (no ancestor constraints). + vcg = SimpleDiGraph(4) + + m = Paths.best_matching!(g, vcg, Set([1, 2]), Set([3, 4])) + @test length(m) == 2 + @test all(k in (3, 4) for k in keys(m)) + @test all(v in (1, 2) for v in values(m)) + @test length(unique(values(m))) == 2 # no duplicate L targets + end end From 277267d6756494b0114d91366651591de2970ff3 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Tue, 28 Apr 2026 15:45:42 +0200 Subject: [PATCH 29/32] Add guards for short discretization grids --- src/render/discretization.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/render/discretization.jl b/src/render/discretization.jl index d2bbb68a9..4d4ead3ea 100644 --- a/src/render/discretization.jl +++ b/src/render/discretization.jl @@ -216,6 +216,7 @@ function discretization_grid( bnds::Tuple{Float64, Float64}=(0.0, 1.0); t_scale=1.0 ) + bnds[1] == bnds[2] && return [bnds[1]] dt = 0.01 * bnds[2] ts = zeros(100) ts[1] = bnds[1] @@ -245,7 +246,7 @@ function discretization_grid( t = ts[i] end # Make sure last two points aren't unnecessarily close together - if ts[i - 1] > (ts[i] + ts[i - 2]) / 2 + if i > 2 && ts[i - 1] > (ts[i] + ts[i - 2]) / 2 ts[i - 1] = (ts[i] + ts[i - 2]) / 2 end return ts[1:i] From a85b0156fd0be435cab95856987a6ae78222c97d Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Tue, 28 Apr 2026 15:57:27 +0200 Subject: [PATCH 30/32] Remove escape test, fix docs --- docs/src/examples/autorouter.md | 2 +- test/test_channels.jl | 139 +------------------------------- 2 files changed, 3 insertions(+), 138 deletions(-) diff --git a/docs/src/examples/autorouter.md b/docs/src/examples/autorouter.md index d3cd3c8cc..d8c124f84 100644 --- a/docs/src/examples/autorouter.md +++ b/docs/src/examples/autorouter.md @@ -140,5 +140,5 @@ save("autoroute_fanout100.svg", c); nothing # hide ``` ```@raw html - + ``` diff --git a/test/test_channels.jl b/test/test_channels.jl index e8054117d..712f9bd24 100644 --- a/test/test_channels.jl +++ b/test/test_channels.jl @@ -292,7 +292,7 @@ end import DeviceLayout.Paths: RouteChannel, ChannelRouter using .SchematicDrivenLayout - function test_simple(; split=false) + function test_simple() mypins = [ Point(4.0, 3.0), Point(6.0, 3.0), @@ -309,28 +309,11 @@ end space_paths = Path[] for x0 in [1.0, 5.0, 9.0] pa = Path(x0, 0.0, α0=90°) - if split && x0 == 5.0 - straight!(pa, 4.5, Paths.Trace(2.0)) - push!(space_paths, pa) - pa = Path(x0, 5.5, α0=90°) - straight!(pa, 4.5, Paths.Trace(2.0)) - push!(space_paths, pa) - continue - end straight!(pa, 10.0, Paths.Trace(2.0)) push!(space_paths, pa) end for y0 in [1.0, 5.0, 9.0] pa = Path(0.0, y0) - if split && y0 == 5.0 - pa = Path(0.0, y0 + 0.7) - straight!(pa, 10.0, Paths.Trace(1.0)) - push!(space_paths, pa) - pa = Path(0.0, y0 - 0.7) - straight!(pa, 10.0, Paths.Trace(1.0)) - push!(space_paths, pa) - continue - end straight!(pa, 10.0, Paths.Trace(2.0)) push!(space_paths, pa) end @@ -339,131 +322,13 @@ end mynets = [(i, i + 4) for i = 1:n_wires] ar = ChannelRouter(mynets, pins, RouteChannel.(space_paths)) - # # # # Split space demo - # # # Cut space 2 in half - # # pin_adjoining_spaces = [2, 2, 4, 4, 8, 6, 8, 6] - # # space_coord = [1.0, 5.0, 9.0, 5.0, 1.0, 5.5, 9.0, 4.5] - # # space_coord_idx = [1, 1, 1, 1, 2, 2, 2, 2] - # # space_widths = [2.0, 2.0, 2.0, 2.0, 2.0, 1.0, 2.0, 1.0] - - # # # Split space demo - # # 2 doesn't connect to upper half of horizontal spaces - # rem_edge!(ar.space_graph, 2, 6) - # rem_edge!(ar.space_graph, 2, 7) - # # 4 (other half of 2) doesn't connect to bottom half - # rem_edge!(ar.space_graph, 4, 5) - # rem_edge!(ar.space_graph, 4, 8) - - Paths.assign_channels!(ar) #, fixed_paths=Dict(2=>[2, 8, 4, 6])) + Paths.assign_channels!(ar) Paths.assign_tracks!(ar) - rule = Paths.StraightAnd90(min_bend_radius=0.1, max_bend_radius=0.1) - # rule = Paths.StraightAnd45(min_bend_radius=0.1, max_bend_radius=0.1) - # rule = Paths.BSplineRouting(endpoints_speed=7.5) - # rts = make_routes!(ar, rule) - # paths = [Path(rt, Paths.Trace(0.1)) for rt in rts] - c = Paths.visualize_router_state(ar) - - save("autoroute_test.svg", c, width=10DeviceLayout.Graphics.inch) return c end - function test_grid_escape() - # Grid of sites - dx = 1.5 - dy = 1.5 - nx = 20 - ny = 20 - grid = [(ix, iy) for ix = 1:nx for iy = 1:ny] - sites = [Point(dx * (ix - 1), dy * (iy - 1)) for (ix, iy) in grid] - site_size = 0.5 - - # Channel coordinates - n_xch = nx + 1 # vertical channels - n_ych = ny + 1 # horizontal channels - x_coords = range(-dx / 2, step=dx, length=n_xch) - y_coords = range(-dy / 2, step=dy, length=n_ych) - - # Build channel Paths (vertical then horizontal) - # Margin ensures channels cross each other but don't extend to edge pin positions - margin = 0.5 - space_paths = Path[] - for x in x_coords - pa = Path(x, first(y_coords) - margin, α0=90°) - straight!(pa, last(y_coords) - first(y_coords) + 2margin, Paths.Trace(1.0)) - push!(space_paths, pa) - end - for y in y_coords - pa = Path(first(x_coords) - margin, y) - straight!(pa, last(x_coords) - first(x_coords) + 2margin, Paths.Trace(1.0)) - push!(space_paths, pa) - end - - # Site pins: each site escapes toward the nearest grid edge - site_dirs = map(grid) do (ix, iy) - ix_delta = ix - nx / 2 - iy_delta = iy - ny / 2 - if abs(ix_delta) > abs(iy_delta) - return ix_delta > 0 ? 0.0 : pi - end - return iy_delta > 0 ? pi / 2 : -pi / 2 - end - site_pin_pos = [ - site + 0.5site_size * Point(cos(d), sin(d)) for - (site, d) in zip(sites, site_dirs) - ] - # PointHook in_direction points inward (away from channel); site_dirs point toward channel - site_hooks = [PointHook(p, d + pi) for (p, d) in zip(site_pin_pos, site_dirs)] - - # Edge pins: placed at grid boundary, one per site - edge_dirs = [rem2pi(d + pi, RoundNearest) for d in site_dirs] - - # Edge geometry: fixed coordinate and orientation for each of 4 edges - edge_fixed = [first(x_coords), last(x_coords), first(y_coords), last(y_coords)] - edge_is_vertical = [true, true, false, false] - function edge_index(dir) - abs(dir) < 0.1 && return 1 # left edge (dir ≈ 0) - abs(abs(dir) - pi) < 0.1 && return 2 # right edge (dir ≈ ±π) - abs(dir - pi / 2) < 0.1 && return 3 # bottom edge (dir ≈ π/2) - return 4 # top edge (dir ≈ -π/2) - end - - # Pre-compute edge assignments and per-edge coordinate ranges - edge_assignments = [edge_index(edir) for edir in edge_dirs] - pins_per_edge = [count(==(ei), edge_assignments) for ei = 1:4] - coord_span = (-dx / 2, -dx / 2 + nx * dx) - coord_ranges = [range(coord_span..., length=n) for n in pins_per_edge] - - edge_count = [1, 1, 1, 1] - edge_hooks = map(zip(edge_dirs, edge_assignments)) do (edir, ei) - ci = edge_count[ei] - edge_count[ei] += 1 - fixed = edge_fixed[ei] - c = coord_ranges[ei][ci] - pos = if edge_is_vertical[ei] - Point(fixed - cos(edir), c) - else - Point(c, fixed - sin(edir)) - end - return PointHook(pos, edir + pi) - end - - # Assemble: each site pin connects to its corresponding edge pin - all_hooks = [site_hooks; edge_hooks] - n_wires = length(sites) - mynets = [(i, i + n_wires) for i = 1:n_wires] - - ar = ChannelRouter(mynets, all_hooks, RouteChannel.(space_paths)) - - Paths.assign_channels!(ar) - Paths.assign_tracks!(ar) - - c = Paths.visualize_router_state(ar, wire_width=0.001) - save("autoroute_escape_test.gds", c) - return c - end # Runs without error test_simple() - test_grid_escape() end From 2f1d863485bdf52c9e6174c809dbf8400b8cb89e Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Tue, 28 Apr 2026 20:53:41 +0200 Subject: [PATCH 31/32] Cleanup and improve visualizations --- docs/src/examples/autorouter.md | 2 +- .../ChannelAutorouter/ChannelAutorouter.jl | 6 ++-- src/paths/channel_autorouter.jl | 31 +++++-------------- src/paths/channel_routing_core.jl | 2 +- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/docs/src/examples/autorouter.md b/docs/src/examples/autorouter.md index d8c124f84..c513f6f54 100644 --- a/docs/src/examples/autorouter.md +++ b/docs/src/examples/autorouter.md @@ -136,7 +136,7 @@ save("autoroute_bspline.png", c); nothing # hide ```@example autorouter c, ar = ChannelAutorouter.example_fanout100() -save("autoroute_fanout100.svg", c); nothing # hide +save("autoroute_fanout100.svg", c; width=8DeviceLayout.Unitful.inch); nothing # hide ``` ```@raw html diff --git a/examples/ChannelAutorouter/ChannelAutorouter.jl b/examples/ChannelAutorouter/ChannelAutorouter.jl index eddd6f24c..bbd19f2b9 100644 --- a/examples/ChannelAutorouter/ChannelAutorouter.jl +++ b/examples/ChannelAutorouter/ChannelAutorouter.jl @@ -458,9 +458,11 @@ function example_fanout100() mynets = [(i, i + n_nets) for i = 1:n_nets] ar = ChannelRouter(mynets, vcat(p0s, p1s), [RouteChannel(fanout_space_bottom)]) routes = Paths.autoroute!(ar, Paths.StraightAnd90(10μm), 10μm) - c = Paths.visualize_router_state(ar, wire_width=1μm) + # c = Paths.visualize_router_state(ar, wire_width=1μm) @assert all(length.(ar.net_wires) .> 0) "All nets should be routed" - paths = Path.(routes, Ref(Paths.Trace(WW))) + paths = Path.(routes, Ref(Paths.Trace(10μm))) + c = Cell("fanout100") + render!.(c, paths, GDSMeta()) @assert isempty(inter_path_intersections(paths)) "No crossings" @assert length(ar.channel_tracks[1]) <= n_nets/2 "Left and right halves share tracks" return c, ar diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 9e02da76f..2816a0f74 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -113,15 +113,9 @@ net_pins(ar::ChannelRouter, net) = ar.net_pins[net] net_wire(ar::ChannelRouter, net) = ar.net_wires[net] pin_coordinates(ar::ChannelRouter, pin) = ar.pins[pin].p pin_direction(ar::ChannelRouter, pin) = ar.pins[pin].in_direction -channel_coordinates(ar::ChannelRouter, channel, s) = ar.channels[channel].node.seg(s) -function channel_direction(ar::ChannelRouter, channel, s) - is_pin(ar, channel) && return pin_direction(ar, graphidx_to_pin(ar, channel)) - return direction(ar.channels[channel].node.seg, s) -end -function channel_width(ar::ChannelRouter{T}, channel, s) where {T} - # Intersecting channel is zero where a wire segment hits a pin +function channel_width(ar::ChannelRouter{T}, channel, s...) where {T} is_pin(ar, channel) && return zero(T) - return width(ar.channels[channel].node.sty, s) + return Paths.width(ar.channels[channel].node.sty, s...) end channel_segments(ar::ChannelRouter, channel) = ar.channel_segments[channel] channel_tracks(ar::ChannelRouter, channel) = ar.channel_tracks[channel] @@ -166,7 +160,7 @@ function segment_offset( reversed = use_wire_direction && against_channel(ar, ws) return track_section_offset( length(ar.channel_tracks[channel_idx]), - Paths.width(ar.channels[channel_idx].node.sty, s...), + channel_width(ar, channel_idx, s...), track_idx; reversed ) @@ -266,17 +260,6 @@ function print_segments(ar::ChannelRouter, net) end end -""" - segment_direction(ar::ChannelRouter, ws::TrackWireSegment) - -The angle with the x-axis made by segment `ws` directed along its wire toward its end pin. -""" -function segment_direction(ar::ChannelRouter, ws::TrackWireSegment, s) - off = segment_offset(ar, ws) - seg = Paths.offset(ar.channels[running_channel(ws)].node.seg, off) - return direction(seg, s) -end - """ autoroute!(ar::ChannelRouter, transition_rule, margin; net_indices, fixed_channel_paths, verbose) @@ -388,11 +371,11 @@ function visualize_router_state(ar::ChannelRouter{T}; wire_width=0.1 * oneunit(T c = DeviceLayout.Cell{T}("track_viz") paths = Path.(ar.net_routes, Ref(Paths.Trace(wire_width))) - DeviceLayout.render!.(c, paths, GDSMeta(5)) + DeviceLayout.render!.(c, paths, GDSMeta(7)) channels = channel_paths(ar) DeviceLayout.render!.(c, channels, GDSMeta(3)) tracks = track_paths(ar) - DeviceLayout.render!.(c, tracks, GDSMeta(2)) + DeviceLayout.render!.(c, tracks, GDSMeta(1)) trlab = track_labels(ar, tracks) DeviceLayout.text!.(c, trlab, GDSMeta(4)) for pa in paths @@ -443,10 +426,10 @@ end function track_path(ar::ChannelRouter{T}, channel_idx, track_idx) where {T} n_tracks = length(channel_tracks(ar, channel_idx)) seg = track_path_segment(n_tracks, ar.channels[channel_idx].node, track_idx) - w = Paths.width(ar.channels[channel_idx].node.sty, zero(T)) + w = channel_width(ar, channel_idx, zero(T)) # Just illustrative, assume constant width pa = Path( [Paths.Node(seg, Paths.Trace(0.9 * w / (n_tracks + 1)))]; - name="$channel_idx:$track_idx" + name="$channel_idx.$track_idx" ) return pa end diff --git a/src/paths/channel_routing_core.jl b/src/paths/channel_routing_core.jl index cd4cdf8ba..28761a43d 100644 --- a/src/paths/channel_routing_core.jl +++ b/src/paths/channel_routing_core.jl @@ -73,7 +73,7 @@ geometry representation: - `num_tracks(prob, ch)::Int` - `pathlength_at_intersection(prob, ch1, ch2)::T` - `direction_at_intersection(prob, ch1, ch2)::Float64` (radians) - - `channel_width(prob, ch, s)::T` + - `channel_width(prob, ch, s...)::T` - `is_pin(prob, idx)::Bool` - `segment_offset(prob, ws, s...; use_wire_direction)::T` """ From bef5ea5d5bf740a5d22da8b26ceb411ff2511e03 Mon Sep 17 00:00:00 2001 From: Gregory Peairs Date: Wed, 29 Apr 2026 15:37:36 +0200 Subject: [PATCH 32/32] Minor cleanup, demo tapered channel --- docs/src/examples/autorouter.md | 2 +- .../ChannelAutorouter/ChannelAutorouter.jl | 26 +++++++++---------- src/paths/channel_autorouter.jl | 2 +- test/test_autorouter_internals.jl | 16 ++++++------ 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/src/examples/autorouter.md b/docs/src/examples/autorouter.md index c513f6f54..45ea1787f 100644 --- a/docs/src/examples/autorouter.md +++ b/docs/src/examples/autorouter.md @@ -119,7 +119,7 @@ save("autoroute_dense.png", c); nothing # hide ## B-spline channels -Curved channels using B-spline geometry with `BSplineRouting` transitions. Same fan-in/fan-out topology as above but with non-straight channels. +Curved channels using B-spline geometry with `BSplineRouting` transitions. Same fan-in/fan-out topology as above but with non-straight channels and a tapered channel. ```@example autorouter c, ar = ChannelAutorouter.example_bspline() diff --git a/examples/ChannelAutorouter/ChannelAutorouter.jl b/examples/ChannelAutorouter/ChannelAutorouter.jl index bbd19f2b9..cf429d9bc 100644 --- a/examples/ChannelAutorouter/ChannelAutorouter.jl +++ b/examples/ChannelAutorouter/ChannelAutorouter.jl @@ -18,8 +18,8 @@ import .Paths: Horizontal channel at height `y`, from `x0` to `x1`. """ function hchannel(x0, x1, y; width=2.0) - pa = Path(Float64(x0), Float64(y)) - straight!(pa, Float64(x1 - x0), Paths.Trace(Float64(width))) + pa = Path(x0, y) + straight!(pa, x1 - x0, Paths.Trace(width)) return pa end @@ -27,8 +27,8 @@ end Vertical channel at `x`, from `y0` to `y1`. """ function vchannel(x, y0, y1; width=2.0) - pa = Path(Float64(x), Float64(y0), α0=90°) - straight!(pa, Float64(y1 - y0), Paths.Trace(Float64(width))) + pa = Path(x, y0, α0=90°) + straight!(pa, y1 - y0, Paths.Trace(width)) return pa end @@ -36,8 +36,8 @@ end Diagonal channel from `(x0,y0)` at angle `α` for length `len`. """ function dchannel(x0, y0, α, len; width=2.0) - pa = Path(Float64(x0), Float64(y0), α0=α) - straight!(pa, Float64(len), Paths.Trace(Float64(width))) + pa = Path(x0, y0, α0=α) + straight!(pa, len, Paths.Trace(width)) return pa end @@ -45,12 +45,12 @@ end B-spline channel from `(x0,y0)` to `(x1, y1)` at angle `α` at endpoints. """ function bchannel(x0, y0, α, x1, y1; width=2.0) - pa = Path(Float64(x0), Float64(y0), α0=α) + pa = Path(x0, y0, α0=α) bspline!( pa, [Point(x1, y1)], α, - Paths.Trace(Float64(width)), + Paths.Trace(width), auto_speed=true, auto_curvature=true, endpoints_speed=1, @@ -64,10 +64,10 @@ end # Right pin (route goes left): rpin(x, y) → in_direction = 0° # Bottom pin (route goes up): bpin(x, y) → in_direction = 270° # Top pin (route goes down): tpin(x, y) → in_direction = 90° -lpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 180°) -rpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 0°) -bpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 270°) -tpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 90°) +lpin(x, y) = PointHook(Point(float(x), float(y)), 180°) +rpin(x, y) = PointHook(Point(float(x), float(y)), 0°) +bpin(x, y) = PointHook(Point(float(x), float(y)), 270°) +tpin(x, y) = PointHook(Point(float(x), float(y)), 90°) const R = Paths.StraightAnd90(0.1) const WW = 0.05 @@ -405,7 +405,7 @@ end function example_bspline() channels = [ bchannel(0, -2, 30°, 1, 12), # v_left - bchannel(-1, 2, -30°, 12, 9), # h_mid + bchannel(-1, 2, -30°, 12, 9; width=s -> 2.0 + s/10), # h_mid bchannel(10, -2, 30°, 11, 12) # v_right ] # Left pins clustered at y=3,4,5,6 — all cross v_left diff --git a/src/paths/channel_autorouter.jl b/src/paths/channel_autorouter.jl index 2816a0f74..43a0ea094 100644 --- a/src/paths/channel_autorouter.jl +++ b/src/paths/channel_autorouter.jl @@ -377,7 +377,7 @@ function visualize_router_state(ar::ChannelRouter{T}; wire_width=0.1 * oneunit(T tracks = track_paths(ar) DeviceLayout.render!.(c, tracks, GDSMeta(1)) trlab = track_labels(ar, tracks) - DeviceLayout.text!.(c, trlab, GDSMeta(4)) + DeviceLayout.text!.(c, trlab, GDSMeta(0)) for pa in paths for node in pa[1:(end - 1)] DeviceLayout.render!( diff --git a/test/test_autorouter_internals.jl b/test/test_autorouter_internals.jl index d349a98c3..bbf35e9f0 100644 --- a/test/test_autorouter_internals.jl +++ b/test/test_autorouter_internals.jl @@ -3,20 +3,20 @@ # ── Helpers (shared with autoroute_examples.jl) ────────────────────────────── hchannel(x0, x1, y; width=2.0) = - let pa = Path(Float64(x0), Float64(y)) - straight!(pa, Float64(x1 - x0), Paths.Trace(Float64(width))) + let pa = Path(x0, y) + straight!(pa, x1 - x0, Paths.Trace(width)) pa end vchannel(x, y0, y1; width=2.0) = - let pa = Path(Float64(x), Float64(y0), α0=90°) - straight!(pa, Float64(y1 - y0), Paths.Trace(Float64(width))) + let pa = Path(x, y0, α0=90°) + straight!(pa, y1 - y0, Paths.Trace(width)) pa end - lpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 180°) - rpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 0°) - bpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 270°) - tpin(x, y) = PointHook(Point(Float64(x), Float64(y)), 90°) + lpin(x, y) = PointHook(Point(float(x), float(y)), 180°) + rpin(x, y) = PointHook(Point(float(x), float(y)), 0°) + bpin(x, y) = PointHook(Point(float(x), float(y)), 270°) + tpin(x, y) = PointHook(Point(float(x), float(y)), 90°) """ Build a ChannelRouter and run channel assignment only (no track assignment).