diff --git a/Project.toml b/Project.toml index 7972a6c2d..8657f02fb 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" @@ -31,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" @@ -47,6 +49,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/docs/make.jl b/docs/make.jl index 1975611a4..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", @@ -71,7 +72,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/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 new file mode 100644 index 000000000..45ea1787f --- /dev/null +++ b/docs/src/examples/autorouter.md @@ -0,0 +1,144 @@ +# [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). 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). + +```@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 + +``` + +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. + +```@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 and a tapered channel. + +```@example autorouter +c, ar = ChannelAutorouter.example_bspline() +save("autoroute_bspline.png", c); nothing # hide +``` + +```@raw html + +``` + +## 100-net fan-out + +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_fanout100() +save("autoroute_fanout100.svg", c; width=8DeviceLayout.Unitful.inch); nothing # hide +``` + +```@raw html + +``` 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/examples/ChannelAutorouter/ChannelAutorouter.jl b/examples/ChannelAutorouter/ChannelAutorouter.jl new file mode 100644 index 000000000..cf429d9bc --- /dev/null +++ b/examples/ChannelAutorouter/ChannelAutorouter.jl @@ -0,0 +1,550 @@ +""" +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, .SchematicDrivenLayout +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(x0, y) + straight!(pa, x1 - x0, Paths.Trace(width)) + return pa +end + +""" +Vertical channel at `x`, from `y0` to `y1`. +""" +function vchannel(x, y0, y1; width=2.0) + pa = Path(x, y0, α0=90°) + straight!(pa, y1 - y0, Paths.Trace(width)) + return pa +end + +""" +Diagonal channel from `(x0,y0)` at angle `α` for length `len`. +""" +function dchannel(x0, y0, α, len; width=2.0) + pa = Path(x0, y0, α0=α) + straight!(pa, len, Paths.Trace(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(x0, y0, α0=α) + bspline!( + pa, + [Point(x1, y1)], + α, + Paths.Trace(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(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 +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. +# 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, MARGIN) + 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, MARGIN) + paths = Path.(routes, Ref(Paths.Trace(WW))) + c = visualize_router_state(ar; wire_width=WW) + @assert isempty(inter_path_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, MARGIN) + 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(inter_path_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, MARGIN) + 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(inter_path_intersections(paths)) "No crossings" + @assert length(ar.channel_tracks[3]) == 3 "Last vertical channel only needs 3 tracks" + return c, ar +end + +# ── Example 5: Multichannel 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, MARGIN) + 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(inter_path_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, MARGIN) + 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, 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(inter_path_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 = 1:6] + + ar = ChannelRouter(nets, hooks, RouteChannel.(channels)) + routes = autoroute!(ar, R, MARGIN) + 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(inter_path_intersections(paths)) "No crossings" + return c, ar +end + +# ── 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 + 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 + # 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 + ) + + 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" + paths = Path.(routes, Ref(Paths.Trace(WW))) + @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 + +# ── Example 10: 40-net fan-out ─────────────────────────────────────────────── +# 40 nets fan out through a single wide channel from inner to outer pin rows. + +function example_fanout100() + 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 = 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] + 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(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 +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.component), SchematicDrivenLayout.path(r2.component)] + 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 = [ + "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, + "fanout100" => example_fanout100, + "crossing_schematic" => example_crossing_schematic, +] + +function run_all_examples(; save_gds=true, save_png=true, dir=@__DIR__) + results = Pair{String, Tuple{Cell, ChannelRouter}}[] + for (name, fn) in ALL_EXAMPLES + @info "Running $name..." + push!(results, name => fn()) + end + if save_gds + for (name, (c, _)) in results + 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(joinpath(dir, "autoroute_$(name).png"), c; spec_warnings=false) + end + @info "Saved $(length(results)) PNG files" + end + return results +end + +end # module diff --git a/src/hooks.jl b/src/hooks.jl index 1ad51b78c..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) @@ -35,6 +38,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)) @@ -104,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 @@ -112,6 +126,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 new file mode 100644 index 000000000..43a0ea094 --- /dev/null +++ b/src/paths/channel_autorouter.jl @@ -0,0 +1,533 @@ +# DeviceLayout-specific ChannelRouter implementation. +# Pure algorithmic core is in channel_routing_core.jl. + +# 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} <: 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 + 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}} + net_routes::Vector{Route{T}} +end + +""" + ChannelRouter( + nets::Vector{Tuple{Int, Int}}, + 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)) + net_wires = [NetWire() for i in eachindex(nets)] + channel_segments = [TrackWireSegment[] for i in eachindex(channels)] + channel_tracks = [Track[] for i in eachindex(channels)] + 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, + Route{T}[] + ) +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)] + return ChannelRouter{T}( + SimpleGraph(), + Tuple{Int, Int}[], + NetWire[], + PointHook{T}[], + channels, + Dict{Tuple{Int, Int}, IntersectionInfo{T}}(), + channel_segments, + channel_tracks, + Route{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) +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 +function channel_width(ar::ChannelRouter{T}, channel, s...) where {T} + is_pin(ar, channel) && return zero(T) + 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] +num_tracks(ar::ChannelRouter, channel) = length(ar.channel_tracks[channel]) + +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) + angle_unitful = running_channel < intersecting_channel ? ixn_info[3] : ixn_info[4] + return rem2pi(uconvert(NoUnits, angle_unitful), RoundNearest) +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]), + channel_width(ar, channel_idx, 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 +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("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) + 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 = (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]) + """) + end +end + +""" + autoroute!(ar::ChannelRouter, transition_rule, margin; net_indices, fixed_channel_paths, verbose) + +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. + +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}}(), + verbose=false +) + affected_nets = reroute_nets!(ar, net_indices; fixed_paths=fixed_channel_paths) + 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 = 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 + +######## 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 + +""" + 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 + +""" + 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)) where {T} + c = DeviceLayout.Cell{T}("track_viz") + + paths = Path.(ar.net_routes, Ref(Paths.Trace(wire_width))) + 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(1)) + trlab = track_labels(ar, tracks) + DeviceLayout.text!.(c, trlab, GDSMeta(0)) + 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) + for l in plab + DeviceLayout.text!(c, l..., GDSMeta(6)) + end + return c +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)] +end + +channel_paths(ar::ChannelRouter) = [channel_path(ar, s) for s = 1:num_channels(ar)] + +function track_labels(ar::ChannelRouter{T}, tracks) where {T} + return [ + 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 + +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 channel_path(ar::ChannelRouter, channel_idx) + return ar.channels[channel_idx].path +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 = 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" + ) + return pa +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 + 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 +entry_rules(r::AutoChannelRouting) = Iterators.repeated(r.transition_rule) +exit_rule(r::AutoChannelRouting) = r.transition_rule + +function track_path_segments(rule::AutoChannelRouting, pa::Path, _) + 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) + ] +end + +function track_path_segment( + ar::ChannelRouter{T}, + 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 + 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 + # 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 + 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, net_idx::Int) + return [running_channel(wireseg) for wireseg in net_wire(ar, net_idx)] +end diff --git a/src/paths/channel_routing_core.jl b/src/paths/channel_routing_core.jl new file mode 100644 index 000000000..28761a43d --- /dev/null +++ b/src/paths/channel_routing_core.jl @@ -0,0 +1,853 @@ +# 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!, + add_vertex!, + rem_edge!, + has_edge, + 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 channels + s_along_start = pathlength_at_intersection(ar, start_channel, channel_idx) + 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 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 + # 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, L, R) + 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, L, R) + # 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. + # 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) + 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 + + # 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` (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) + 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 + +""" + 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([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(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( + 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/src/paths/channels.jl b/src/paths/channels.jl index 4d2494133..f52e01543 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] ) @@ -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} @@ -177,7 +156,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), 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/src/paths/paths.jl b/src/paths/paths.jl index df8b6a91a..07a5ce214 100644 --- a/src/paths/paths.jl +++ b/src/paths/paths.jl @@ -709,8 +709,11 @@ 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 """ @@ -761,7 +764,9 @@ include("segments/bspline_optimization.jl") include("routes.jl") +include("channel_routing_core.jl") include("channels.jl") +include("channel_autorouter.jl") function change_handedness!(seg::Union{Turn, Corner}) return seg.α = -seg.α 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] diff --git a/src/schematics/routes.jl b/src/schematics/routes.jl index 481151d44..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)) +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 +269,36 @@ 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 +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)) + # If all paths have been added, go ahead and run autorouting + 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), + 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 _ = 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_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 diff --git a/test/test_autorouter_internals.jl b/test/test_autorouter_internals.jl new file mode 100644 index 000000000..bbf35e9f0 --- /dev/null +++ b/test/test_autorouter_internals.jl @@ -0,0 +1,246 @@ +@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(x0, y) + straight!(pa, x1 - x0, Paths.Trace(width)) + pa + end + vchannel(x, y0, y1; width=2.0) = + let pa = Path(x, y0, α0=90°) + straight!(pa, y1 - y0, Paths.Trace(width)) + pa + end + + 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). + """ + 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 + + # ── 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 + + # ── 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 diff --git a/test/test_channels.jl b/test/test_channels.jl index 531a09943..712f9bd24 100644 --- a/test/test_channels.jl +++ b/test/test_channels.jl @@ -287,3 +287,48 @@ ## Schematic-level routing test_schematic_single_channel() end + +@testitem "Channel Autorouter" setup = [CommonTestSetup] begin + import DeviceLayout.Paths: RouteChannel, ChannelRouter + using .SchematicDrivenLayout + + function test_simple() + 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°) + 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) + 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)) + + Paths.assign_channels!(ar) + Paths.assign_tracks!(ar) + + c = Paths.visualize_router_state(ar) + return c + end + + # Runs without error + test_simple() +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)