app
AI Flow: Routing
Public Made by Adomby adom
The routing stage of AI Flow: routes a placed KiCad board connection by connection and reports what it made, what it could not and why. A guide, never a gate.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
//! Buses: what makes hand routing look hand-routed, and what road navigation has no reason to do.
//! Connections between the same two parts are planned as a group: the ones that don't cross share a
//! layer and run side by side in pin order, and the ones that would cross them take the other layer
//! on purpose instead of finding that out one at a time.
//!
//! Adapted from KiCadRoutingTools (MIT, Copyright (c) 2026 drandyhaas), docs/bus-routing.md and
//! docs/net-ordering.md: its maximum-planar-subset greedy (fewest active conflicts first, then shorter),
//! routing a bus's members middle-out, and each member following its routed neighbour. Two things
//! differ. Its crossings come from unrolling each chip's boundary, which assumes copper can't pass
//! through a part; benchmark B's runs eighteen lines through the pins of a module, so chords
//! are compared directly. And its attraction is a discount around the neighbour's path, where here a
//! member is drawn to the neighbour's shape carried over to its own pins: "each line leaves its
//! pin straight out, parallel to its neighbours at the pin pitch" (4.7, QSPI on PiProbe).
//!
//! The hierarchy isn't touched: this only orders connections inside one level (4.2), and the layer
//! and the neighbour's shape are preferences the search can overrule.
use crate::board::*;
use crate::classify::*;
use crate::geom::*;
use crate::global::chords_cross;
use crate::router::{dir_of, Plan, Router};
use crate::rules::Rules;
use crate::search::{Profile, Query, World};
use std::collections::{BTreeMap, HashMap};
/// Contacts and two-pad parts this close count as one row, the way a connector's pins do. KRT's
/// default detection radius.
const ROW_REACH: f64 = 5.0 * NM;
const ROW_BAND: f64 = 0.5 * NM;
/// Fewest connections that make a bus (KRT's strict filter).
const MIN_MEMBERS: usize = 3;
#[derive(Clone, Debug)]
pub struct Member {
/// Which bus, and this member's turn inside it; buses go first within their level.
pub group: usize,
pub seq: usize,
/// Preferred layer (index into Board::layers), by crossing count.
pub layer: usize,
/// Both pads reach that layer, so it costs no via to use.
pub free: bool,
/// The routed neighbour to follow, and whether its ends are swapped relative to this one's.
pub follow: Option<((usize, usize), bool)>,
}
#[derive(Default)]
pub struct BusPlan {
pub members: HashMap<(usize, usize), Member>,
/// Each bus's lines that don't cross, in their order across the bus, and their layer: what can
/// be laid as one band.
pub bands: Vec<(usize, Vec<(usize, usize)>)>,
pub notes: Vec<String>,
}
impl BusPlan {
/// Sort key inside a level: buses first, biggest first, each member in its turn.
pub fn order_of(&self, t: &crate::classify::Task) -> (usize, usize) {
self.members.get(&(t.a, t.b)).map(|m| (m.group, m.seq)).unwrap_or((usize::MAX, 0))
}
}
/// One end of a bus: a part with a row of pins, or a row of separate contacts or passives.
fn clusters(board: &Board) -> Vec<usize> {
let n = board.footprints.len();
let mut uf = crate::conn::Uf::new(n);
let small: Vec<bool> = board.footprints.iter().map(|f| f.pads.iter().filter(|&&p| !board.pads[p].layers.is_empty()).count() <= 2).collect();
for i in 0..n {
if !small[i] {
continue;
}
for j in i + 1..n {
if small[j] && ref_prefix(&board.footprints[i].reference) == ref_prefix(&board.footprints[j].reference) {
let (pi, pj): (V, V) = (board.footprints[i].at.into(), board.footprints[j].at.into());
// In line with each other: a row. Parts merely near each other aren't one end of a bus.
let aligned = (pi.0 - pj.0).abs() < ROW_BAND || (pi.1 - pj.1).abs() < ROW_BAND;
if aligned && hypot(sub(pi, pj)) <= ROW_REACH {
uf.union(i as u32, j as u32);
}
}
}
}
(0..n).map(|i| uf.find(i as u32) as usize).collect()
}
/// Find the buses among this pass's connections, and give each member its layer, turn and neighbour.
/// A row this long or longer goes down as one band (lines back to back, each beside the last, kept
/// out of optimise). Benchmark B's 15- and 11-line rows look like the designer's that way; on benchmark C the
/// same treatment of its 4-line rows cost 11 joins (290 against 301 of 302, four runs each way,
/// 2026-11-09), so short rows route as before.
const BAND_MIN: usize = 6;
pub fn plan(board: &Board, world: &World, _rules: &Rules, plan: &Plan) -> BusPlan {
let mut out = BusPlan::default();
let cluster = clusters(board);
let mut by: BTreeMap<(Level, usize, usize), Vec<usize>> = BTreeMap::new();
let tasks = &plan.tasks;
for (i, t) in tasks.iter().enumerate() {
// Data only: non-priority lines may via often without a noticeable effect (4.2 item 4), and
// holding them to a plan shut GPIO2's pin on piprobe_v13 for no gain in the board's looks.
if t.a == t.b || t.zone.is_some() || t.level != Level::Data {
continue;
}
let (x, y) = (cluster[board.pads[t.a].fp], cluster[board.pads[t.b].fp]);
if x == y {
continue;
}
by.entry((t.level, x.min(y), x.max(y))).or_default().push(i);
}
let mut groups: Vec<((Level, usize, usize), Vec<usize>)> = by.into_iter().filter(|(_, v)| v.len() >= MIN_MEMBERS).collect();
// Biggest first: within a level the largest bus has the least room to move. Stable, so the
// order is the same on every run.
groups.sort_by_key(|(k, v)| (k.0, std::cmp::Reverse(v.len())));
let chord = |i: usize| -> (V, V) { (board.pads[tasks[i].a].center.into(), board.pads[tasks[i].b].center.into()) };
let free_on = |i: usize, l: usize| board.pads[tasks[i].a].layers.contains(&l) && board.pads[tasks[i].b].layers.contains(&l);
let layers = &world.route_layers;
// What each layer already holds, for counting crossings between buses.
let mut placed: HashMap<usize, Vec<(V, V)>> = HashMap::new();
for (gi, (key, members)) in groups.iter().enumerate() {
let m = members.len();
let conflict: Vec<Vec<bool>> = (0..m).map(|a| (0..m).map(|b| a != b && { let (p, q) = (chord(members[a]), chord(members[b])); chords_cross(p.0, p.1, q.0, q.1) }).collect()).collect();
// The largest set that doesn't cross: fewest active conflicts first, shorter on a tie (KRT).
let mut left: Vec<usize> = (0..m).collect();
let mut planar: Vec<usize> = Vec::new();
let len = |a: usize| { let c = chord(members[a]); hypot(sub(c.0, c.1)) };
while !left.is_empty() {
let &pick = left.iter().min_by(|&&a, &&b| {
let count = |x: usize| left.iter().filter(|&&o| conflict[x][o]).count();
count(a).cmp(&count(b)).then(len(a).total_cmp(&len(b)))
}).unwrap();
planar.push(pick);
left.retain(|&o| o != pick && !conflict[pick][o]);
}
let others: Vec<usize> = (0..m).filter(|a| !planar.contains(a)).collect();
// Which layer for the planar set? A via is dearest, then crossing other buses, then the
// bottom: "don't hop to the bottom layer where the top works" (4.2).
let score = |a_layer: usize, b_layer: usize| -> f64 {
let mut s = 0.0;
for (set, l) in [(&planar, a_layer), (&others, b_layer)] {
for &a in set.iter() {
let c = chord(members[a]);
if !free_on(members[a], l) {
s += 100.0;
}
if let Some(v) = placed.get(&l) {
s += v.iter().filter(|q| chords_cross(c.0, c.1, q.0, q.1)).count() as f64;
}
if l != 0 {
s += 0.01 * (1.0 + hypot(sub(c.0, c.1)) / NM);
}
}
}
s
};
let mut best: Option<(f64, usize, usize)> = None;
for &la in layers {
for &lb in layers {
if layers.len() > 1 && la == lb {
continue;
}
let s = score(la, lb);
if best.map(|b| s < b.0).unwrap_or(true) {
best = Some((s, la, lb));
}
}
}
let Some((_, la, lb)) = best else { continue };
// Physical order across the bus, so each member's neighbour really is beside it.
let (dx, dy) = planar.iter().fold((0.0, 0.0), |acc, &a| {
let c = chord(members[a]);
let (mut x, mut y) = (c.1 .0 - c.0 .0, c.1 .1 - c.0 .1);
// Chords can be listed either way round; add them all facing the same way.
if x * (acc.0 as f64) + y * (acc.1 as f64) < 0.0 {
x = -x;
y = -y;
}
(acc.0 + x, acc.1 + y)
});
let norm = (dx * dx + dy * dy).sqrt().max(1.0);
let across = |a: usize| {
let c = chord(members[a]);
((c.0 .0 + c.1 .0) / 2.0 * -dy + (c.0 .1 + c.1 .1) / 2.0 * dx) / norm
};
let mut row = planar.clone();
row.sort_by(|&a, &b| across(a).total_cmp(&across(b)));
if row.len() >= BAND_MIN {
out.bands.push((la, row.iter().map(|&a| (tasks[members[a]].a, tasks[members[a]].b)).collect()));
}
// Middle out: each member's neighbour is the one before it, toward the middle, and no
// member is ever walled off from its pins by the order.
let mid = row.len() / 2;
let mut turns: Vec<(usize, Option<usize>)> = vec![(row[mid], None)];
for k in 1..=row.len() {
if mid + k < row.len() {
turns.push((row[mid + k], Some(row[mid + k - 1])));
}
if k <= mid {
turns.push((row[mid - k], Some(row[mid - k + 1])));
}
}
// The rest go shortest first: a line that must wrap round another's pin goes after it, so
// they nest, as the bottom row of benchmark B's bus does.
let mut rest = others.clone();
rest.sort_by(|&a, &b| len(a).total_cmp(&len(b)));
// They nest, so they run beside each other too: the next one out follows the one inside it.
if rest.len() >= BAND_MIN {
out.bands.push((lb, rest.iter().map(|&a| (tasks[members[a]].a, tasks[members[a]].b)).collect()));
}
for a in rest {
turns.push((a, None));
}
let same_way = |a: usize, b: usize| {
let (ta, tb) = (&tasks[members[a]], &tasks[members[b]]);
cluster[board.pads[ta.a].fp] == cluster[board.pads[tb.a].fp]
};
for (seq, (a, near)) in turns.iter().enumerate() {
let t = &tasks[members[*a]];
let layer = if planar.contains(a) { la } else { lb };
let follow = near.map(|n| {
let o = &tasks[members[n]];
((o.a, o.b), !same_way(*a, n))
});
out.members.insert((t.a, t.b), Member { group: gi, seq, layer, free: free_on(members[*a], layer), follow });
placed.entry(layer).or_default().push(chord(members[*a]));
}
let name = |c: usize| {
let f = board.footprints.iter().enumerate().find(|(i, _)| cluster[*i] == c).map(|(_, f)| f.reference.clone()).unwrap_or_default();
if board.footprints.iter().enumerate().filter(|(i, _)| cluster[*i] == c).count() > 1 { format!("{}..", f) } else { f }
};
out.notes.push(format!("bus {} to {}: {} lines, {} level, {} side by side on {}, {} on {}", name(key.1), name(key.2), m, key.0.name(), planar.len(), board.layers[la].name, others.len(), board.layers[lb].name));
}
out
}
/// How much of a bus's line length runs beside its neighbour in the bus: same layer, parallel, within
/// `BESIDE` of another line of the same bus. His buses move as one band of lines at 3W; lines routed one at a time fan out and cross,
/// which bends and vias don't show (benchmark B: ours had fewer bends than his and looked worse).
/// Returns (lines measured, share beside, mm measured).
pub fn coherence(board: &Board, rules: &Rules) -> (usize, f64, f64) {
const BESIDE: f64 = 1.5 * NM;
const STEP: f64 = 0.2 * NM;
let mut bare = board.clone();
bare.tracks.clear();
bare.vias.clear();
let mut r = rules.clone();
r.bind(&bare);
let p = crate::router::make_plan(&bare, &r);
let w = crate::router::build_world(&bare, &r);
let bp = plan(&bare, &w, &r, &p);
let net_of = |a: usize| board.pads[a].net;
let (mut lines, mut beside, mut total) = (0usize, 0.0, 0.0);
for ((a, _), m) in &bp.members {
let mine = net_of(*a);
// Any other line of the same bus counts: the order he routes them in is his, not the plan's.
let others: Vec<NetId> = bp.members.iter().filter(|((x, _), o)| o.group == m.group && net_of(*x) != mine).map(|((x, _), _)| net_of(*x)).collect();
let segs_of = |n: NetId| board.tracks.iter().filter(move |t| t.net == n);
let theirs: Vec<&Track> = board.tracks.iter().filter(|t| others.contains(&t.net)).collect();
lines += 1;
for s in segs_of(mine) {
let (sa, sb): (V, V) = (s.a.into(), s.b.into());
let len = hypot(sub(sa, sb));
if len < 1.0 {
continue;
}
let d = dir_of(s.a, s.b);
let k = (len / STEP).ceil().max(1.0) as usize;
for i in 0..k {
let t = (i as f64 + 0.5) / k as f64;
let pt = V(sa.0 + (sb.0 - sa.0) * t, sa.1 + (sb.1 - sa.1) * t);
total += len / k as f64;
let near = theirs.iter().any(|o| {
o.layer == s.layer && pt_seg(pt, o.a.into(), o.b.into()) <= BESIDE && match (d, dir_of(o.a, o.b)) {
(Some(x), Some(y)) => x % 4 == y % 4,
_ => false,
}
});
if near {
beside += len / k as f64;
}
}
}
}
(lines, if total > 0.0 { beside / total } else { 0.0 }, total / NM)
}
/// How close a GND via counts as close by. His benchmark C has one within this of 48 of its 61 signal vias.
const RETURN_REACH: f64 = 1.5 * NM;
impl<'a> Router<'a> {
/// "Any signal via benefits from a GND via close by, when possible" (4.4): once every route is
/// done, so nothing moved for it (lowest priority, like stitching), a GND via next to each signal
/// via that lacks one, wherever one fits. It reaches the plane by itself.
pub(crate) fn return_vias(&mut self) {
let planes = self.board.plane_nets();
let Some(&gnd) = planes.keys().find(|&&n| looks_ground(self.board.net_name(n))) else { return };
let Some(pad) = self.board.pads.iter().position(|p| p.net == gnd && !p.layers.is_empty()) else { return };
let plane_at = |p: V, world: &World| world.zones.iter().any(|(n, _, poly)| *n == gnd && in_poly(p, poly));
let task = Task { net: gnd, level: Level::PlaneFanout, a: pad, b: pad, main: false, zone: None };
// A through pin on the plane is a return path as good as a via.
let mut returns: Vec<V> = self.board.pads.iter().filter(|p| p.net == gnd && p.drill.is_some() && !p.npth).map(|p| p.center.into()).collect();
let mut gnd_vias: Vec<V> = self.board.vias.iter().filter(|v| v.net == gnd).map(|v| v.at.into()).collect();
let mut signal: Vec<(u32, P)> = Vec::new();
for r in self.routes.values() {
if r.task.net == gnd {
gnd_vias.extend(r.vias.iter().map(|&v| V::from(v)));
} else if !matches!(r.task.level, Level::Power) {
signal.extend(r.vias.iter().map(|&v| (r.id, v)));
}
}
signal.sort();
returns.extend(gnd_vias.iter().copied());
let close = |set: &[V], v: P| set.iter().any(|g| hypot(sub(*g, v.into())) <= RETURN_REACH);
let total = signal.len();
let before = signal.iter().filter(|(_, v)| close(&gnd_vias, *v)).count();
let vr = self.rules.via as f64 / 2.0;
let mut added = 0;
let mut boxed = 0;
for (_, v) in &signal {
if close(&returns, *v) {
continue;
}
let c: V = (*v).into();
let profile = Profile { width: 0, neck: 0, clearance: self.rules.clearance_for(gnd, gnd), target_gap: 0, max_vias: 0, margin: 0.0, layer_bias: None };
let q = Query { net: gnd, also: None, sources: &[], targets: &[], profile, rules: &self.rules, soft: None, via_in: None, history: None, free: &[], arrive: None };
let mut site = None;
let mut radius = 2.0 * vr + self.rules.hole_hole as f64;
'look: while radius <= RETURN_REACH {
for k in 0..16 {
let a = k as f64 / 16.0 * std::f64::consts::TAU;
let at = crate::search::snap(to_p(V(c.0 + radius * a.cos(), c.1 + radius * a.sin())));
let (mut b, mut sc) = (Vec::new(), 0.0);
if plane_at(at.into(), &self.world) && self.world.via_ok(at, &q, None, &mut b, &mut sc) {
site = Some(at);
break 'look;
}
}
radius += 0.05 * NM;
}
let Some(at) = site else {
boxed += 1;
continue;
};
let id = self.fresh_id();
self.place(id, task.clone(), Vec::new(), vec![at]);
returns.push(at.into());
gnd_vias.push(at.into());
added += 1;
}
let after = signal.iter().filter(|(_, v)| close(&gnd_vias, *v)).count();
self.log.push(format!("return vias: {after} of {total} signal vias have a GND via within {:.1} mm ({before} before; {added} added, {boxed} with no room)", RETURN_REACH / NM));
}
}
#[cfg(test)]
mod tests {
use super::*;
/// His benchmark B: 26 lines from the module to the contact row. The lines that keep their order run
/// together; the bottom row, which is reversed against the contacts, goes on the other layer, as
/// he routed it.
#[test]
fn benchmark_b_splits_its_bus_the_way_the_designer_did() {
let path = format!("{}/bench/boards/benchb_v210.ref.kicad_pcb", env!("CARGO_MANIFEST_DIR"));
if !std::path::Path::new(&path).exists() {
eprintln!("skipped: {path} is not here (the reference boards do not ship with the source)");
return;
}
let mut b = Board::read(&path).unwrap();
b.tracks.clear();
b.vias.clear();
let mut r = Rules::default();
crate::project::apply(&path, &mut r);
let p = crate::router::make_plan(&b, &r);
let w = crate::router::build_world(&b, &r);
let bus = plan(&b, &w, &r, &p);
let layer_of = |net: &str| {
let id = b.net_ids[net];
let t = p.tasks.iter().find(|t| t.net == id).unwrap();
bus.members[&(t.a, t.b)].layer
};
for n in ["/USER_IO.13", "/USER_IO.16", "/USER_IO.22", "/USER_IO.27"] {
assert_eq!(layer_of(n), 0, "{n} should be with the bus on F.Cu");
}
// Everything with its pins in the reversed order is not on the bus's layer, bar the one it
// can keep. Pairwise reversal means at most one stays.
let top = ["/USER_IO.0", "/USER_IO.1", "/USER_IO.2", "/USER_IO.3", "/USER_IO.4", "/USER_IO.5"].iter().filter(|n| layer_of(n) == 0).count();
assert!(top <= 1, "{top} of the reversed lines stayed on the top");
}
#[test]
fn piprobe_finds_the_qspi_bus() {
let path = format!("{}/bench/boards/piprobe_v13.ref.kicad_pcb", env!("CARGO_MANIFEST_DIR"));
if !std::path::Path::new(&path).exists() {
eprintln!("skipped: {path} is not here (the reference boards do not ship with the source)");
return;
}
let mut b = Board::read(&path).unwrap();
b.tracks.clear();
b.vias.clear();
let mut r = Rules::default();
crate::project::apply(&path, &mut r);
let p = crate::router::make_plan(&b, &r);
let w = crate::router::build_world(&b, &r);
let bus = plan(&b, &w, &r, &p);
let qspi: Vec<_> = p.tasks.iter().filter(|t| b.net_name(t.net).starts_with("QSPI_") && bus.members.contains_key(&(t.a, t.b))).collect();
assert!(qspi.len() >= 6, "{} QSPI lines in a bus", qspi.len());
// SMD on one side: no line may be pushed to the other layer without a via, so none is held.
assert!(qspi.iter().all(|t| bus.members[&(t.a, t.b)].layer == 0 || !bus.members[&(t.a, t.b)].free));
}
}