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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317
//! The search. Not a grid: an implicit lattice of exact 0/45/90 degree lines through every pad
//! centre and along every obstacle at exact clearance, so a route hugs copper at the clearance it
//! asked for, leaves a pin straight along the pin's own line, and has few bends by construction.
//!
//! What is borrowed from road navigation:
//! - Direction is part of the search state (OSRM's edge-based graph), so a turn's cost is exact
//! instead of depending on which path reached a cell first.
//! - The search runs in rounds by layer changes (RAPTOR's rounds by transfers): first with no via
//! at all, then with vias, so "don't hop to the bottom layer where the top works" is structural.
//!
//! What is borrowed from routers: a legality check against real copper at every step, a slightly
//! inflated A* heuristic (VTR, nextpnr), soft search for finding who is in the way (Freerouting's
//! rip-in-search, KiCadRoutingTools' blocker analysis).
//!
//! What the search can't see from where it stands comes from global.rs: a coarse map that gives the
//! estimate its true way round obstacles, proves "no path" early, and carries the board-wide plan.
use crate::board::*;
use crate::geom::*;
use crate::index::*;
use crate::rules::Rules;
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap, HashSet};
/// A fast multiplicative hash for the search's state table. SipHash took 18 of 61 s on benchmark C
/// (measured with timers round every call), and these keys aren't attacker-controlled.
#[derive(Default, Clone, Copy)]
pub struct Fx(u64);
impl std::hash::Hasher for Fx {
#[inline]
fn finish(&self) -> u64 {
self.0
}
#[inline]
fn write(&mut self, b: &[u8]) {
for &x in b {
self.write_u64(x as u64);
}
}
#[inline]
fn write_u8(&mut self, v: u8) {
self.write_u64(v as u64);
}
#[inline]
fn write_i64(&mut self, v: i64) {
self.write_u64(v as u64);
}
#[inline]
fn write_u64(&mut self, v: u64) {
self.0 = (self.0.rotate_left(5) ^ v).wrapping_mul(0x517c_c1b7_2722_0a95);
}
}
pub type FxBuild = std::hash::BuildHasherDefault<Fx>;
pub const DIRS: [(i64, i64); 8] = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)];
const NO_DIR: u8 = 8;
/// Guide lines, by family: x = a, y = b, x - y = c, x + y = e. All constants are even nanometres,
/// so every crossing is a whole nanometre.
#[derive(Default, Clone)]
pub struct Lines {
pub v: Vec<i64>,
pub h: Vec<i64>,
pub dp: Vec<i64>,
pub dm: Vec<i64>,
}
#[inline]
fn even_up(v: f64) -> i64 {
let x = v.ceil() as i64;
x + (x & 1)
}
#[inline]
fn even_down(v: f64) -> i64 {
let x = v.floor() as i64;
x - (x & 1)
}
pub fn snap(p: P) -> P {
(p.0 & !1, p.1 & !1)
}
impl Lines {
pub fn add_point(&mut self, p: P) {
let p = snap(p);
self.v.push(p.0);
self.h.push(p.1);
self.dp.push(p.0 - p.1);
self.dm.push(p.0 + p.1);
}
/// The eight tangents of an outline at distance `r`, rounded outward so clearance is kept.
pub fn add_tangents(&mut self, s: &Shape, r: f64) {
let rd = r * SQRT2;
self.v.push(even_up(s.support(1.0, 0.0) + r));
self.v.push(even_down(-s.support(-1.0, 0.0) - r));
self.h.push(even_up(s.support(0.0, 1.0) + r));
self.h.push(even_down(-s.support(0.0, -1.0) - r));
self.dp.push(even_up(s.support(1.0, -1.0) + rd));
self.dp.push(even_down(-s.support(-1.0, 1.0) - rd));
self.dm.push(even_up(s.support(1.0, 1.0) + rd));
self.dm.push(even_down(-s.support(-1.0, -1.0) - rd));
}
pub fn finish(&mut self) {
for v in [&mut self.v, &mut self.h, &mut self.dp, &mut self.dm] {
v.sort_unstable();
v.dedup();
}
}
#[inline]
fn on_family(&self, p: P, d: u8) -> bool {
match d % 4 {
0 => self.h.binary_search(&p.1).is_ok(),
1 => self.dp.binary_search(&(p.0 - p.1)).is_ok(),
2 => self.v.binary_search(&p.0).is_ok(),
_ => self.dm.binary_search(&(p.0 + p.1)).is_ok(),
}
}
/// Distance along direction `d`, in axis steps, to the next crossing line.
#[inline]
fn next_stop(&self, p: P, d: u8) -> Option<i64> {
let (dx, dy) = DIRS[d as usize];
let mut t = i64::MAX;
let next = |a: &[i64], val: i64, up: bool| -> Option<i64> {
if up {
let i = a.partition_point(|&x| x <= val);
a.get(i).map(|&x| x - val)
} else {
let i = a.partition_point(|&x| x < val);
if i > 0 { Some(val - a[i - 1]) } else { None }
}
};
if dx != 0 {
if let Some(s) = next(&self.v, p.0, dx > 0) {
t = t.min(s);
}
}
if dy != 0 {
if let Some(s) = next(&self.h, p.1, dy > 0) {
t = t.min(s);
}
}
let sp = dx - dy;
if sp != 0 {
if let Some(s) = next(&self.dp, p.0 - p.1, sp > 0) {
t = t.min((s + sp.abs() - 1) / sp.abs());
}
}
let sm = dx + dy;
if sm != 0 {
if let Some(s) = next(&self.dm, p.0 + p.1, sm > 0) {
t = t.min((s + sm.abs() - 1) / sm.abs());
}
}
if t == i64::MAX { None } else { Some(t) }
}
}
/// Everything already on the board that a route must respect.
/// How much dearer a millimetre inside a pour to leave is than one anywhere else.
const POUR_AVOID_COST: f64 = 8.0;
/// What straying from the bus line before costs per unit length, at two pitches or more away.
const FOLLOW_COST: f64 = 3.0;
/// What a millimetre costs on top of the pair partner's track on another layer. Two tracks stacked
/// through the board are a wall on both layers: on adom/carrier-fc-rp2350a-mini a split USB pair ran
/// stacked for the module's whole height and sealed nine pads behind it.
const STACK_COST: f64 = 6.0;
/// What a millimetre costs inside the thermal-relief gap of another net's pad. KiCad wants two
/// spokes from each such pad into its pour (starved_thermal otherwise); a track through the gap
/// takes the spokes on that side. A cost, not a wall: one side lost is allowed, and where there is
/// no other way the track still goes. a flight controller board came back with 9 starved pads.
const RELIEF_COST: f64 = 2.5;
/// How near the partner's centre line counts as on top of it.
const STACK_BAND: f64 = 0.6 * NM;
pub struct World {
pub layers: Vec<Grid>,
pub holes: Grid,
pub edge: Grid,
pub route_layers: Vec<usize>,
pub escapes: Vec<(f64, f64, f64, f64)>,
pub zones: Vec<(NetId, usize, Vec<V>)>,
/// The coarse map, its backward searches and the board-wide plan; see global.rs.
pub guide: std::cell::RefCell<crate::global::Guide>,
/// The bus line routed just before this one, on its layer: (layer, its segments, the pitch to
/// keep from it). Running farther than the pitch costs extra, so a bus comes out as parallel
/// lines, each passing a pad row's gaps on its own, as his benchmark B bus does.
pub follow: std::cell::RefCell<Option<(usize, Vec<(V, V)>, f64)>>,
/// Nets with an approved current (the spec's `rails`). Their tracks keep their width on every
/// layer and neck only for the last stretch into a terminal: the width is what carries the
/// current, so a stub layer at neck width (4.2's habit for unrated rails) is not allowed them.
pub rail_nets: std::collections::HashSet<NetId>,
/// Pads' solder mask openings: foreign copper may not bridge one (KiCad's solder_mask_bridge).
pub mask: crate::mask::MaskMap,
/// Each pad's own clearance override, nm (0 when it has none).
pub pad_clearance: Vec<f64>,
pub max_local: f64,
/// Per zone: the other nets with a pad inside its outline. Such a pour can't be a wall to them:
/// they have to get out, and KiCad refills around them. On benchmark B the USB connector
/// sits inside the VBUS pour, and his own D+, D-, CC and GND copper crosses it.
pub pour_open: Vec<Vec<NetId>>,
/// Big pours each net with a pad inside them should leave at once: layer and outline. Copper in
/// one costs extra in the search itself, so the way round is found rather than rejected after.
pub pour_avoid: HashMap<NetId, Vec<(usize, Vec<V>)>>,
/// Pads that join their own net's pour through thermal spokes, per layer, and each one's relief
/// gap (by pad index; 0 where there is none). Copper of another net inside that gap cuts spokes:
/// see RELIEF_COST.
pub relief: Vec<Grid>,
pub relief_gap: Vec<f64>,
pub relief_reach: f64,
/// Each net of a differential pair and its partner. A pair that could not be coupled is routed
/// as two tracks; the second must not run on top of the first on another layer (see STACK_COST).
pub partner: HashMap<NetId, NetId>,
/// The tracks now on the board for each pair net: (route id, layer, ends). Kept by place/unplace.
pub pair_segs: std::cell::RefCell<HashMap<NetId, Vec<(u32, usize, V, V)>>>,
}
#[derive(Clone, Copy, Debug)]
pub struct Profile {
/// Track width on open runs, and inside a fine-pitch escape.
pub width: i64,
pub neck: i64,
/// Hard minimum gap to other nets.
pub clearance: i64,
/// Preferred gap (3W target); less costs extra, never below `clearance`.
pub target_gap: i64,
pub max_vias: u32,
pub margin: f64,
/// A rail's bus layer. `width` then applies on this layer only, everywhere on it; on the other
/// layers the route is a stub at `neck` and each mm costs more.
pub layer_bias: Option<usize>,
}
#[derive(Clone, Debug)]
pub enum Terminal {
/// A pad centre, on any of these layers.
Point { p: P, layers: Vec<usize> },
/// Anywhere inside this net's zone copper on a layer.
Zone { layer: usize, poly: Vec<V> },
/// Anywhere along a routed track of this net: a tap onto a rail (4.2). `wide` rails take a stub
/// at 45 or 90 degrees, others only a Y. `stand_off` is how far a via must stay off the track,
/// measured along the stub (4.4: no via inside a trace, branch 0.6-0.85 mm to the side).
Track { a: P, b: P, layer: usize, wide: bool, stand_off: f64 },
}
/// A rail's cost per mm off its bus layer (`Profile::layer_bias`): stubs go to the bus and stop.
pub const OFF_BUS: f64 = 3.0;
const RAIL_WEIGHT: f64 = 2.0;
#[derive(Clone, Debug, Default)]
pub struct Path {
/// Points with their layer; a layer change between neighbours is a via.
pub pts: Vec<(P, usize)>,
pub vias: u32,
pub cost: f64,
/// Other routes' connection ids this path crosses (only in a soft search).
pub blockers: Vec<u32>,
pub nodes: usize,
}
pub struct Query<'a> {
pub net: NetId,
/// Nets treated as own copper (a pair searches for both its nets together).
pub also: Option<NetId>,
pub sources: &'a [Terminal],
pub targets: &'a [Terminal],
pub profile: Profile,
pub rules: &'a Rules,
/// May cross routed copper in this set, at a price (who is in the way?).
pub soft: Option<&'a dyn Fn(u32) -> f64>,
/// Via stand-off from these pads is waived (own exposed pad).
pub via_in: Option<usize>,
pub history: Option<&'a History>,
/// Circles where clearance isn't checked: a pair's centreline inside its own fan-in. The two
/// real tracks and their stubs are checked exactly afterwards.
pub free: &'a [(P, f64)],
/// The direction the route continues in after the target, so it doesn't arrive at an acute
/// angle (a stub joining a pair's coupled run).
pub arrive: Option<u8>,
}
/// How contested each part of the board has been: raised where nets fought, decayed each pass
/// (FastRoute's 0.9 per pass, CUGR's fade). Only contested places are raised, not whole routes.
pub struct History {
pub x0: f64,
pub y0: f64,
pub cell: f64,
pub w: usize,
pub h: usize,
pub v: Vec<f32>,
}
impl History {
pub fn new(bb: (f64, f64, f64, f64)) -> History {
let cell = 0.5 * NM;
let w = ((bb.2 - bb.0) / cell).ceil() as usize + 2;
let h = ((bb.3 - bb.1) / cell).ceil() as usize + 2;
History { x0: bb.0, y0: bb.1, cell, w, h, v: vec![0.0; w * h] }
}
#[inline]
pub fn at(&self, p: V) -> f32 {
let i = ((p.0 - self.x0) / self.cell).max(0.0) as usize;
let j = ((p.1 - self.y0) / self.cell).max(0.0) as usize;
if i < self.w && j < self.h { self.v[j * self.w + i] } else { 0.0 }
}
pub fn bump(&mut self, p: V) {
let i = ((p.0 - self.x0) / self.cell).max(0.0) as usize;
let j = ((p.1 - self.y0) / self.cell).max(0.0) as usize;
if i < self.w && j < self.h {
self.v[j * self.w + i] += 1.0;
}
}
pub fn decay(&mut self) {
for x in &mut self.v {
*x *= 0.9;
}
}
}
impl World {
/// Whether this copper is a pour this net may cross.
pub fn crosses_pour(&self, o: &Obs, net: NetId) -> bool {
match o.owner {
Owner::Zone(z) => self.pour_open.get(z as usize).map(|v| v.binary_search(&net).is_ok()).unwrap_or(false),
_ => false,
}
}
pub fn in_escape(&self, p: V) -> bool {
self.escapes.iter().any(|b| p.0 >= b.0 && p.0 <= b.2 && p.1 >= b.1 && p.1 <= b.3)
}
/// Smallest edge distance from a centreline to copper of other nets on a layer, stopping once
/// it is certainly below `floor` (then a blocker that is not soft-crossable has been met).
#[inline]
#[allow(clippy::too_many_arguments)]
fn gap(&self, rules: &Rules, base: i64, layer: usize, a: V, b: V, net: NetId, also: NetId, reach: f64, floor: f64, soft: Option<&dyn Fn(u32) -> f64>, blockers: &mut Vec<u32>, soft_cost: &mut f64) -> f64 {
// Clearance is per pair of nets, as KiCad judges it: a 0.5 mm class pushes only what's beside
// it. The returned gap is normalised to `base`, so callers compare it against one number.
let reach = reach + (rules.max_clearance() - base).max(0) as f64 + self.max_pad_clearance();
let bb = (a.0.min(b.0) - reach, a.1.min(b.1) - reach, a.0.max(b.0) + reach, a.1.max(b.1) + reach);
let mut best = f64::INFINITY;
self.layers[layer].visit(bb, |o| {
if o.net == net && net != NO_NET {
return true;
}
if o.net == also && also != NO_NET {
return true;
}
if self.crosses_pour(o, net) {
return true;
}
let local = if let Owner::Pad(i) = o.owner { self.pad_clearance[i as usize] } else { 0.0 };
let pad = if let Owner::Pad(i) = o.owner { Some(i) } else { None };
let d = o.dist_within(a, b, reach) - ((rules.clearance_at(net, o.net, pad) as f64).max(local) - base as f64);
if d < floor {
if let (Some(f), Owner::Conn(c)) = (soft, o.owner) {
let price = f(c);
if price.is_finite() {
if !blockers.contains(&c) {
blockers.push(c);
*soft_cost += price;
}
return true;
}
}
best = d;
return false;
}
best = best.min(d);
true
});
best
}
/// Edge distance from a centreline to the nearest copper of another net within `reach`, less
/// any extra clearance that net pair needs over the board's: the room a track has.
pub fn room(&self, rules: &Rules, layer: usize, a: V, b: V, net: NetId, reach: f64) -> f64 {
let (mut blockers, mut cost) = (Vec::new(), 0.0);
self.gap(rules, rules.clearance, layer, a, b, net, NO_NET, reach, f64::NEG_INFINITY, None, &mut blockers, &mut cost).min(reach)
}
#[inline]
pub fn max_pad_clearance(&self) -> f64 {
self.max_local
}
/// Whether a track stays clear of every through hole of another net, plated or not: every
/// layer sees every hole (KiCad's hole_clearance).
pub fn holes_clear(&self, rules: &Rules, a: V, b: V, half: f64, net: NetId) -> bool {
self.holes_clear_also(rules, a, b, half, net, NO_NET)
}
/// `holes_clear` for copper shared by a pair: the partner's holes are not another net's.
pub fn holes_clear_also(&self, rules: &Rules, a: V, b: V, half: f64, net: NetId, also: NetId) -> bool {
let need = half + rules.hole_clearance() as f64;
let far = need + self.max_pad_clearance();
let bb = (a.0.min(b.0) - far, a.1.min(b.1) - far, a.0.max(b.0) + far, a.1.max(b.1) + far);
let mut ok = true;
self.holes.visit(bb, |o| {
if (o.net == net && net != NO_NET) || (o.net == also && also != NO_NET) || matches!(o.shape, Shape::Poly { .. }) {
return true;
}
let local = if let Owner::Pad(i) = o.owner { self.pad_clearance[i as usize] + half } else { 0.0 };
if o.shape.dist_seg(a, b) < need.max(local) {
ok = false;
return false;
}
true
});
ok
}
fn edge_gap(&self, a: V, b: V, reach: f64) -> f64 {
let bb = (a.0.min(b.0) - reach, a.1.min(b.1) - reach, a.0.max(b.0) + reach, a.1.max(b.1) + reach);
let mut best = f64::INFINITY;
self.edge.visit(bb, |o| {
best = best.min(o.shape.dist_seg(a, b));
true
});
best
}
/// Whether a through via can stand here: copper clear on every layer, holes apart, on the
/// board, and off every pad (via-in-pad needs a decision, 4.4).
pub fn via_ok(&self, p: P, q: &Query, soft: Option<&dyn Fn(u32) -> f64>, blockers: &mut Vec<u32>, soft_cost: &mut f64) -> bool {
let r = q.rules;
let c: V = p.into();
// The net class's via, not the board's: the size 2.2 says the class may set.
let (via_dia, via_drill) = r.via_for(q.net);
let vr = via_dia as f64 / 2.0;
let also = q.also.unwrap_or(NO_NET);
let base = r.clearance_for(q.net, q.net);
for l in 0..self.layers.len() {
// A micron over: a pad corner here is a polygon inside KiCad's arc (benchmark C: 0.1994 against 0.2).
let need = vr + base as f64 + 1000.0;
let d = self.gap(r, base, l, c, c, q.net, also, need, need, soft, blockers, soft_cost);
if d < need {
return false;
}
}
// Own-net pads: the via sits just clear of the pad, not in it.
let own = vr + r.pad_via_gap as f64;
let bb = (c.0 - own, c.1 - own, c.0 + own, c.1 + own);
let mut clear = true;
for l in [0, self.layers.len() - 1] {
self.layers[l].visit(bb, |o| {
if let Owner::Pad(i) = o.owner {
if (o.net == q.net || (also != NO_NET && o.net == also)) && Some(i as usize) != q.via_in && o.shape.dist_pt(c) < own {
clear = false;
return false;
}
}
true
});
}
if !clear {
return false;
}
if self.mask.via_hit(q.net, c, vr).is_some() || !self.holes_clear(r, c, c, vr, q.net) {
return false;
}
let hole = via_drill as f64 / 2.0 + r.hole_to_hole() as f64;
let bb = (c.0 - hole, c.1 - hole, c.0 + hole, c.1 + hole);
self.holes.visit(bb, |o| {
if o.shape.dist_pt(c) < hole {
clear = false;
return false;
}
true
});
if !clear {
return false;
}
let e = vr + r.edge as f64;
self.edge_gap(c, c, e) >= e
}
}
fn octile(a: P, b: P) -> f64 {
let (dx, dy) = ((a.0 - b.0).abs() as f64, (a.1 - b.1).abs() as f64);
let (hi, lo) = if dx > dy { (dx, dy) } else { (dy, dx) };
(hi - lo) + lo * SQRT2
}
fn near_poly(p: P, poly: &[V]) -> P {
let c: V = p.into();
if in_poly(c, poly) {
return p;
}
let mut best = (f64::INFINITY, c);
let n = poly.len();
for i in 0..n {
let (a, b) = (poly[i], poly[(i + 1) % n]);
let ab = sub(b, a);
let l2 = dot(ab, ab).max(1.0);
let t = (dot(sub(c, a), ab) / l2).clamp(0.0, 1.0);
let q = V(a.0 + ab.0 * t, a.1 + ab.1 * t);
let d = hypot(sub(c, q));
if d < best.0 {
best = (d, q);
}
}
to_p(best.1)
}
struct Node {
p: P,
layer: u8,
dir: u8,
vias: u8,
parent: u32,
g: f64,
/// Not yet checked against copper. Moves are validated only when the search picks them
/// (lazy A*), which is most of the speed: most candidate moves are never looked at.
pending: Pending,
}
#[derive(Clone, Copy, PartialEq)]
enum Pending {
None,
Move { from: P },
Via,
}
thread_local! {
/// Whether the last failed search stopped on its budget rather than exhausting its window:
/// the difference between "ran out" and "walled in" when a task goes to the designer.
pub static RAN_OUT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[inline]
fn soft_ignores(q: &Query, o: &Obs) -> bool {
match (q.soft, o.owner) {
(Some(f), Owner::Conn(c)) => f(c).is_finite(),
_ => false,
}
}
/// Build the guide lines for a search window.
pub fn build_lines(w: &World, q: &Query, window: (f64, f64, f64, f64)) -> Lines {
build_lines_on(w, q, window, &w.route_layers)
}
/// The same, from the copper on the given layers only. A rail's bus layer is mostly empty: it has no
/// use for a line along every pad on the other side of the board, and each one multiplies the states.
pub fn build_lines_on(w: &World, q: &Query, window: (f64, f64, f64, f64), layers: &[usize]) -> Lines {
let mut lines = Lines::default();
let r = q.rules;
let p = q.profile;
let also = q.also.unwrap_or(NO_NET);
let mut terminal = |t: &Terminal, lines: &mut Lines| match t {
Terminal::Point { p, .. } => lines.add_point(*p),
Terminal::Zone { poly, .. } => {
let b = Shape::Poly { pts: poly.clone(), r: 0.0 }.bbox();
lines.add_point(((b.0 + b.2) as i64 / 2, (b.1 + b.3) as i64 / 2));
}
Terminal::Track { a, b, .. } => {
add_track_lines(lines, *a, *b);
lines.add_point(*a);
lines.add_point(*b);
}
};
for t in q.sources.iter().chain(q.targets.iter()) {
terminal(t, &mut lines);
}
let wide = p.width as f64 / 2.0;
let neck = p.neck as f64 / 2.0;
let tgt = p.target_gap as f64 + 1000.0;
for &l in layers {
w.layers[l].visit(window, |o| {
let own = o.net == q.net || (also != NO_NET && o.net == also);
// A soft search crosses movable copper, so it has no use for lines around it, and
// those lines are most of the lattice on a dense board (see Router::soft_path).
if soft_ignores(q, o) {
return true;
}
if !own {
let at_pad = if let Owner::Pad(i) = o.owner { Some(i) } else { None };
let clr = r.clearance_at(q.net, o.net, at_pad);
let c = clr as f64 + 1000.0;
let via_r = r.via_for(q.net).0 as f64 / 2.0 + clr as f64 + 1000.0;
// Hug at the neck's clearance; at the run's own width if that is meaningfully wider.
lines.add_tangents(&o.shape, neck + c);
if wide > neck + 40_000.0 {
lines.add_tangents(&o.shape, wide + c);
}
// The preferred spacing, along routed tracks only: that is what makes a bus.
if tgt > c && matches!(o.owner, Owner::Conn(_) | Owner::Fixed) {
lines.add_tangents(&o.shape, wide + tgt);
}
if q.profile.max_vias > 0 && !matches!(o.owner, Owner::Keepout | Owner::Zone(_)) {
let (b, _) = (o.bb, 0);
lines.v.push(even_up(b.2 + via_r));
lines.v.push(even_down(b.0 - via_r));
lines.h.push(even_up(b.3 + via_r));
lines.h.push(even_down(b.1 - via_r));
}
}
if let Owner::Pad(i) = o.owner {
// Hug a pad's solder mask opening too, where it reaches past the clearance.
let limit = w.mask.limit_of(i as usize);
if !own && limit > r.clearance_for(q.net, o.net) as f64 {
lines.add_tangents(&o.shape, neck + limit + 1000.0);
// And a second set clear of the laying guard: the first serves runs along a
// straight side, this one a diagonal past a rounded corner (mask.rs `curved_near`).
if w.mask.guard() > 0.0 {
lines.add_tangents(&o.shape, neck + limit + w.mask.guard() + 1000.0);
}
}
let b = o.bb;
let cx = ((b.0 + b.2) / 2.0) as i64;
let cy = ((b.1 + b.3) / 2.0) as i64;
// A neighbouring pin's own line: parallel runs at pin pitch (4.7).
lines.v.push(cx & !1);
lines.h.push(cy & !1);
}
true
});
}
w.edge.visit(window, |o| {
lines.add_tangents(&o.shape, wide + r.edge as f64 + 1000.0);
true
});
w.holes.visit(window, |o| {
if o.net != q.net && !matches!(o.shape, Shape::Poly { .. }) {
let local = if let Owner::Pad(i) = o.owner { w.pad_clearance[i as usize] } else { 0.0 };
lines.add_tangents(&o.shape, neck + local.max(r.hole_clearance() as f64) + 1000.0);
}
true
});
lines.finish();
lines
}
/// The line a track lies on, so a search stops exactly where it crosses that track.
fn add_track_lines(lines: &mut Lines, a: P, b: P) {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
if dy == 0 {
lines.h.push(a.1);
} else if dx == 0 {
lines.v.push(a.0);
} else if dx == dy {
lines.dp.push(a.0 - a.1);
} else if dx == -dy {
lines.dm.push(a.0 + a.1);
}
}
/// Whether a route arriving at `p` on `layer` heading `dir` may end on this track. A square T only
/// off a wide rail, never parallel, and any via at the required distance from the track.
#[allow(clippy::too_many_arguments)]
fn track_accepts(t: &Terminal, p: P, layer: usize, dir: u8, last_via: Option<P>) -> bool {
let Terminal::Track { a, b, layer: tl, wide, stand_off } = t else { return false };
if *tl != layer || pt_seg(p.into(), (*a).into(), (*b).into()) > 2.0 {
return false;
}
if let Some(v) = last_via {
if pt_seg(v.into(), (*a).into(), (*b).into()) < *stand_off {
return false;
}
}
// Straight off a via onto the track: a via inside a trace (4.4), only as a marked last resort.
if dir == NO_DIR {
return *stand_off < 0.0;
}
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let Some(axis) = DIRS.iter().position(|d| d.0 == dx.signum() && d.1 == dy.signum() && (dx.abs() == dy.abs() || dx == 0 || dy == 0)) else { return true };
// Landing on the track's very end is a corner, not a junction: it must not fold back acutely.
for (end, onward) in [(*a, axis as i32), (*b, axis as i32 + 4)] {
if octile(p, end) <= 2.0 {
let t = (onward - dir as i32).rem_euclid(8);
if t.min(8 - t) > if *wide { 2 } else { 1 } {
return false;
}
}
}
let turn = (axis as i32 - dir as i32).rem_euclid(4);
let turn = turn.min(4 - turn);
match turn {
0 => false,
1 => true,
_ => *wide,
}
}
/// A* over the lattice from any source to any target. Returns None when no legal path exists in
/// the window within the node budget.
pub fn search(w: &World, q: &Query) -> Option<Path> {
RAN_OUT.with(|c| c.set(false));
crate::clock::tick(crate::clock::SEARCH_START);
let t0 = std::time::Instant::now();
let mut nodes = 0usize;
let found = search_inner(w, q, &mut nodes);
{
let mut g = w.guide.borrow_mut();
g.searches += 1;
if found.is_some() {
g.nodes_ok += nodes as u64;
} else {
g.nodes_failed += nodes as u64;
g.note_failed(q);
}
}
if trace() {
eprintln!(" search vias={} margin={:.1} w={:.2} gap={:.2} soft={} -> {} nodes={} {:.1} ms", q.profile.max_vias, q.profile.margin / NM, q.profile.width as f64 / NM, q.profile.target_gap as f64 / NM, q.soft.is_some(), found.is_some(), nodes, t0.elapsed().as_secs_f64() * 1000.0);
}
found
}
fn search_inner(w: &World, q: &Query, node_count: &mut usize) -> Option<Path> {
let r = q.rules;
let pf = q.profile;
// Window: the bounding box of the terminals plus a margin.
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for t in q.sources.iter().chain(q.targets.iter()) {
let b = match t {
Terminal::Point { p, .. } => (p.0 as f64, p.1 as f64, p.0 as f64, p.1 as f64),
Terminal::Zone { poly, .. } => Shape::Poly { pts: poly.clone(), r: 0.0 }.bbox(),
Terminal::Track { a, b, .. } => ((a.0.min(b.0)) as f64, (a.1.min(b.1)) as f64, (a.0.max(b.0)) as f64, (a.1.max(b.1)) as f64),
};
x0 = x0.min(b.0);
y0 = y0.min(b.1);
x1 = x1.max(b.2);
y1 = y1.max(b.3);
}
let mut window = (x0 - pf.margin, y0 - pf.margin, x1 + pf.margin, y1 + pf.margin);
// A planned detour may leave the terminals' box: the first attempt can already follow it.
if q.soft.is_none() {
if let Some(cr) = w.guide.borrow().corridor_of(q) {
let b = cr.bbox;
window = (window.0.min(b.0), window.1.min(b.1), window.2.max(b.2), window.3.max(b.3));
}
}
let table = w.guide.borrow_mut().prepare(w, q, window);
let audit = audit_guide();
let walled = table.is_some() && w.guide.borrow_mut().hopeless(w, q);
if walled && !audit {
return None;
}
let started = *w.guide.borrow_mut().start.get_or_insert_with(crate::clock::Instant::now);
let hint = if q.soft.is_none() { w.guide.borrow().hint.as_ref().filter(|h| h.net == q.net).cloned() } else { None };
let mut lines = build_lines(w, q, window);
if let Some(h) = &hint {
for (a, b, _) in &h.lanes {
lines.add_point(to_p(*a));
lines.add_point(to_p(*b));
}
lines.finish();
}
let bus_lines = pf.layer_bias.map(|l| build_lines_on(w, q, window, &[l]));
let also = q.also.unwrap_or(NO_NET);
// The targets' own lines: a route may turn onto one of these anywhere.
let mut term = Lines::default();
for t in q.targets {
match t {
Terminal::Point { p, .. } => term.add_point(*p),
Terminal::Zone { poly, .. } => {
let b = Shape::Poly { pts: poly.clone(), r: 0.0 }.bbox();
term.add_point(((b.0 + b.2) as i64 / 2, (b.1 + b.3) as i64 / 2));
}
// A route turns toward a track where it meets a line through the track's ends: empty
// bus layers have no copper to bend at otherwise.
Terminal::Track { a, b, .. } => {
term.add_point(*a);
term.add_point(*b);
}
}
}
// A bus member may also turn onto its neighbour's shape anywhere.
if let Some(h) = &hint {
for (a, b, _) in &h.lanes {
term.add_point(to_p(*a));
term.add_point(to_p(*b));
}
}
term.finish();
// A shortest route only bends where it wraps something: next to copper, or onto a target's
// line. Everywhere else it goes straight. This is what keeps the lattice small.
let bend_reach = ((pf.width.max(pf.neck) as f64 / 2.0 + pf.target_gap.max(pf.clearance) as f64) * 1.2 + 40_000.0).max(r.via as f64 / 2.0 + r.clearance as f64 + 60_000.0);
// The hugging lines lie at each neighbour's own class clearance, so the turn must be allowed out
// there: at a USB class's 0.4 mm they lay outside this reach, every legal bend read as open
// copper, and a net of a wide class had nowhere to turn (benchmark B's pair, "no room").
let bend_half = pf.width.max(pf.neck) as f64 / 2.0;
let bend_far = bend_reach.max(bend_half + r.max_clearance() as f64 * 1.2 + 40_000.0);
let near_copper = |p: P, layer: usize| -> bool {
let c: V = p.into();
let bb = (c.0 - bend_far, c.1 - bend_far, c.0 + bend_far, c.1 + bend_far);
let mut hit = false;
w.layers[layer].visit(bb, |o| {
if (o.net == q.net && q.net != NO_NET) || (also != NO_NET && o.net == also) || soft_ignores(q, o) {
return true;
}
let reach = bend_reach.max(bend_half + r.clearance_for(q.net, o.net) as f64 * 1.2 + 40_000.0);
if o.dist_within(c, c, reach * 1.01) <= reach {
hit = true;
return false;
}
true
});
if !hit {
w.edge.visit(bb, |o| {
if o.dist_within(c, c, bend_reach * 1.01) <= bend_reach {
hit = true;
return false;
}
true
});
}
hit
};
// Target positions for the heuristic and the arrival test.
let mut point_targets: HashSet<(P, usize)> = HashSet::new();
let mut h_points: Vec<(P, Vec<usize>)> = Vec::new();
let mut zone_targets: Vec<(usize, &Vec<V>)> = Vec::new();
let mut track_targets: Vec<(P, P, usize)> = Vec::new();
for t in q.targets {
match t {
Terminal::Point { p, layers } => {
let s = snap(*p);
for &l in layers {
point_targets.insert((s, l));
}
h_points.push((s, layers.clone()));
}
Terminal::Zone { layer, poly } => zone_targets.push((*layer, poly)),
Terminal::Track { a, b, layer, .. } => track_targets.push((*a, *b, *layer)),
}
}
// Remaining cost of covering `d` to a target that is on `on_layer(l)`. For a rail, staying off the
// bus costs OFF_BUS per mm, so the bound has to know a via is usually the cheap way: without this
// the search crawls the crowded stub layer for millimetres before it tries going down.
let rest = |d: f64, layer: usize, on_layer: &dyn Fn(usize) -> bool| -> f64 {
match pf.layer_bias {
None => d + if on_layer(layer) { 0.0 } else { r.cost_via },
Some(bus) => {
let via_end = if on_layer(bus) { 0.0 } else { r.cost_via };
if layer == bus {
d + via_end
} else {
let down = r.cost_via + d + via_end;
if on_layer(layer) { down.min(OFF_BUS * d) } else { down }
}
}
}
};
// A rail's targets are every piece of its copper, pours of hundreds of points included, and working
// that out per state was most of a rail search's time on benchmark C. With many targets the estimate
// is kept per 0.1 mm square, less the most it can change across a square, so it's still a bound.
let many = h_points.len() + track_targets.len() + zone_targets.len() * 8 > 8;
let near_cache: std::cell::RefCell<HashMap<(i64, i64, u8), f64, FxBuild>> = Default::default();
const CACHE_CELL: i64 = 100_000;
let target_bound = |p: P, layer: usize| -> f64 {
let mut best = f64::INFINITY;
for (t, ls) in &h_points {
best = best.min(rest(octile(p, *t), layer, &|l| ls.contains(&l)));
}
for (l, poly) in &zone_targets {
best = best.min(rest(octile(p, near_poly(p, poly)), layer, &|x| x == *l));
}
for (a, b, l) in &track_targets {
// Straight-line distance never exceeds the octile path, so this stays a lower bound.
best = best.min(rest(pt_seg(p.into(), (*a).into(), (*b).into()), layer, &|x| x == *l));
}
best
};
let h = |p: P, layer: usize| -> f64 {
let mut best = if many {
let key = (p.0.div_euclid(CACHE_CELL), p.1.div_euclid(CACHE_CELL), layer as u8);
let mut cache = near_cache.borrow_mut();
*cache.entry(key).or_insert_with(|| {
let c = (key.0 * CACHE_CELL + CACHE_CELL / 2, key.1 * CACHE_CELL + CACHE_CELL / 2);
// OFF_BUS can steepen the bound by its factor, so the square's slack is scaled by it.
(target_bound(c, layer) - OFF_BUS.max(1.0) * SQRT2 * CACHE_CELL as f64).max(0.0)
})
} else {
target_bound(p, layer)
};
// Raised to the coarse map's true way round obstacles. Infinity is the map's proof that this
// place can't reach the target.
if let Some(tb) = table {
let g = w.guide.borrow_mut().cost(tb, w, r, p.into(), layer);
if g.is_finite() {
best = best.max(g);
} else if !audit {
return g;
}
}
best
};
// The pair partner's tracks, if this net has one on the board (STACK_COST).
let stack: Vec<(usize, V, V)> = w.partner.get(&q.net).and_then(|p| w.pair_segs.borrow().get(p).map(|v| v.iter().map(|(_, l, a, b)| (*l, *a, *b)).collect())).unwrap_or_default();
let mut nodes: Vec<Node> = Vec::new();
let mut best: HashMap<(P, u8, u8), f64, FxBuild> = HashMap::with_capacity_and_hasher(4096, FxBuild::default());
let mut heap: BinaryHeap<Reverse<(i64, u32)>> = BinaryHeap::new();
// A rail is pulled tight afterwards and its bus layer is mostly empty, so its search may run
// greedier: the answer barely changes and the node count drops by an order of magnitude.
let weight = if pf.layer_bias.is_some() { RAIL_WEIGHT } else { r.search_weight };
let mut push = |nodes: &mut Vec<Node>, best: &mut HashMap<(P, u8, u8), f64, FxBuild>, heap: &mut BinaryHeap<Reverse<(i64, u32)>>, n: Node| {
let key = (n.p, n.layer, n.dir);
let est = h(n.p, n.layer as usize);
if est.is_infinite() {
return;
}
if let Some(&g) = best.get(&key) {
if g <= n.g {
return;
}
}
best.insert(key, n.g);
let f = n.g + weight * est;
nodes.push(n);
heap.push(Reverse((f as i64, (nodes.len() - 1) as u32)));
};
for s in q.sources {
match s {
Terminal::Point { p, layers } => {
for &l in layers {
if w.route_layers.contains(&l) {
push(&mut nodes, &mut best, &mut heap, Node { p: snap(*p), layer: l as u8, dir: NO_DIR, vias: 0, parent: u32::MAX, g: 0.0, pending: Pending::None });
}
}
}
Terminal::Zone { layer, poly } => {
let b = Shape::Poly { pts: poly.to_vec(), r: 0.0 }.bbox();
let c = snap(((b.0 + b.2) as i64 / 2, (b.1 + b.3) as i64 / 2));
if w.route_layers.contains(layer) {
push(&mut nodes, &mut best, &mut heap, Node { p: c, layer: *layer as u8, dir: NO_DIR, vias: 0, parent: u32::MAX, g: 0.0, pending: Pending::None });
}
}
// Routes leave a pad or a fill; a track is only ever something to join.
Terminal::Track { .. } => {}
}
}
// Every source closed off on the coarse map: a proof that no route exists in this window.
if table.is_some() && heap.is_empty() {
w.guide.borrow_mut().closed_early += 1;
return None;
}
let source_points: Vec<P> = q.sources.iter().filter_map(|s| if let Terminal::Point { p, .. } = s { Some(snap(*p)) } else { None }).collect();
let target_points: Vec<P> = h_points.iter().map(|x| x.0).collect();
// How far from a terminal a track may run at its neck. A wide rail's body can't come as close to
// a small pad as a signal's can: it necks from where its own edge would meet the pad's neighbours.
let rated = w.rail_nets.contains(&q.net) && pf.width > pf.neck;
let neck_reach = if rated { (pf.width as f64 / 2.0 + pf.clearance as f64 + 0.3 * NM).max(0.4 * NM) } else { 0.4 * NM };
let near_term = |x: P| source_points.iter().chain(target_points.iter()).any(|&s| octile(x, s) <= neck_reach);
let mut expanded = 0usize;
while let Some(Reverse((_, id))) = heap.pop() {
let (p, layer, dir, vias, g, pending) = {
let n = &nodes[id as usize];
(n.p, n.layer as usize, n.dir, n.vias, n.g, n.pending)
};
let key = (p, layer as u8, dir);
if best.get(&key).map(|&b| g > b).unwrap_or(false) {
continue;
}
// Lazy check: is the move that reached this state legal, and what does it really cost?
match pending {
Pending::Move { from } => {
expanded += 1;
crate::clock::tick(1);
*node_count = expanded;
// The whole-run limit holds inside a long search too (4.6).
if expanded & 4095 == 0 && started.elapsed().as_secs_f64() > r.time_limit {
// Cut short, not closed: nothing was proved about the way.
RAN_OUT.with(|c| c.set(true));
return None;
}
if expanded > pf_limit(q) {
RAN_OUT.with(|c| c.set(true));
return None;
}
if expanded % 1024 == 0 && r.deadline.map(|d| crate::clock::Instant::now() > d).unwrap_or(false) {
RAN_OUT.with(|c| c.set(true));
return None;
}
let (a, b): (V, V) = (from.into(), p.into());
// On its bus layer a rail keeps its width under a fine-pitch part: the escape is on
// the other side. Off the bus layer it is a stub.
let on_bus = pf.layer_bias == Some(layer);
let in_esc = if pf.layer_bias.is_some() && !rated { !on_bus } else { (w.in_escape(a) && w.in_escape(b)) || near_term(from) || near_term(p) };
let half = if in_esc { pf.neck } else { pf.width } as f64 / 2.0;
let hard = half + pf.clearance as f64 - DRC_TOL;
let soft_reach = half + pf.target_gap as f64;
let reach = hard.max(soft_reach);
let is_free = q.free.iter().any(|(c, rad)| octile(from, *c) <= *rad && octile(p, *c) <= *rad);
let mut blockers = Vec::new();
let mut soft_cost = 0.0;
let gap = if is_free { f64::INFINITY } else { w.gap(r, pf.clearance, layer, a, b, q.net, also, reach, hard, q.soft, &mut blockers, &mut soft_cost) };
if gap < hard {
best.remove(&key);
continue;
}
let e = half + r.edge as f64;
if !is_free && (w.edge_gap(a, b, e) < e || w.mask.seg_hit_also(q.net, also, layer, a, b, half).is_some() || !w.holes_clear_also(r, a, b, half, q.net, also)) {
best.remove(&key);
continue;
}
let len = hypot(sub(a, b));
// A crossing costs its price once however finely the lattice cuts it: the band of a
// track and its clearance is about 0.5 mm wide, and lattice steps are far smaller.
let mut extra = soft_cost * (len / (0.5 * NM)).min(1.0);
if !in_esc && gap < soft_reach {
extra += len * r.cost_tight * (1.0 - (gap - hard) / (soft_reach - hard).max(1.0));
}
// His J1: CC2 stays on B.Cu past the VBUS pour's edge, then hops to F.Cu.
if let Some(polys) = w.pour_avoid.get(&q.net) {
let m = V((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0);
if polys.iter().any(|(l, p)| *l == layer && in_poly(m, p)) {
extra += len * POUR_AVOID_COST;
}
}
if let Some(hist) = q.history {
let m = V((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0);
extra += hist.at(m) as f64 * len * 0.5;
}
if w.relief_reach > 0.0 {
let reach = w.relief_reach + half;
let bb = (a.0.min(b.0) - reach, a.1.min(b.1) - reach, a.0.max(b.0) + reach, a.1.max(b.1) + reach);
let mut hit = false;
w.relief[layer].visit(bb, |o| {
if o.net != q.net {
if let Owner::Pad(i) = o.owner {
if o.shape.dist_seg(a, b) - half < w.relief_gap[i as usize] {
hit = true;
return false;
}
}
}
true
});
if hit {
extra += len * RELIEF_COST;
}
}
if !stack.is_empty() {
let m = V((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0);
if stack.iter().any(|(l, s0, s1)| *l != layer && pt_seg(m, *s0, *s1) < STACK_BAND) {
extra += len * STACK_COST;
}
}
if let Some((fl, segs, pitch)) = w.follow.borrow().as_ref() {
if *fl == layer && !segs.is_empty() {
let m = V((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0);
let d = segs.iter().map(|(s0, s1)| pt_seg(m, *s0, *s1)).fold(f64::INFINITY, f64::min);
extra += len * FOLLOW_COST * ((d - pitch) / pitch).clamp(0.0, 1.0);
}
}
nodes[id as usize].pending = Pending::None;
if extra > 0.0 {
let ng = g + extra;
nodes[id as usize].g = ng;
best.insert(key, ng);
let f = ng + weight * h(p, layer);
heap.push(Reverse((f as i64, id)));
continue;
}
}
Pending::Via => {
expanded += 1;
crate::clock::tick(1);
// Two vias of one path are holes too (2.2), and neither is on the board yet to see.
let mut up = nodes[id as usize].parent;
let min_hole = r.drill as f64 + r.hole_hole as f64;
let mut close = false;
while up != u32::MAX && nodes[id as usize].vias > 1 {
let n = &nodes[up as usize];
if n.pending == Pending::None && n.dir == NO_DIR && n.parent != u32::MAX && n.p != p && hypot(sub(n.p.into(), p.into())) < min_hole {
close = true;
break;
}
up = n.parent;
}
if close {
best.remove(&key);
continue;
}
let mut blockers = Vec::new();
let mut soft_cost = 0.0;
if !w.via_ok(p, q, q.soft, &mut blockers, &mut soft_cost) {
best.remove(&key);
continue;
}
nodes[id as usize].pending = Pending::None;
if soft_cost > 0.0 {
let ng = g + soft_cost;
nodes[id as usize].g = ng;
best.insert(key, ng);
let f = ng + weight * h(p, layer);
heap.push(Reverse((f as i64, id)));
continue;
}
}
Pending::None => {}
}
let mut arrived = point_targets.contains(&(p, layer)) || zone_targets.iter().any(|(l, poly)| *l == layer && in_poly(p.into(), poly));
if !arrived && !track_targets.is_empty() {
let last_via = last_via_of(&nodes, id);
arrived = q.targets.iter().any(|t| track_accepts(t, p, layer, dir, last_via));
}
let arrive_ok = match (q.arrive, dir) {
(Some(next), d) if d != NO_DIR => ((next as i32 - d as i32).rem_euclid(8)).min((d as i32 - next as i32).rem_euclid(8)) <= 1,
_ => true,
};
if arrived && arrive_ok {
if let (true, Some(tb)) = (audit, table) {
audit_path(&nodes, id, w, q, tb, walled);
}
return Some(rebuild(&nodes, id, q, w, expanded));
}
// Straight on and 45-degree turns; 90 degrees only at a price (chamfered afterwards).
// Asked at most once per node, and only if a turn is on a line: near_copper took about 10%.
let mut may_bend = if dir == NO_DIR { Some(true) } else { None };
let lines = match (&bus_lines, pf.layer_bias) {
(Some(b), Some(l)) if l == layer => b,
_ => &lines,
};
for d in 0..8u8 {
let turn = if dir == NO_DIR { 0 } else { ((d as i32 - dir as i32).rem_euclid(8)).min((dir as i32 - d as i32).rem_euclid(8)) };
if turn >= 3 {
continue;
}
// Straight off a via onto a bus layer's sparser lines, from wherever the via stands.
if dir != NO_DIR && !lines.on_family(p, d) {
continue;
}
if dir == NO_DIR && pf.layer_bias.is_none() && !lines.on_family(p, d) {
continue;
}
if turn != 0 && !term.on_family(p, d) && !*may_bend.get_or_insert_with(|| near_copper(p, layer)) {
continue;
}
let Some(mut t) = lines.next_stop(p, d) else { continue };
let (dx, dy) = DIRS[d as usize];
let mut np = (p.0 + dx * t, p.1 + dy * t);
// In open copper a stop can only go straight on, so skip it: on a dense board's lattice
// a straight run across an empty layer otherwise stops thousands of times per mm, each
// a node and a clearance check, and floods the budget (piprobe_v13: 362k states on
// an almost empty B.Cu, then "no path" 10 mm short). Stops where a bend or an arrival
// is possible are all kept, so no route shape is lost.
let open_stop = |x: P| !near_copper(x, layer) && !(0..8u8).any(|k| k != d && term.on_family(x, k)) && !point_targets.contains(&(x, layer)) && zone_targets.is_empty();
let mut hops = 0;
while hops < 512 && open_stop(np) {
let Some(more) = lines.next_stop(np, d) else { break };
let further = (np.0 + dx * more, np.1 + dy * more);
if (further.0 as f64) < window.0 || (further.0 as f64) > window.2 || (further.1 as f64) < window.1 || (further.1 as f64) > window.3 {
break;
}
t += more;
np = further;
hops += 1;
}
if (np.0 as f64) < window.0 || (np.0 as f64) > window.2 || (np.1 as f64) < window.1 || (np.1 as f64) > window.3 {
continue;
}
let len = t as f64 * if dx != 0 && dy != 0 { SQRT2 } else { 1.0 };
let per_mm = match pf.layer_bias {
Some(l) if l == layer => 1.0,
Some(_) => OFF_BUS,
None if layer == 0 => 1.0,
None => r.cost_bottom,
};
let mut cost = len * per_mm;
if let Some(h) = &hint {
cost += len * h.extra(p.into(), np.into(), layer);
}
cost += match turn {
0 => 0.0,
1 => r.cost_bend45,
_ => r.cost_bend90,
};
push(&mut nodes, &mut best, &mut heap, Node { p: np, layer: layer as u8, dir: d, vias, parent: id, g: g + cost, pending: Pending::Move { from: p } });
}
// Change layer, in rounds by via count.
// Most via candidates fail (9 in 10 on the benchmark boards), so the map's proof that none
// can stand in this cell saves each one a trip through the heap and a full check.
if (vias as u32) < pf.max_vias && (audit || table.map_or(true, |tb| w.guide.borrow_mut().via_possible(tb, w, r, p.into()))) {
for &l in &w.route_layers {
if l != layer {
push(&mut nodes, &mut best, &mut heap, Node { p, layer: l as u8, dir: NO_DIR, vias: vias + 1, parent: id, g: g + r.cost_via, pending: Pending::Via });
}
}
}
}
None
}
fn trace() -> bool {
static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*V.get_or_init(|| std::env::var_os("ADOM_ROUTE_TRACE").is_some())
}
/// Audit mode: the coarse map's proofs aren't acted on, and any finished route through a place the
/// map called closed is reported. This is how the map's optimism is measured.
fn audit_guide() -> bool {
static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*V.get_or_init(|| std::env::var_os("ADOM_ROUTE_AUDIT_GUIDE").is_some())
}
fn audit_path(nodes: &[Node], mut id: u32, w: &World, q: &Query, table: usize, walled: bool) {
if walled {
eprintln!("GUIDE VIOLATION net {}: routed although a terminal was called walled in", q.net);
}
loop {
let n = &nodes[id as usize];
let g = w.guide.borrow_mut().cost(table, w, q.rules, n.p.into(), n.layer as usize);
if g.is_infinite() {
eprintln!("GUIDE VIOLATION net {} vias {} at ({:.3},{:.3}) layer {}", q.net, q.profile.max_vias, n.p.0 as f64 / NM, n.p.1 as f64 / NM, n.layer);
}
if n.parent != u32::MAX && nodes[n.parent as usize].layer != n.layer && !w.guide.borrow_mut().via_possible(table, w, q.rules, n.p.into()) {
eprintln!("GUIDE VIOLATION net {}: via at ({:.3},{:.3}) in a cell the map called closed", q.net, n.p.0 as f64 / NM, n.p.1 as f64 / NM);
}
if n.parent == u32::MAX {
break;
}
id = n.parent;
}
}
/// Where this node's copper last came through a via, if it did on the way here.
fn last_via_of(nodes: &[Node], mut id: u32) -> Option<P> {
loop {
let n = &nodes[id as usize];
if n.parent == u32::MAX {
return None;
}
let parent = &nodes[n.parent as usize];
if parent.layer != n.layer {
return Some(n.p);
}
id = n.parent;
}
}
fn pf_limit(q: &Query) -> usize {
// Small windows get a small budget; the wide last-resort searches get the full one.
let m = q.profile.margin;
// A soft search that finds its way does so early: the longest that succeeded across the three
// benchmark boards took 182k nodes, while every wider one spent its full budget and failed.
if q.soft.is_some() {
return q.rules.node_limit / 4;
}
if m < 2.0 * NM { q.rules.node_limit / 16 } else if m < 6.0 * NM { q.rules.node_limit / 4 } else { q.rules.node_limit }
}
fn rebuild(nodes: &[Node], mut id: u32, q: &Query, w: &World, expanded: usize) -> Path {
let mut pts = Vec::new();
let cost = nodes[id as usize].g;
let vias = nodes[id as usize].vias as u32;
loop {
let n = &nodes[id as usize];
pts.push((n.p, n.layer as usize));
if n.parent == u32::MAX {
break;
}
id = n.parent;
}
pts.reverse();
let mut blockers = Vec::new();
if let Some(f) = q.soft {
// Find exactly who the finished path crosses.
let half = q.profile.neck as f64 / 2.0 + q.profile.clearance as f64;
let also = q.also.unwrap_or(NO_NET);
for s in pts.windows(2) {
let ((a, la), (b, lb)) = (s[0], s[1]);
let (va, vb): (V, V) = (a.into(), b.into());
if la == lb {
let mut c = 0.0;
w.gap(q.rules, q.profile.clearance, la, va, vb, q.net, also, half, half, Some(f), &mut blockers, &mut c);
} else {
let need = q.rules.via_for(q.net).0 as f64 / 2.0 + q.profile.clearance as f64;
for l in 0..w.layers.len() {
let mut c = 0.0;
w.gap(q.rules, q.profile.clearance, l, va, va, q.net, also, need, need, Some(f), &mut blockers, &mut c);
}
}
}
}
Path { pts, vias, cost, blockers, nodes: expanded }
}
#[cfg(test)]
mod tests {
use super::*;
fn rail(wide: bool, stand_off: f64) -> Terminal {
Terminal::Track { a: (0, 0), b: (1_000_000, 0), layer: 3, wide, stand_off }
}
#[test]
fn a_tap_joins_a_wide_rail_at_45_or_90_and_a_thin_one_only_as_a_y() {
let mid = (500_000, 0);
assert!(track_accepts(&rail(true, 0.0), mid, 3, 2, None));
assert!(track_accepts(&rail(true, 0.0), mid, 3, 1, None));
assert!(!track_accepts(&rail(false, 0.0), mid, 3, 2, None));
assert!(track_accepts(&rail(false, 0.0), mid, 3, 1, None));
// Never along the rail, never on the wrong layer.
assert!(!track_accepts(&rail(true, 0.0), mid, 3, 0, None));
assert!(!track_accepts(&rail(true, 0.0), mid, 0, 2, None));
}
#[test]
fn a_tap_keeps_its_via_off_the_rail_and_never_folds_back_at_its_end() {
let mid = (500_000, 0);
// A via on the rail, or one too close to it, is refused while a stand-off is asked for (4.4).
assert!(!track_accepts(&rail(true, 600_000.0), mid, 3, NO_DIR, Some(mid)));
assert!(!track_accepts(&rail(true, 600_000.0), mid, 3, 2, Some((500_000, -300_000))));
assert!(track_accepts(&rail(true, 600_000.0), mid, 3, 2, Some((500_000, -700_000))));
// Arriving heading -x at the rail's start would turn back along +x: an acute corner.
assert!(!track_accepts(&rail(true, 0.0), (0, 0), 3, 3, None));
assert!(track_accepts(&rail(true, 0.0), (0, 0), 3, 1, None));
}
}