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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
//! What the designer rejects on sight and DRC never sees: copper that doubles back on itself, runs beside
//! itself, changes width every few millimetres, vias a short net needs no vias for, routes far longer
//! than their pads are apart, fills nothing feeds, rails thinner than 4.2's floor. Each one is graded
//! on the reference boards (bench/grade.json); where a threshold is a number he never wrote, the comment says
//! what it was read off.
use crate::board::*;
use crate::check::{Finding, NetStat, RailStat, Score};
use crate::classify::*;
use crate::geom::*;
use crate::rules::Rules;
use std::collections::{BTreeMap, HashMap, HashSet};
/// Shorter than this a segment is a width step or a stray join, not a direction change.
const JOG: f64 = 20_000.0;
/// Centre to centre, and overlap along the run, that make two same-net tracks one doubled run.
const DOUBLED_D: f64 = 0.4 * NM;
const DOUBLED_RUN: f64 = 0.3 * NM;
/// A retrace shorter than this past the pad or via it starts in is a join, not a fold. His boards'
/// longest such join is under his pads; our DIO retrace is 1.23 mm.
const FOLD_RUN: f64 = 0.1 * NM;
fn key(p: P) -> P {
((p.0 + 1000).div_euclid(2000), (p.1 + 1000).div_euclid(2000))
}
fn at_mm(p: P) -> (f64, f64) {
(to_mm(p.0), to_mm(p.1))
}
fn len_of(t: &Track) -> f64 {
hypot(sub(t.a.into(), t.b.into()))
}
/// Endpoints per net and layer, with micro-jogs merged so a 4 micron step is one node.
pub struct Nodes {
link: HashMap<(NetId, usize, P), P>,
pub at: HashMap<(NetId, usize, P), Vec<usize>>,
}
impl Nodes {
pub fn new(board: &Board) -> Nodes {
let mut n = Nodes { link: HashMap::new(), at: HashMap::new() };
for t in &board.tracks {
if len_of(t) <= JOG {
let (x, y) = (n.rep(t.net, t.layer, t.a), n.rep(t.net, t.layer, t.b));
if x != y {
n.link.insert((t.net, t.layer, x), y);
}
}
}
for (i, t) in board.tracks.iter().enumerate() {
if len_of(t) > JOG {
let (x, y) = (n.rep(t.net, t.layer, t.a), n.rep(t.net, t.layer, t.b));
n.at.entry((t.net, t.layer, x)).or_default().push(i);
n.at.entry((t.net, t.layer, y)).or_default().push(i);
}
}
n
}
pub fn rep(&self, net: NetId, layer: usize, p: P) -> P {
let mut k = key(p);
while let Some(&n) = self.link.get(&(net, layer, k)) {
k = n;
}
k
}
/// A track's ends as seen from node `n`: (the end at n, the far end).
fn from(&self, t: &Track, n: P) -> (P, P) {
if self.rep(t.net, t.layer, t.a) == n { (t.a, t.b) } else { (t.b, t.a) }
}
}
fn angle(from: P, to: P) -> f64 {
((to.1 - from.1) as f64).atan2((to.0 - from.0) as f64).to_degrees()
}
fn diff(a: f64, b: f64) -> f64 {
let d = (a - b).rem_euclid(360.0);
d.min(360.0 - d)
}
fn own_pad_at(board: &Board, net: NetId, layer: usize, v: V) -> Option<usize> {
board.pads.iter().position(|q| q.net == net && q.layers.contains(&layer) && q.shape.dist_pt(v) <= 1.0)
}
/// How much of a to b lies outside the net's own pads and via lands: copper inside them can't be seen,
/// so a 0.3 mm overlap wholly inside U1.33 on his v1.3 is nothing, and a retrace out of a pad is.
fn visible(board: &Board, net: NetId, layer: usize, a: V, b: V) -> f64 {
let l = hypot(sub(b, a));
if l <= 0.0 {
return 0.0;
}
let steps = ((l / 20_000.0).ceil() as usize).max(1);
let mut out = 0.0;
for i in 0..steps {
let u = (i as f64 + 0.5) / steps as f64;
let p = V(a.0 + (b.0 - a.0) * u, a.1 + (b.1 - a.1) * u);
let hid = board.pads.iter().any(|q| q.net == net && q.layers.contains(&layer) && q.shape.dist_pt(p) <= 0.0)
|| board.vias.iter().any(|v| v.net == net && hypot(sub(v.at.into(), p)) <= v.size as f64 / 2.0);
if !hid {
out += l / steps as f64;
}
}
out
}
/// Fold-back (a track retracing its neighbour from a shared node) and doubled runs (two same-net
/// tracks side by side). 4.2: "route as straight as possible, with the fewest segments". A pad or a
/// via hides a corner, not a retrace: DIO at R15.2 on the routed PiProbe goes to the pad's centre and
/// comes back along itself.
pub fn shape_rules(board: &Board, nodes: &Nodes, f: &mut Vec<Finding>) {
for ((net, layer, n), list) in &nodes.at {
for (x, &i) in list.iter().enumerate() {
for &j in &list[x + 1..] {
if i == j {
continue;
}
let (ti, tj) = (&board.tracks[i], &board.tracks[j]);
let (a, b) = nodes.from(ti, *n);
let (c, d) = nodes.from(tj, *n);
if diff(angle(a, b), angle(c, d)) <= 3.0 {
let short = if len_of(ti) <= len_of(tj) { b } else { d };
let run = visible(board, *net, *layer, a.into(), short.into());
if run < FOLD_RUN {
continue;
}
let v: V = a.into();
let place = match own_pad_at(board, *net, *layer, v) {
Some(p) => format!(" at its pad {}", board.pad_name(p)),
None => String::new(),
};
f.push(Finding { rule: "fold-back", severity: "error", at: at_mm(a), text: format!("{}: retraces itself for {:.2} mm on {}{place} (4.2)", board.net_name(*net), run / NM, board.layers[*layer].name) });
}
}
}
}
let mut by: BTreeMap<(NetId, usize), Vec<usize>> = BTreeMap::new();
for (i, t) in board.tracks.iter().enumerate() {
if len_of(t) > JOG && t.net != NO_NET {
by.entry((t.net, t.layer)).or_default().push(i);
}
}
for ((net, layer), list) in by {
for (x, &i) in list.iter().enumerate() {
for &j in &list[x + 1..] {
let (s, o) = (&board.tracks[i], &board.tracks[j]);
let (a, b): (V, V) = (s.a.into(), s.b.into());
let (c, e): (V, V) = (o.a.into(), o.b.into());
let (ls, lo) = (hypot(sub(b, a)), hypot(sub(e, c)));
let u = V((b.0 - a.0) / ls, (b.1 - a.1) / ls);
let w = sub(e, c);
if (u.0 * w.1 - u.1 * w.0).abs() / lo > 0.02 {
continue;
}
let d = ((c.0 - a.0) * -u.1 + (c.1 - a.1) * u.0).abs();
if d > DOUBLED_D {
continue;
}
let (t0, t1) = (dot(sub(c, a), u), dot(sub(e, a), u));
let (from, to) = (t0.min(t1).max(0.0), ls.min(t0.max(t1)));
if to - from < DOUBLED_RUN {
continue;
}
let on_a = |u: f64| V(a.0 + (b.0 - a.0) * u / ls, a.1 + (b.1 - a.1) * u / ls);
let run = visible(board, net, layer, on_a(from), on_a(to));
if run < DOUBLED_RUN {
continue;
}
// Two tracks sharing a node and leaving it the same way are the fold-back above.
let share = [s.a, s.b].iter().any(|p| {
let k = nodes.rep(net, layer, *p);
k == nodes.rep(net, layer, o.a) || k == nodes.rep(net, layer, o.b)
});
if share && d < 2000.0 {
continue;
}
f.push(Finding { rule: "same-net-parallel", severity: "error", at: at_mm(s.a), text: format!("{}: runs beside itself on {} for {:.2} mm, {:.3} mm centre to centre (4.2)", board.net_name(net), board.layers[layer].name, run / NM, d / NM) });
}
}
}
}
/// Width steps on a net: a node of exactly two tracks of different widths, outside the net's pads,
/// where the step does not show. The reviewer's total on the reference boards: 5, 53, 45.
fn width_steps(board: &Board, nodes: &Nodes) -> HashMap<NetId, Vec<P>> {
let mut out: HashMap<NetId, Vec<P>> = HashMap::new();
for ((net, layer, n), list) in &nodes.at {
if list.len() != 2 || board.tracks[list[0]].width == board.tracks[list[1]].width {
continue;
}
let (a, _) = nodes.from(&board.tracks[list[0]], *n);
if own_pad_at(board, *net, *layer, a.into()).is_some() {
continue;
}
out.entry(*net).or_default().push(a);
}
out
}
/// Straight-line length of the shortest tree over a net's pads (Prim).
fn tree_length(board: &Board, pads: &[usize]) -> f64 {
if pads.len() < 2 {
return 0.0;
}
let c: Vec<V> = pads.iter().map(|&p| board.pads[p].shape.center()).collect();
let mut best: Vec<f64> = c.iter().map(|q| hypot(sub(*q, c[0]))).collect();
let mut used = vec![false; c.len()];
used[0] = true;
let mut total = 0.0;
for _ in 1..c.len() {
let (i, d) = best.iter().enumerate().filter(|(i, _)| !used[*i]).map(|(i, d)| (i, *d)).min_by(|x, y| x.1.total_cmp(&y.1)).unwrap();
used[i] = true;
total += d;
for k in 0..c.len() {
if !used[k] {
best[k] = best[k].min(hypot(sub(c[k], c[i])));
}
}
}
total
}
/// A net's copper as a graph: pads, via lands and fills are single nodes, tracks are edges, split
/// wherever another track or a via lands on them. Everything the checker asks about topology (is
/// the cap first, where is the main run) is asked of this, so a via or a mid-track tap can't hide it.
pub struct Graph {
pub adj: Vec<Vec<(usize, f64, i64, P)>>,
pub pads_of: Vec<Vec<usize>>,
pub fill: Vec<bool>,
pub pad_node: HashMap<usize, usize>,
}
impl Graph {
pub fn build(board: &Board, net: NetId) -> Graph {
let tracks: Vec<&Track> = board.tracks.iter().filter(|t| t.net == net).collect();
let vias: Vec<&Via> = board.vias.iter().filter(|v| v.net == net).collect();
let mut ids: HashMap<(usize, P), usize> = HashMap::new();
let mut pos: Vec<(usize, V)> = Vec::new();
let mut node = |layer: usize, p: V, pos: &mut Vec<(usize, V)>| -> usize {
*ids.entry((layer, key(to_p(p)))).or_insert_with(|| {
pos.push((layer, p));
pos.len() - 1
})
};
let mut edges: Vec<(usize, usize, f64, i64, P)> = Vec::new();
for t in &tracks {
let (a, b): (V, V) = (t.a.into(), t.b.into());
let l = hypot(sub(b, a)).max(1.0);
let mut cuts: Vec<(f64, V)> = vec![(0.0, a), (l, b)];
let mut try_cut = |p: V| {
if pt_seg(p, a, b) <= 2000.0 {
let u = dot(sub(p, a), V((b.0 - a.0) / l, (b.1 - a.1) / l));
if u > 2000.0 && u < l - 2000.0 {
cuts.push((u, p));
}
}
};
for o in &tracks {
if o.layer == t.layer {
try_cut(o.a.into());
try_cut(o.b.into());
}
}
for v in &vias {
try_cut(v.at.into());
}
cuts.sort_by(|x, y| x.0.total_cmp(&y.0));
for w in cuts.windows(2) {
let (p, q) = (node(t.layer, w[0].1, &mut pos), node(t.layer, w[1].1, &mut pos));
edges.push((p, q, w[1].0 - w[0].0, t.width, t.a));
}
}
let n0 = pos.len();
let pads: Vec<usize> = (0..board.pads.len()).filter(|&i| board.pads[i].net == net && !board.pads[i].layers.is_empty()).collect();
let fills: Vec<(usize, Vec<V>)> = board.zones.iter().filter(|z| z.net == net && !z.keepout_tracks).flat_map(|z| {
if z.fills.is_empty() { z.layers.iter().map(|l| (*l, z.outline.clone())).collect::<Vec<_>>() } else { z.fills.clone() }
}).collect();
// Node ids after the track nodes: pads, then vias, then fills.
let (first_via, first_fill) = (n0 + pads.len(), n0 + pads.len() + vias.len());
let total = first_fill + fills.len();
let mut uf = crate::conn::Uf::new(total);
for (k, &i) in pads.iter().enumerate() {
let p = &board.pads[i];
for (m, (l, v)) in pos.iter().enumerate() {
if p.layers.contains(l) && p.shape.dist_pt(*v) <= 1.0 {
uf.union(m as u32, (n0 + k) as u32);
}
}
for (vi, v) in vias.iter().enumerate() {
if p.shape.dist_pt(v.at.into()) <= v.size as f64 / 2.0 {
uf.union((first_via + vi) as u32, (n0 + k) as u32);
}
}
for (fi, (l, poly)) in fills.iter().enumerate() {
if p.layers.contains(l) && crate::conn::shape_meets_poly(&p.shape, poly) {
uf.union((first_fill + fi) as u32, (n0 + k) as u32);
}
}
for (k2, &j) in pads.iter().enumerate().skip(k + 1) {
let q = &board.pads[j];
if p.layers.iter().any(|l| q.layers.contains(l)) && p.shape.dist_shape(&q.shape) <= 1.0 {
uf.union((n0 + k) as u32, (n0 + k2) as u32);
}
}
}
for (vi, v) in vias.iter().enumerate() {
let c: V = v.at.into();
for (m, (_, q)) in pos.iter().enumerate() {
if hypot(sub(*q, c)) <= v.size as f64 / 2.0 {
uf.union(m as u32, (first_via + vi) as u32);
}
}
for (fi, (_, poly)) in fills.iter().enumerate() {
if in_poly(c, poly) {
uf.union((first_fill + fi) as u32, (first_via + vi) as u32);
}
}
}
for (fi, (l, poly)) in fills.iter().enumerate() {
for (m, (ml, q)) in pos.iter().enumerate() {
if ml == l && in_poly(*q, poly) {
uf.union(m as u32, (first_fill + fi) as u32);
}
}
}
let mut g = Graph { adj: vec![Vec::new(); total], pads_of: vec![Vec::new(); total], fill: vec![false; total], pad_node: HashMap::new() };
for (k, &i) in pads.iter().enumerate() {
let r = uf.find((n0 + k) as u32) as usize;
g.pads_of[r].push(i);
g.pad_node.insert(i, r);
}
for fi in 0..fills.len() {
let r = uf.find((first_fill + fi) as u32) as usize;
g.fill[r] = true;
}
for (p, q, l, w, at) in edges {
let (a, b) = (uf.find(p as u32) as usize, uf.find(q as u32) as usize);
if a != b {
g.adj[a].push((b, l, w, at));
g.adj[b].push((a, l, w, at));
}
}
g
}
/// The longest of the shortest paths between two pads, as its edges (length, width). The main
/// run: it carries the net end to end, whatever is drawn widest somewhere else.
pub fn main_run(&self) -> Vec<(f64, i64)> {
let dijkstra = |s: usize| -> (Vec<f64>, Vec<Option<(usize, f64, i64)>>) {
let mut d = vec![f64::INFINITY; self.adj.len()];
let mut from = vec![None; self.adj.len()];
let mut done = vec![false; self.adj.len()];
d[s] = 0.0;
loop {
let Some(u) = (0..d.len()).filter(|&i| !done[i] && d[i].is_finite()).min_by(|a, b| d[*a].total_cmp(&d[*b])) else { break };
done[u] = true;
for &(v, l, w, _) in &self.adj[u] {
if d[u] + l < d[v] {
d[v] = d[u] + l;
from[v] = Some((u, l, w));
}
}
}
(d, from)
};
let ends: Vec<usize> = (0..self.adj.len()).filter(|&i| !self.pads_of[i].is_empty() && !self.adj[i].is_empty()).collect();
let mut best: Vec<(f64, i64)> = Vec::new();
let mut best_len = 0.0;
for &s in &ends {
let (d, from) = dijkstra(s);
for &e in &ends {
if d[e].is_finite() && d[e] > best_len {
best_len = d[e];
best.clear();
let mut c = e;
while let Some((p, l, w)) = from[c] {
best.push((l, w));
c = p;
}
}
}
}
best
}
}
/// Where a walk from a supply pin first stops (4.2: "each supply pin routes to its cap's pad first").
#[derive(Debug, PartialEq, Clone, Copy)]
enum Stop {
Cap,
Fill,
Pad(usize),
Junction(P),
End,
}
fn walk(g: &Graph, board: &Board, start: usize, first: usize, at: P, is_cap: &dyn Fn(usize) -> bool) -> Stop {
let (mut prev, mut cur, mut last) = (start, first, at);
for _ in 0..200 {
// A fill merges everything it touches into one node, so it is judged first: reaching it is
// reaching the rail, whatever else it also touches.
if g.fill[cur] {
return Stop::Fill;
}
if g.pads_of[cur].iter().any(|&p| is_cap(p)) {
return Stop::Cap;
}
if let Some(&p) = g.pads_of[cur].first() {
return Stop::Pad(p);
}
// Distinct neighbours: a track and a stub laid over it are one way on.
let mut on: Vec<(usize, P)> = Vec::new();
for &(v, _, _, a) in &g.adj[cur] {
if v != prev && !on.iter().any(|o| o.0 == v) {
on.push((v, a));
}
}
match on.len() {
0 => return Stop::End,
1 => {
prev = cur;
last = on[0].1;
cur = on[0].0;
}
_ => return Stop::Junction(last),
}
}
let _ = board;
Stop::End
}
/// Cap first, walked on the real copper, through vias and mid-track taps.
fn cap_first(board: &Board, net: NetId, g: &Graph, f: &mut Vec<Finding>, s: &mut Score) {
let is_cap = |p: usize| ref_prefix(&board.footprints[board.pads[p].fp].reference) == "C";
let caps = g.pads_of.iter().flatten().filter(|&&p| is_cap(p)).count();
if caps == 0 {
return;
}
let mut pins: Vec<usize> = g.pads_of.iter().flatten().copied().filter(|&p| crate::placement::is_supply_pin(board, p)).collect();
pins.sort();
for pin in pins {
let Some(&start) = g.pad_node.get(&pin) else { continue };
s.cap_first.pins += 1;
if g.fill[start] {
s.cap_first.on_fill += 1;
continue;
}
if g.pads_of[start].iter().any(|&p| is_cap(p)) {
s.cap_first.direct += 1;
continue;
}
// Pins tied side by side count as one pin (4.1): carry on from the neighbour.
let mut stops: Vec<Stop> = Vec::new();
let mut seen = vec![start];
let mut work = vec![start];
while let Some(n) = work.pop() {
for &(v, _, _, a) in &g.adj[n] {
let st = walk(g, board, n, v, a, &is_cap);
if let Stop::Pad(p) = st {
if board.pads[p].fp == board.pads[pin].fp {
if let Some(&m) = g.pad_node.get(&p) {
if !seen.contains(&m) {
seen.push(m);
work.push(m);
continue;
}
}
continue;
}
}
stops.push(st);
}
}
if stops.iter().any(|x| *x == Stop::Cap) {
s.cap_first.cap += 1;
} else if stops.iter().any(|x| *x == Stop::Fill) {
s.cap_first.on_fill += 1;
} else {
// A pin with no copper yet is unrouted, which is `unconnected`'s to report, not this.
let (at, what) = match stops.iter().find(|x| matches!(x, Stop::Junction(_))) {
Some(Stop::Junction(p)) => (*p, "a junction".to_string()),
_ => match stops.iter().find_map(|x| if let Stop::Pad(p) = x { Some(*p) } else { None }) {
Some(p) => (board.pads[pin].center, format!("pad {}", board.pad_name(p))),
None => continue,
},
};
s.cap_first.junction += 1;
f.push(Finding { rule: "cap-first", severity: "warn", at: at_mm(at), text: format!("{} ({}): reaches {what} before a cap; route pin to cap first (4.2)", board.pad_name(pin), board.net_name(net)) });
}
}
}
/// Boards under 3 x 3 in carry 4.2's floor on the main run; larger ones record the margin.
fn is_small(board: &Board) -> bool {
let (w, h) = (board.bbox.2 - board.bbox.0, board.bbox.3 - board.bbox.1);
w < 76.2 * NM && h < 76.2 * NM && w > 0.0
}
/// Rails, from the copper graph (4.2, and his decision of 2026-10-27: "Main runs only. Perform
/// analysis on branches"). The main run is the path that carries the net end to end; its width is
/// what most of that length is drawn at, so short necks at pins don't count against it and a short
/// wide patch doesn't excuse it. Everything else is a branch, recorded with its width and length.
fn rail_rules(board: &Board, rules: &Rules, net: NetId, g: &Graph, f: &mut Vec<Finding>, s: &mut Score) {
let name = board.net_name(net);
let main = g.main_run();
if main.is_empty() {
return;
}
let len: f64 = main.iter().map(|e| e.0).sum();
let mut by: Vec<(i64, f64)> = Vec::new();
for (l, w) in &main {
match by.iter_mut().find(|x| x.0 == *w) {
Some(x) => x.1 += l,
None => by.push((*w, *l)),
}
}
by.sort_by_key(|x| x.0);
let mut acc = 0.0;
let mut width = by[0].0;
for (w, l) in &by {
acc += l;
if acc >= len / 2.0 {
width = *w;
break;
}
}
let mut widths: BTreeMap<i64, f64> = BTreeMap::new();
for t in board.tracks.iter().filter(|t| t.net == net && t.width != width) {
*widths.entry(t.width).or_insert(0.0) += len_of(t);
}
s.rails.push(RailStat { name: name.to_string(), main_width_mm: to_mm(width), main_len_mm: len / NM, fed_by_fill: g.fill.iter().any(|x| *x), branches: widths.iter().map(|(w, l)| (to_mm(*w), (l / NM * 100.0).round() / 100.0)).collect() });
let first = board.tracks.iter().find(|t| t.net == net && t.width == width).map(|t| t.a).unwrap_or((0, 0));
if is_small(board) && width < rules.power_floor - 1000 && len >= RAIL_MIN_RUN {
f.push(Finding { rule: "rail-floor", severity: "error", at: at_mm(first), text: format!("{name}: main run {:.2} mm long is {:.3} mm wide; 4.2's floor on a board under 3 x 3 in is {:.3} mm", len / NM, to_mm(width), to_mm(rules.power_floor)) });
}
}
/// Under this the "main run" is a pin to its cap, which 4.2 calls a branch.
const RAIL_MIN_RUN: f64 = 3.0 * NM;
/// Fewest millimetres of straight line per via. His boards' lowest: 2.98 (benchmark C /MCU2_TO_1, a
/// non-priority net), so this reads off his practice; it warns, it does not gate. /RX_A: 0.93.
const VIA_SPACING: f64 = 2.9;
/// Copper over straight line on a net, read off the reference boards: worst 1.87 (v1.3 GPIO2). Warns only.
const EFFICIENCY: f64 = 1.9;
/// More width steps than this on one net. His boards' most on any net: 3 (benchmark C).
const NET_STEPS: usize = 3;
pub fn net_rules(board: &Board, rules: &Rules, infos: &[NetInfo], nodes: &Nodes, f: &mut Vec<Finding>, s: &mut Score) {
let steps = width_steps(board, nodes);
let mut with_fill: HashSet<NetId> = HashSet::new();
for z in &board.zones {
if z.net != NO_NET && !z.keepout_tracks {
with_fill.insert(z.net);
}
}
for info in infos {
let name = board.net_name(info.net);
let tracks: Vec<&Track> = board.tracks.iter().filter(|t| t.net == info.net).collect();
if !looks_ground(name) && (looks_power(name) || info.level == Level::Power) {
let g = Graph::build(board, info.net);
cap_first(board, info.net, &g, f, s);
rail_rules(board, rules, info.net, &g, f, s);
}
if tracks.is_empty() {
continue;
}
let len: f64 = tracks.iter().map(|t| len_of(t)).sum();
let mst = tree_length(board, &info.pads);
let vias = board.vias.iter().filter(|v| v.net == info.net).count();
let st = steps.get(&info.net).cloned().unwrap_or_default();
let fill = with_fill.contains(&info.net);
s.nets.push(NetStat { name: name.to_string(), level: info.level.name(), pads: info.pads.len(), len_mm: len / NM, tree_mm: mst / NM, vias, width_steps: st.len(), fill });
let at = at_mm(tracks[0].a);
if st.len() > NET_STEPS {
f.push(Finding { rule: "width-steps", severity: "warn", at: at_mm(st[0]), text: format!("{name}: {} width changes; the reference boards' most on a net is {NET_STEPS} (4.2: one technique, fewest segments)", st.len()) });
}
// A rail may via often (4.2 item 3), and a fill gives no straight-line measure.
let rail = looks_power(name) || looks_ground(name) || info.level == Level::PlaneFanout || fill;
if !rail && vias > 0 && mst / NM / (vias as f64) < VIA_SPACING {
f.push(Finding { rule: "via-count", severity: "warn", at, text: format!("{name}: {vias} vias on a net {:.2} mm across; the reference boards' tightest is {:.2} mm per via (4.2: don't hop where the top works)", mst / NM, 2.98) });
}
if !rail && mst >= 1.0 * NM && len / mst > EFFICIENCY {
f.push(Finding { rule: "route-efficiency", severity: "warn", at, text: format!("{name}: {:.2} mm of copper for {:.2} mm of straight line, {:.2} x; the reference boards' worst is 1.87 x (4.2: as straight as possible)", len / NM, mst / NM, len / mst) });
}
}
s.nets.sort_by(|a, b| a.name.cmp(&b.name));
s.width_steps = steps.values().map(|v| v.len()).sum();
}
/// Copper a net has lying apart from any pad (4.3: "islands: in practice, never"). A fill nothing
/// feeds is an error: connectivity counted only pads, so an unfed ring passed. Tracks and vias with
/// no pad are reported as a warning, since a stitching via in a plane is one too.
pub fn island_rules(board: &Board, f: &mut Vec<Finding>) {
for isl in crate::conn::islands(board) {
if isl.has_pad {
continue;
}
let name = board.net_name(isl.net);
if isl.fill_pieces > 0 {
f.push(Finding { rule: "fill-unfed", severity: "error", at: (isl.at.0 / NM, isl.at.1 / NM), text: format!("{name}: a fill on {} of about {:.2} sq mm reaches no pad of the net (4.3)", board.layers[isl.layer].name, isl.area / NM / NM) });
} else {
f.push(Finding { rule: "orphan-copper", severity: "warn", at: (isl.at.0 / NM, isl.at.1 / NM), text: format!("{name}: {} tracks and {} vias reach no pad of the net", isl.tracks, isl.vias) });
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::check::check;
fn board(extra: &str) -> Board {
let head = r#"(kicad_pcb (layers (0 "F.Cu" signal) (2 "B.Cu" signal)) (setup (pad_to_mask_clearance 0.05))
(gr_rect (start 0 0) (end 30 30) (layer "Edge.Cuts"))
(footprint "x" (layer "F.Cu") (at 10 10) (property "Reference" "R1")
(pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu" "F.Mask") (net "A"))
(pad "2" smd rect (at 8 0) (size 1 1) (layers "F.Cu" "F.Mask") (net "A")))"#;
Board::parse(format!("{head}{extra})")).unwrap()
}
fn seg(a: (f64, f64), b: (f64, f64)) -> String {
format!(r#"(segment (start {} {}) (end {} {}) (width 0.2) (layer "F.Cu") (net "A"))"#, a.0, a.1, b.0, b.1)
}
fn count(extra: &str, rule: &str) -> usize {
check(&board(extra), &Rules::default()).0.iter().filter(|x| x.rule == rule).count()
}
#[test]
fn a_retrace_out_of_a_pad_is_a_fold_back_and_one_inside_it_is_not() {
// Up to the pad's centre and back out along itself, as DIO did at R15.2.
let out = format!("{}{}", seg((14.0, 14.0), (10.0, 10.0)), seg((10.0, 10.0), (13.0, 13.0)));
assert_eq!(count(&out, "fold-back"), 1);
assert_eq!(count(&out, "corner-acute"), 0);
// The same retrace, wholly under the pad's copper.
let hid = format!("{}{}", seg((14.0, 14.0), (10.0, 10.0)), seg((10.0, 10.0), (10.2, 10.2)));
assert_eq!(count(&hid, "fold-back"), 0);
}
#[test]
fn a_doubled_run_counts_only_what_shows() {
let side = format!("{}{}", seg((12.0, 5.0), (16.0, 5.0)), seg((12.0, 5.3), (16.0, 5.3)));
assert_eq!(count(&side, "same-net-parallel"), 1);
let far = format!("{}{}", seg((12.0, 5.0), (16.0, 5.0)), seg((12.0, 5.6), (16.0, 5.6)));
assert_eq!(count(&far, "same-net-parallel"), 0);
}
#[test]
fn a_fill_with_no_pad_is_unfed() {
let z = r#"(zone (net "A") (layer "F.Cu") (polygon (pts (xy 20 20) (xy 22 20) (xy 22 22) (xy 20 22))) (filled_polygon (layer "F.Cu") (pts (xy 20 20) (xy 22 20) (xy 22 22) (xy 20 22))))"#;
assert_eq!(count(z, "fill-unfed"), 1);
let fed = format!("{z}{}", seg((18.0, 10.0), (21.0, 21.0)));
assert_eq!(count(&fed, "fill-unfed"), 0);
}
/// An IC with one supply pin at (10, 20), a cap's supply pad at (14, 20), one more pad on the rail.
fn supply(extra: &str) -> Board {
let head = r#"(kicad_pcb (layers (0 "F.Cu" signal) (2 "B.Cu" signal))
(gr_rect (start 0 0) (end 30 30) (layer "Edge.Cuts"))
(footprint "ic" (layer "F.Cu") (at 8 20) (property "Reference" "U1")
(pad "1" smd rect (at 2 0) (size 0.5 0.5) (layers "F.Cu") (net "+3V3") (pintype "power_in"))
(pad "2" smd rect (at 2 2) (size 0.5 0.5) (layers "F.Cu"))
(pad "3" smd rect (at -2 0) (size 0.5 0.5) (layers "F.Cu"))
(pad "4" smd rect (at -2 2) (size 0.5 0.5) (layers "F.Cu"))
(pad "5" smd rect (at 0 3) (size 0.5 0.5) (layers "F.Cu")))
(footprint "c" (layer "F.Cu") (at 14 20) (property "Reference" "C1")
(pad "1" smd rect (at 0 0) (size 0.5 0.5) (layers "F.Cu") (net "+3V3"))
(pad "2" smd rect (at 1 0) (size 0.5 0.5) (layers "F.Cu") (net "GND")))
(footprint "r" (layer "F.Cu") (at 25 20) (property "Reference" "R1")
(pad "1" smd rect (at 0 0) (size 0.5 0.5) (layers "F.Cu") (net "+3V3"))
(pad "2" smd rect (at 1 0) (size 0.5 0.5) (layers "F.Cu") (net "X")))"#;
Board::parse(format!("{head}{extra})")).unwrap()
}
fn t(a: (f64, f64), b: (f64, f64), layer: &str) -> String {
format!(r#"(segment (start {} {}) (end {} {}) (width 0.5) (layer "{layer}") (net "+3V3"))"#, a.0, a.1, b.0, b.1)
}
fn caps(b: &Board) -> usize {
check(b, &Rules::default()).0.iter().filter(|x| x.rule == "cap-first").count()
}
#[test]
fn cap_first_sees_past_vias_and_taps() {
// Pin, cap, then rail: fine.
let good = format!("{}{}", t((10.0, 20.0), (14.0, 20.0), "F.Cu"), t((14.0, 20.0), (25.0, 20.0), "F.Cu"));
assert_eq!(caps(&supply(&good)), 0);
// A tap landing mid-track between the pin and the cap: the old walk saw only endpoints.
let tap = format!("{good}{}", t((12.0, 20.0), (12.0, 24.0), "F.Cu"));
assert_eq!(caps(&supply(&tap)), 1);
// The pin goes down a via with the rail on the far side, and only then to the cap.
let via = format!(
"{}{}{}{}{}",
t((10.0, 20.0), (11.0, 20.0), "F.Cu"),
r#"(via (at 11 20) (size 0.6) (drill 0.3) (layers "F.Cu" "B.Cu") (net "+3V3"))"#,
t((11.0, 20.0), (25.0, 20.0), "B.Cu"),
t((11.0, 20.0), (14.0, 20.0), "F.Cu"),
r#"(via (at 25 20) (size 0.6) (drill 0.3) (layers "F.Cu" "B.Cu") (net "+3V3"))"#,
);
assert_eq!(caps(&supply(&via)), 1);
}
#[test]
fn the_floor_reads_the_main_run_not_the_widest_scrap() {
// 20 mm drawn at 0.2 with 1 mm at 0.8 is a 0.2 mm rail, on a small board.
let small = |s: &str| Board::parse(format!(r#"(kicad_pcb (layers (0 "F.Cu" signal) (2 "B.Cu" signal)) (gr_rect (start 0 0) (end 30 30) (layer "Edge.Cuts")) (footprint "r" (layer "F.Cu") (at 2 5) (property "Reference" "R1") (pad "1" smd rect (at 0 0) (size 0.5 0.5) (layers "F.Cu") (net "+3V3"))) (footprint "q" (layer "F.Cu") (at 23 5) (property "Reference" "R2") (pad "1" smd rect (at 0 0) (size 0.5 0.5) (layers "F.Cu") (net "+3V3"))){s})"#)).unwrap();
let tr = |a: f64, b: f64, w: f64| format!(r#"(segment (start {a} 5) (end {b} 5) (width {w}) (layer "F.Cu") (net "+3V3"))"#);
let thin = format!("{}{}", tr(2.0, 22.0, 0.2), tr(22.0, 23.0, 0.8));
let rails = |b: &Board| check(b, &Rules::default()).0.iter().filter(|x| x.rule == "rail-floor").count();
assert_eq!(rails(&small(&thin)), 1);
// Mostly 0.5 with short necks at the pins passes: 4.2 lets a branch be narrower.
let good = format!("{}{}{}", tr(2.0, 3.0, 0.2), tr(3.0, 22.0, 0.5), tr(22.0, 23.0, 0.2));
assert_eq!(rails(&small(&good)), 0);
}
}