main
Caleb Publish 0.2.0 cbe04a0 11h ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
//! Sort nets into the designer's routing hierarchy (4.2) and work out each net's topology: the order its
//! pads are joined in. Decoupling is cap first, crystals are daisy chains, data prefers a chain.

use crate::board::*;
use crate::geom::*;
use crate::rules::Rules;
use std::collections::{BTreeMap, HashMap, HashSet};

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Level {
    Kelvin = 0,
    Pair = 1,
    Crystal = 2,
    Data = 3,
    Power = 4,
    PlaneFanout = 5,
    Other = 6,
}

impl Level {
    pub fn name(self) -> &'static str {
        match self {
            Level::Kelvin => "kelvin",
            Level::Pair => "differential pair",
            Level::Crystal => "crystal",
            Level::Data => "data",
            Level::Power => "power",
            Level::PlaneFanout => "plane via",
            Level::Other => "non-priority",
        }
    }
}

pub struct NetInfo {
    pub net: NetId,
    pub level: Level,
    pub pads: Vec<usize>,
    /// Why this level, from the pins and parts on the net. Printed by `plan`, because a wrong class
    /// means wrong widths and the wrong turn in the hierarchy, and the designer has to be able to see it.
    pub why: String,
    /// A switch node (4.2 Switching loops): keep it compact, never pour it, MCU outside the loop.
    pub switching: bool,
    /// Kelvin taps on this net, each a chain of pads ending on the shunt (4.2): dedicated copper,
    /// routed apart from the rest of the net, which is planned without the amplifier's pins.
    pub taps: Vec<Vec<usize>>,
}

pub fn ref_prefix(r: &str) -> &str {
    let end = r.find(|c: char| c.is_ascii_digit()).unwrap_or(r.len());
    &r[..end]
}

/// A power rail by name: +3v3, +5V, VBUS, VCC, VDD... (never by width, 4.2).
pub fn looks_power(name: &str) -> bool {
    let n = name.trim_start_matches('/').to_ascii_uppercase();
    n.starts_with('+') || n.starts_with("VCC") || n.starts_with("VDD") || n.starts_with("VBUS") || n.starts_with("VIN") || n.starts_with("VSYS") || n.starts_with("PWR") || n == "VBAT"
}

pub fn looks_ground(name: &str) -> bool {
    let n = name.trim_start_matches('/').to_ascii_uppercase();
    n == "GND" || n.starts_with("GND") || n == "VSS" || n == "AGND" || n == "DGND" || n == "PGND"
}

/// Non-switching signals that can via often without a noticeable effect (4.2 item 4).
pub fn looks_low_priority(name: &str) -> bool {
    let n = name.trim_start_matches('/').to_ascii_uppercase();
    n.contains("LED") || n.contains("RST") || n.contains("RESET") || n == "RUN" || n.contains("BOOT") || n.contains("SW_") || n.contains("BTN")
}

/// The 90-ohm figures in PCB_Troubleshooting ("A USB pair fails"), measured for one JLC 4-layer stack.
/// Only a last resort when neither the spec nor the project sets a pair: every use is reported.
const TROUBLESHOOTING_PAIR: (f64, f64) = (0.2817, 0.15);

/// Pairs by name when the spec does not list them: ..D+/..D-, .._P/.._N, ..DP/..DN, then the width
/// and gap of every pair (4.2: specs are predetermined, not chosen here): the spec's, else the net
/// class's (`Rules::bind` must have run). Returns a note for each figure that came from neither.
pub fn find_pairs(board: &Board, rules: &mut Rules) -> Vec<String> {
    let names: HashSet<&str> = board.nets.iter().map(|s| s.as_str()).collect();
    let listed: HashSet<String> = rules.pairs.iter().flat_map(|p| [p.p.clone(), p.n.clone()]).collect();
    let mut found = Vec::new();
    for name in board.nets.iter() {
        for (a, b) in [("D+", "D-"), ("_P", "_N"), ("_DP", "_DN"), ("+", "-")] {
            if let Some(stem) = name.strip_suffix(a) {
                let other = format!("{stem}{b}");
                if names.contains(other.as_str()) && !listed.contains(name) && !(a == "+" && stem.is_empty()) {
                    if a == "+" && !stem.to_ascii_uppercase().ends_with("USB") && !stem.to_ascii_uppercase().ends_with('D') {
                        continue;
                    }
                    found.push((name.clone(), other));
                    break;
                }
            }
        }
    }
    found.sort();
    found.dedup();
    for (p, n) in found {
        rules.pairs.push(crate::rules::Pair { p, n, width: 0, gap: 0 });
    }
    let mut notes = Vec::new();
    let mut pairs = std::mem::take(&mut rules.pairs);
    for p in pairs.iter_mut() {
        let class = board.net_ids.get(&p.p).and_then(|&id| rules.pair_for(id));
        if p.width == 0 {
            p.width = class.map(|c| c.0).unwrap_or(0);
        }
        if p.gap == 0 {
            p.gap = class.map(|c| c.1).unwrap_or(0);
        }
        if p.width == 0 || p.gap == 0 {
            if p.width == 0 {
                p.width = mm(TROUBLESHOOTING_PAIR.0);
            }
            if p.gap == 0 {
                p.gap = mm(TROUBLESHOOTING_PAIR.1);
            }
            notes.push(format!("DEFAULT: pair {}/{} has no width or gap in the spec or a net class, so it takes {:.4}/{:.3} mm from PCB_Troubleshooting's JLC 4-layer table. Confirm it with the fab's calculator for this stack (2.2)", p.p, p.n, to_mm(p.width), to_mm(p.gap)));
        }
    }
    rules.pairs = pairs;
    notes
}

pub fn is_crystal(board: &Board, fp: usize) -> bool {
    let f = &board.footprints[fp];
    let r = ref_prefix(&f.reference);
    (r == "Y" || r == "X" || f.lib.to_ascii_lowercase().contains("crystal")) && f.pads.len() >= 2 && f.pads.len() <= 6
}

/// An LED by its part, not its net name: the reference says LED, or the library part does.
pub fn is_led(board: &Board, fp: usize) -> bool {
    let f = &board.footprints[fp];
    let r = ref_prefix(&f.reference);
    r == "LED" || (r == "D" && f.lib.to_ascii_lowercase().contains("led"))
}

/// A net that drives an LED, directly or through a series resistor: non-switching, may via often
/// (4.2 item 4). A diode that isn't an LED doesn't count: an ESD array on a terminal line is on the
/// line for a reason, and benchmark C's TI/TO lines were going last because of one.
pub fn drives_led(board: &Board, pads: &[usize]) -> bool {
    pads.iter().any(|&p| {
        let fp = board.pads[p].fp;
        if is_led(board, fp) {
            return true;
        }
        let f = &board.footprints[fp];
        if ref_prefix(&f.reference) != "R" || f.pads.len() != 2 {
            return false;
        }
        let other = f.pads.iter().copied().find(|&o| o != p);
        let Some(other) = other else { return false };
        let net = board.pads[other].net;
        // A resistor to GND or a rail also reaches some LED: that's a pull or a set resistor.
        let name = board.net_name(net);
        net != NO_NET && !looks_ground(name) && (!looks_power(name) || name.to_ascii_uppercase().contains("LED")) && board.pads.iter().any(|q| q.net == net && is_led(board, q.fp))
    })
}

/// A part value as ohms: R068, 0R1, 10m, 100mR, 4k7, 22, 0.05. None when it isn't a plain resistance.
pub fn ohms(value: &str) -> Option<f64> {
    let v = value.trim().to_ascii_lowercase().replace("ohms", "").replace("ohm", "").replace('\u{3a9}', "");
    let v = v.split_whitespace().next()?.to_string();
    for (mark, scale) in [('m', 1e-3), ('k', 1e3), ('r', 1.0)] {
        if let Some(i) = v.find(mark) {
            let (head, tail) = (&v[..i], &v[i + 1..]);
            // "100mr" is 100 milliohms: the trailing r only repeats the unit.
            let tail = tail.strip_suffix('r').unwrap_or(tail);
            let head = if head.is_empty() { "0" } else { head };
            let text = if tail.is_empty() { head.to_string() } else { format!("{head}.{tail}") };
            return text.parse::<f64>().ok().map(|x| x * scale);
        }
    }
    v.parse().ok()
}

/// A current-sense shunt: a low-value two-pad resistor. His standard parts read R068 and the like.
fn is_shunt(board: &Board, fp: usize) -> bool {
    let f = &board.footprints[fp];
    ref_prefix(&f.reference).starts_with('R') && f.pads.iter().filter(|&&p| !board.pads[p].layers.is_empty()).count() == 2 && ohms(&f.value).map(|x| x > 0.0 && x < 1.0).unwrap_or(false)
}

/// Each shunt with an amplifier's sense pins across it: (footprint of the shunt, then for each side
/// its pad and the amplifier pins on that side). The nets are rails, so the name says nothing: what
/// makes these Kelvin taps is the amplifier's input pins on both sides of a milliohm resistor.
pub fn kelvin_taps(board: &Board) -> Vec<Vec<usize>> {
    let mut out = Vec::new();
    for (fi, f) in board.footprints.iter().enumerate() {
        if !is_shunt(board, fi) || f.dnp {
            continue;
        }
        let pads: Vec<usize> = f.pads.iter().copied().filter(|&p| !board.pads[p].layers.is_empty()).collect();
        let (a, b) = (pads[0], pads[1]);
        let (na, nb) = (board.pads[a].net, board.pads[b].net);
        if na == NO_NET || nb == NO_NET || na == nb {
            continue;
        }
        for (gi, g) in board.footprints.iter().enumerate() {
            if gi == fi || g.pads.len() < 5 || !matches!(ref_prefix(&g.reference), "U" | "IC") {
                continue;
            }
            // A power pin on the rail is just the part being fed; sense pins are inputs.
            let sense = |net: NetId| -> Vec<usize> { g.pads.iter().copied().filter(|&p| board.pads[p].net == net && !board.pads[p].layers.is_empty() && matches!(board.pads[p].pintype.as_str(), "input" | "passive")).collect() };
            let (sa, sb) = (sense(na), sense(nb));
            if sa.is_empty() || sb.is_empty() {
                continue;
            }
            for (shunt_pad, mut pins) in [(a, sa), (b, sb)] {
                pins.push(shunt_pad);
                out.push(pins);
            }
        }
    }
    out
}

/// Is this a switch node? Topology first: an inductor meeting a diode, a transistor or an IC pin at
/// one net (4.2 Switching loops). A converter module hides its inductor, so a net that is only pins of
/// one IC that also has power pins, and is named for a switch node, counts too (benchmark C's U5 SW).
fn is_switch_node(board: &Board, name: &str, pads: &[usize]) -> Option<String> {
    let prefix = |p: usize| ref_prefix(&board.footprints[board.pads[p].fp].reference).to_string();
    let inductor = pads.iter().find(|&&p| prefix(p) == "L");
    let active = pads.iter().find(|&&p| match prefix(p).as_str() {
        "D" => !is_led(board, board.pads[p].fp),
        "Q" | "U" | "IC" => board.pads[p].pintype != "input",
        _ => false,
    });
    if let (Some(&l), Some(&x)) = (inductor, active) {
        return Some(format!("inductor {} meets {}", board.pad_name(l), board.pad_name(x)));
    }
    let tail = name.rsplit('/').next().unwrap_or(name).to_ascii_uppercase();
    let named = matches!(tail.as_str(), "SW" | "LX" | "PHASE") || tail.starts_with("SW_NODE");
    let first = board.pads[*pads.first()?].fp;
    let one_part = pads.iter().all(|&p| board.pads[p].fp == first);
    let powered = board.footprints[first].pads.iter().any(|&p| matches!(board.pads[p].pintype.as_str(), "power_in" | "power_out"));
    if named && one_part && powered {
        return Some(format!("only pins of {}, a part with power pins, named {tail}", board.footprints[first].reference));
    }
    None
}

/// Which nets carry power, and why, from the pins and parts rather than from names or widths (4.2:
/// never judge current from a net name or a trace width alone). A name counts only where nothing on
/// the net contradicts it: /VBUS_G is named like a rail and is two FET gates.
fn power_nets(board: &Board, rules: &Rules, by_net: &BTreeMap<NetId, Vec<usize>>) -> HashMap<NetId, String> {
    let mut power: HashMap<NetId, String> = HashMap::new();
    let ground = |n: NetId| looks_ground(board.net_name(n));
    let prefix = |p: usize| ref_prefix(&board.footprints[board.pads[p].fp].reference).to_string();
    let is_gate = |p: usize| prefix(p) == "Q" && board.pads[p].pintype == "input";
    let is_power_pin = |p: usize| matches!(board.pads[p].pintype.as_str(), "power_in" | "power_out");
    for (&net, pads) in by_net {
        let name = board.net_name(net);
        if ground(net) {
            continue;
        }
        if rules.wide.contains_key(name) {
            power.insert(net, "listed in the spec's wideNets".to_string());
        } else if let Some(&p) = pads.iter().find(|&&p| is_power_pin(p)) {
            power.insert(net, format!("power pin {} ({})", board.pad_name(p), board.pads[p].pintype));
        } else if looks_power(name) && !pads.iter().any(|&p| is_gate(p)) && !drives_led(board, pads) {
            power.insert(net, "named as a rail, and no gate or LED on it says otherwise".to_string());
        } else {
            // An output pin with only bypass capacitors on it is a regulator's own supply, not a
            // signal: nothing receives it (benchmark C's U5 VCC).
            let outputs = pads.iter().filter(|&&p| board.pads[p].pintype == "output").count();
            let caps_to_ground = pads.iter().all(|&p| {
                board.pads[p].pintype == "output" || (prefix(p) == "C" && board.footprints[board.pads[p].fp].pads.iter().any(|&o| o != p && ground(board.pads[o].net)))
            });
            if outputs > 0 && pads.len() > outputs && caps_to_ground {
                power.insert(net, "an output pin with only bypass capacitors on it".to_string());
            }
        }
    }
    // Power carries through what is in series with it: fuses, ferrites, inductors, shunts, and the
    // channel of a power FET (several pins tied on each side, the gate typed input).
    let mut links: Vec<(NetId, NetId, String)> = Vec::new();
    for (fi, f) in board.footprints.iter().enumerate() {
        let copper: Vec<usize> = f.pads.iter().copied().filter(|&p| !board.pads[p].layers.is_empty() && board.pads[p].net != NO_NET).collect();
        let r = ref_prefix(&f.reference);
        if copper.len() == 2 && (matches!(r, "F" | "FB" | "L") || is_shunt(board, fi)) {
            links.push((board.pads[copper[0]].net, board.pads[copper[1]].net, f.reference.clone()));
        }
        if r == "Q" {
            let mut sides: BTreeMap<NetId, usize> = BTreeMap::new();
            for &p in &copper {
                if !is_gate(p) {
                    *sides.entry(board.pads[p].net).or_insert(0) += 1;
                }
            }
            let tied: Vec<NetId> = sides.iter().filter(|(_, &c)| c >= 2).map(|(&n, _)| n).collect();
            for w in tied.iter().enumerate().flat_map(|(i, a)| tied[i + 1..].iter().map(move |b| (*a, *b))) {
                links.push((w.0, w.1, format!("{} channel", f.reference)));
            }
        }
    }
    loop {
        let mut changed = false;
        for (a, b, via) in &links {
            for (from, to) in [(*a, *b), (*b, *a)] {
                if power.contains_key(&from) && !power.contains_key(&to) && !ground(to) && by_net.contains_key(&to) {
                    power.insert(to, format!("in series with {} through {via}", board.net_name(from)));
                    changed = true;
                }
            }
        }
        if !changed {
            return power;
        }
    }
}

/// Is this footprint fine pitch? 4.2 only says "fine-pitch MCU", so this asks what that means for
/// routing: between two neighbouring pads of different nets, does a straight lane at the neck fit
/// with more than the minimum spacing either side? If it doesn't, spacing inside the escape has to
/// tighten to the minimum, which is what 4.2 allows a fine-pitch escape to do. Two-pad parts have no
/// pin row to escape from.
pub fn is_fine_pitch(board: &Board, rules: &Rules, fp: usize) -> bool {
    let f = &board.footprints[fp];
    let copper: Vec<usize> = f.pads.iter().copied().filter(|&p| !board.pads[p].layers.is_empty()).collect();
    if copper.len() < 3 {
        return false;
    }
    let lane = (rules.neck + 2 * rules.clearance_for(NO_NET, NO_NET).max(rules.floor)) as f64;
    for (i, &a) in copper.iter().enumerate() {
        for &b in &copper[i + 1..] {
            let (pa, pb) = (&board.pads[a], &board.pads[b]);
            if pa.net != pb.net && pa.shape.dist_shape(&pb.shape) < lane {
                return true;
            }
        }
    }
    false
}

pub fn is_passive2(board: &Board, fp: usize) -> bool {
    let f = &board.footprints[fp];
    f.pads.iter().filter(|&&p| !board.pads[p].layers.is_empty()).count() == 2 && matches!(ref_prefix(&f.reference), "C" | "R" | "L" | "FB" | "D")
}

pub fn classify(board: &Board, rules: &Rules) -> Vec<NetInfo> {
    let planes = board.plane_nets();
    let plane_names: HashSet<&str> = rules.plane_nets.iter().map(|s| s.as_str()).collect();
    let pair_nets: HashSet<&str> = rules.pairs.iter().flat_map(|p| [p.p.as_str(), p.n.as_str()]).collect();
    let kelvin: HashSet<&str> = rules.kelvin.iter().map(|s| s.as_str()).collect();
    let skip: HashSet<&str> = rules.skip_refs.iter().map(|s| s.as_str()).collect();
    let mut by_net: BTreeMap<NetId, Vec<usize>> = BTreeMap::new();
    for (i, p) in board.pads.iter().enumerate() {
        if p.net == NO_NET || p.layers.is_empty() {
            continue;
        }
        let f = &board.footprints[p.fp];
        // A part marked do-not-populate is still copper on the board: an option resistor has to be
        // wired or it is no option, and KiCad counts its pads as open (on a current-monitor board, three
        // unpopulated address resistors left five joins open while the router reported all made).
        // Only the spec's skipRefs leave a part out.
        if skip.contains(f.reference.as_str()) {
            continue;
        }
        by_net.entry(p.net).or_default().push(i);
    }
    let power = power_nets(board, rules, &by_net);
    let mut taps: HashMap<NetId, Vec<Vec<usize>>> = HashMap::new();
    for t in kelvin_taps(board) {
        taps.entry(board.pads[t[0]].net).or_default().push(t);
    }
    let mut out = Vec::new();
    for (net, pads) in by_net {
        let name = board.net_name(net);
        // KiCad names a net "unconnected-" when no schematic wire reaches it, but two pads on one
        // such net are one pin brought out twice, and its DRC wants them joined (benchmark C's U2).
        if name.is_empty() || (name.starts_with("unconnected-") && pads.len() < 2) {
            continue;
        }
        let switch = is_switch_node(board, name, &pads);
        let (level, why) = if kelvin.contains(name) {
            (Level::Kelvin, "listed as Kelvin in the spec".to_string())
        } else if pair_nets.contains(name) {
            (Level::Pair, "a differential pair".to_string())
        } else if planes.contains_key(&net) || plane_names.contains(name) || looks_ground(name) && !planes.is_empty() {
            (Level::PlaneFanout, "has a plane".to_string())
        } else if let Some(s) = &switch {
            (Level::Power, format!("switch node: {s}"))
        } else if let Some(p) = power.get(&net) {
            (Level::Power, p.clone())
        } else if drives_led(board, &pads) {
            (Level::Other, "drives an LED".to_string())
        } else if looks_low_priority(name) {
            (Level::Other, "reset, boot or button line by name".to_string())
        } else if pads.iter().any(|&p| is_crystal(board, board.pads[p].fp)) {
            (Level::Crystal, "on a crystal".to_string())
        } else {
            (Level::Data, "signal pins only".to_string())
        };
        let taps = taps.remove(&net).unwrap_or_default();
        out.push(NetInfo { net, level, pads, why, switching: switch.is_some(), taps });
    }
    out
}

/// A connection the router must make: pad to pad, inside one net.
#[derive(Clone, Debug)]
pub struct Task {
    pub net: NetId,
    pub level: Level,
    pub a: usize,
    pub b: usize,
    /// Main rail run rather than a stub to a pin.
    pub main: bool,
    /// Join this pad to its net's copper fill (index into World::zones) instead of another pad.
    pub zone: Option<usize>,
}

/// `Task::zone` value meaning: join pad `a` to the net's rail copper, or, when `a != b`, to pad `b`
/// and whatever copper it already has. The targets are worked out from the copper at route time.
pub const RAIL_TAP: usize = usize::MAX;

fn dist(board: &Board, a: usize, b: usize) -> f64 {
    hypot(sub(board.pads[a].center.into(), board.pads[b].center.into()))
}

/// Daisy chain through `pads`, starting from the end that makes the chain shortest (4.2).
fn chain(board: &Board, pads: &[usize]) -> Vec<usize> {
    if pads.len() <= 2 {
        return pads.to_vec();
    }
    let mut best: Option<(f64, Vec<usize>)> = None;
    for &start in pads {
        let mut left: Vec<usize> = pads.iter().copied().filter(|&p| p != start).collect();
        let mut seq = vec![start];
        let mut total = 0.0;
        while !left.is_empty() {
            let cur = *seq.last().unwrap();
            let (i, d) = left.iter().enumerate().map(|(i, &p)| (i, dist(board, cur, p))).min_by(|a, b| a.1.total_cmp(&b.1)).unwrap();
            total += d;
            seq.push(left.swap_remove(i));
        }
        if best.as_ref().map(|b| total < b.0).unwrap_or(true) {
            best = Some((total, seq));
        }
    }
    best.unwrap().1
}

/// Minimum spanning tree edges between groups, nearest pads first (Prim).
fn tree(board: &Board, groups: &[Vec<usize>]) -> Vec<(usize, usize)> {
    let mut joined = vec![0usize];
    let mut out = Vec::new();
    while joined.len() < groups.len() {
        let mut best: Option<(f64, usize, usize, usize)> = None;
        for &g in &joined {
            for h in 0..groups.len() {
                if joined.contains(&h) {
                    continue;
                }
                for &a in &groups[g] {
                    for &b in &groups[h] {
                        let d = dist(board, a, b);
                        if best.map(|x| d < x.0).unwrap_or(true) {
                            best = Some((d, a, b, h));
                        }
                    }
                }
            }
        }
        let (_, a, b, h) = best.unwrap();
        out.push((a, b));
        joined.push(h);
    }
    out
}

/// Plan the pad-to-pad connections for one signal or power net, given which pads the board's
/// copper already joins.
pub fn plan_net(board: &Board, info: &NetInfo, groups: &[Vec<usize>]) -> Vec<Task> {
    let mk = |a, b, main| Task { net: info.net, level: info.level, a, b, main, zone: None };
    let live: Vec<Vec<usize>> = groups
        .iter()
        .map(|g| g.iter().copied().filter(|p| info.pads.contains(p)).collect::<Vec<_>>())
        .filter(|g: &Vec<usize>| !g.is_empty())
        .collect();
    if live.len() < 2 {
        return Vec::new();
    }
    let singles = live.iter().all(|g| g.len() == 1);
    match info.level {
        Level::Crystal if singles => {
            // IC pin -> load-cap pad -> crystal pad (4.2 crystal routing).
            let pads: Vec<usize> = live.iter().map(|g| g[0]).collect();
            let mut xtal: Vec<usize> = pads.iter().copied().filter(|&p| is_crystal(board, board.pads[p].fp)).collect();
            let mut caps: Vec<usize> = pads.iter().copied().filter(|&p| ref_prefix(&board.footprints[board.pads[p].fp].reference) == "C").collect();
            let mut rest: Vec<usize> = pads.iter().copied().filter(|p| !xtal.contains(p) && !caps.contains(p)).collect();
            let far = xtal.first().copied();
            if let Some(f) = far {
                caps.sort_by(|a, b| dist(board, *b, f).total_cmp(&dist(board, *a, f)));
                rest.sort_by(|a, b| dist(board, *b, f).total_cmp(&dist(board, *a, f)));
            }
            let mut seq = Vec::new();
            seq.append(&mut rest);
            seq.append(&mut caps);
            seq.append(&mut xtal);
            seq.windows(2).map(|w| mk(w[0], w[1], false)).collect()
        }
        Level::Power => plan_rail(board, info, &live, &[], &|_, _| false, f64::INFINITY),
        _ if singles => {
            let pads: Vec<usize> = live.iter().map(|g| g[0]).collect();
            // Each new pad joins what is already laid, so it starts as one pad and a route has no
            // copper of its own behind it to double back along. `targets_of` lets it land on any of
            // that copper, weighed against reaching the previous pad (4.2: a daisy chain preferred).
            chain(board, &pads).windows(2).map(|w| mk(w[1], w[0], false)).collect()
        }
        _ => tree(board, &live).into_iter().map(|(a, b)| mk(a, b, false)).collect(),
    }
}

/// Plan one rail (4.2 power): supply pins join their own fill or their cap first (never pin ->
/// junction -> cap), then each resulting cluster joins the rail on the bus layer, tapping onto
/// whatever rail copper is nearest when its turn comes. `zones`: this net's fills, as (index in World::zones,
/// layer, outline). `stub_clear`: can this pad reach this fill straight, on its own layer? A fill
/// behind another net's copper is reached from the bus instead (4.7: DVDD pins go out to their caps).
/// `cap_reach`: how far a pin's stub to its cap may run. Past where a stub on the pad layer costs more
/// than going down to the bus and back up, the pin and the cap each join the rail on their own.
pub fn plan_rail(board: &Board, info: &NetInfo, live: &[Vec<usize>], zones: &[(usize, usize, &Vec<V>)], stub_clear: &dyn Fn(usize, usize) -> bool, cap_reach: f64) -> Vec<Task> {
    let mut tasks = Vec::new();
    let is_ic_pin = |p: usize| {
        let f = &board.footprints[board.pads[p].fp];
        f.pads.len() > 4 && matches!(ref_prefix(&f.reference), "U" | "IC" | "Q")
    };
    let is_cap = |p: usize| is_passive2(board, board.pads[p].fp) && ref_prefix(&board.footprints[board.pads[p].fp].reference) == "C";
    let (ng, nz) = (live.len(), zones.len());
    let mut uf = crate::conn::Uf::new(ng + nz);
    let mut group_of: HashMap<usize, usize> = HashMap::new();
    for (i, g) in live.iter().enumerate() {
        for &p in g {
            group_of.insert(p, i);
        }
    }
    let edge_dist = |p: usize, poly: &Vec<V>| -> f64 {
        let n = poly.len();
        if in_poly(board.pads[p].center.into(), poly) {
            return 0.0;
        }
        (0..n).map(|i| board.pads[p].shape.dist_seg(poly[i], poly[(i + 1) % n])).fold(f64::INFINITY, f64::min)
    };
    for &p in &info.pads {
        let Some(&g) = group_of.get(&p) else { continue };
        for (zi, (_, layer, poly)) in zones.iter().enumerate() {
            if board.pads[p].layers.contains(layer) && edge_dist(p, poly) <= 1.0 {
                uf.union(g as u32, (ng + zi) as u32);
            }
        }
    }
    // A pin next to its net's fill stubs into it (4.7's method, where the designer drew the fill).
    for &p in &info.pads {
        let Some(&g) = group_of.get(&p) else { continue };
        if !is_ic_pin(p) {
            continue;
        }
        for (zi, (z, layer, poly)) in zones.iter().enumerate() {
            let same = uf.find(g as u32) == uf.find((ng + zi) as u32);
            if !same && board.pads[p].layers.contains(layer) && edge_dist(p, poly) <= 1.0 * NM && stub_clear(p, *z) {
                tasks.push(Task { net: info.net, level: info.level, a: p, b: p, main: false, zone: Some(*z) });
                uf.union(g as u32, (ng + zi) as u32);
                break;
            }
        }
    }
    let caps: Vec<usize> = info.pads.iter().copied().filter(|&p| is_cap(p)).collect();
    let mut pins: Vec<usize> = info.pads.iter().copied().filter(|&p| is_ic_pin(p)).collect();
    pins.sort_by(|&a, &b| {
        let da = caps.iter().map(|&c| dist(board, a, c)).fold(f64::INFINITY, f64::min);
        let db = caps.iter().map(|&c| dist(board, b, c)).fold(f64::INFINITY, f64::min);
        da.total_cmp(&db)
    });
    for pin in pins {
        let Some(&cap) = caps.iter().min_by(|&&a, &&b| dist(board, pin, a).total_cmp(&dist(board, pin, b))) else { continue };
        if dist(board, pin, cap) > cap_reach {
            continue;
        }
        let (Some(&ga), Some(&gb)) = (group_of.get(&pin), group_of.get(&cap)) else { continue };
        if uf.find(ga as u32) != uf.find(gb as u32) {
            uf.union(ga as u32, gb as u32);
            // Rail-routed, so the search itself weighs a long stub against the bus. `main: false`
            // holds it to the cap's own pad: cap first, never a junction on the way (4.2).
            tasks.push(Task { net: info.net, level: info.level, a: pin, b: cap, main: false, zone: Some(RAIL_TAP) });
        }
    }
    // Clusters, and the pads each one is fed from: a cap if it has one (cap first), else a passive
    // or connector pad, else the pin.
    let mut clusters: BTreeMap<u32, Vec<usize>> = BTreeMap::new();
    for (i, g) in live.iter().enumerate() {
        clusters.entry(uf.find(i as u32)).or_default().extend(g.iter().copied());
    }
    let feeds: Vec<Vec<usize>> = clusters
        .values()
        .map(|pads| {
            let c: Vec<usize> = pads.iter().copied().filter(|&p| is_cap(p)).collect();
            if !c.is_empty() {
                return c;
            }
            let o: Vec<usize> = pads.iter().copied().filter(|&p| !is_ic_pin(p)).collect();
            if !o.is_empty() { o } else { pads.clone() }
        })
        .collect();
    // With a fill on the net, the fill is the hub and every cluster taps toward it at the main width.
    // Without one, the two clusters farthest apart make the trunk and the rest tap onto it. Nearest
    // to what is already joined goes next either way, so each tap finds rail close by.
    let mut joined: Vec<usize> = Vec::new();
    let mut left: Vec<usize> = (0..feeds.len()).collect();
    if zones.is_empty() && feeds.len() >= 2 {
        let mut far: Option<(f64, usize, usize, usize, usize)> = None;
        for i in 0..feeds.len() {
            for j in i + 1..feeds.len() {
                for &a in &feeds[i] {
                    for &b in &feeds[j] {
                        let d = dist(board, a, b);
                        if far.map(|x| d > x.0).unwrap_or(true) {
                            far = Some((d, i, j, a, b));
                        }
                    }
                }
            }
        }
        let (_, i, j, a, b) = far.unwrap();
        tasks.push(Task { net: info.net, level: info.level, a, b, main: true, zone: Some(RAIL_TAP) });
        joined = vec![a, b];
        left.retain(|&k| k != i && k != j);
    }
    let hub = !zones.is_empty();
    while !left.is_empty() {
        let near = |p: usize| -> f64 {
            let to_pads = joined.iter().map(|&q| dist(board, p, q)).fold(f64::INFINITY, f64::min);
            zones.iter().map(|(_, _, poly)| edge_dist(p, poly)).fold(to_pads, f64::min)
        };
        let (pos, pad) = left
            .iter()
            .enumerate()
            .flat_map(|(pos, &k)| feeds[k].iter().map(move |&p| (pos, p)))
            .min_by(|x, y| near(x.1).total_cmp(&near(y.1)))
            .unwrap();
        left.swap_remove(pos);
        joined.push(pad);
        tasks.push(Task { net: info.net, level: info.level, a: pad, b: pad, main: hub, zone: Some(RAIL_TAP) });
    }
    // A fill with no pad of its own still has to be fed: an island is never acceptable (4.3). These
    // go last: by then the pads are one piece, so the tap's only target is the fill itself.
    let reps: Vec<usize> = feeds.iter().flatten().copied().collect();
    for (zi, (_, _, poly)) in zones.iter().enumerate() {
        let root = uf.find((ng + zi) as u32);
        let has_pad = (0..ng).any(|g| uf.find(g as u32) == root);
        if !has_pad && !poly.is_empty() {
            let c = V(poly.iter().map(|p| p.0).sum::<f64>() / poly.len() as f64, poly.iter().map(|p| p.1).sum::<f64>() / poly.len() as f64);
            if let Some(&near) = reps.iter().min_by(|&&x, &&y| hypot(sub(board.pads[x].center.into(), c)).total_cmp(&hypot(sub(board.pads[y].center.into(), c)))) {
                tasks.push(Task { net: info.net, level: info.level, a: near, b: near, main: false, zone: Some(RAIL_TAP) });
            }
        }
    }
    tasks
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A reference board, or None where the reference boards are not present (they do not ship with
    /// the source): the test that asked is then skipped.
    fn load(name: &str) -> Option<(Board, Rules)> {
        let path = format!("{}/bench/boards/{name}.ref.kicad_pcb", env!("CARGO_MANIFEST_DIR"));
        if !std::path::Path::new(&path).exists() {
            eprintln!("skipped: {path} is not here");
            return None;
        }
        let b = Board::read(&path).unwrap();
        let mut r = Rules::default();
        crate::project::apply(&path, &mut r);
        Some((b, r))
    }

    fn level_of(b: &Board, infos: &[NetInfo], name: &str) -> Level {
        infos.iter().find(|i| b.net_name(i.net) == name).unwrap_or_else(|| panic!("{name} not classified")).level
    }

    #[test]
    fn values_read_as_ohms() {
        for (text, ohm) in [("R068", 0.068), ("0R1", 0.1), ("10m", 0.01), ("100mR", 0.1), ("4k7", 4700.0), ("22", 22.0), ("0.05", 0.05), ("100mOhm", 0.1), ("1k", 1000.0)] {
            assert!((ohms(text).unwrap() - ohm).abs() < 1e-9, "{text}");
        }
        assert_eq!(ohms("BLU"), None);
    }

    /// The reviewer's list on benchmark C, and what the parts on those nets say.
    #[test]
    fn benchmark_c_classes_come_from_its_parts() {
        let Some((b, r)) = load("benchc_v10") else { return };
        let infos = classify(&b, &r);
        assert_eq!(level_of(&b, &infos, "/MCU_Upstream/5V_VCC"), Level::Power);
        assert_eq!(level_of(&b, &infos, "/MCU_Upstream/SW"), Level::Power);
        assert!(infos.iter().find(|i| b.net_name(i.net) == "/MCU_Upstream/SW").unwrap().switching);
        assert_eq!(level_of(&b, &infos, "Net-(D3-K)"), Level::Power);
        assert_eq!(level_of(&b, &infos, "Net-(D3-A)"), Level::Power);
        // An ESD array on a terminal line isn't an LED; a real LED's anode net is low priority.
        assert_eq!(level_of(&b, &infos, "/TI_0_J"), Level::Data);
        assert_eq!(level_of(&b, &infos, "/MCU_Upstream/+3V3_AN"), Level::Other);
        // A set resistor to GND doesn't drive an LED just because GND reaches one.
        assert_eq!(level_of(&b, &infos, "/MCU_Downstream/ILIM"), Level::Data);
        // RS1 is the shunt, U11 the amplifier, and its two tied pins on the far side share one tap.
        let tap = |name: &str| infos.iter().find(|i| b.net_name(i.net) == name).unwrap().taps.iter().map(|t| t.iter().map(|&p| b.pad_name(p)).collect::<Vec<_>>()).collect::<Vec<_>>();
        assert_eq!(tap("+3V3_EXT"), vec![vec!["U11.10".to_string(), "RS1.1".to_string()]]);
        assert_eq!(tap("+3V3_EXT_SENS"), vec![vec!["U11.8".to_string(), "U11.9".to_string(), "RS1.2".to_string()]]);
    }

    #[test]
    fn benchmark_b_classes_come_from_its_parts() {
        let Some((b, r)) = load("benchb_v210") else { return };
        let infos = classify(&b, &r);
        assert_eq!(level_of(&b, &infos, "-PS1"), Level::Power);
        assert_eq!(level_of(&b, &infos, "/USB2_RLY"), Level::Power);
        // Named like a rail, and is two FET gates.
        assert_eq!(level_of(&b, &infos, "/VBUS_G"), Level::Data);
        // Through the FET's channel from a power pin; the name says nothing.
        assert_eq!(level_of(&b, &infos, "/USB2_VBUS"), Level::Power);
        assert_eq!(level_of(&b, &infos, "/VBUS_CS"), Level::Power);
        assert_eq!(level_of(&b, &infos, "/USER_IO.0"), Level::Data);
    }

    #[test]
    fn piprobe_classes_come_from_its_parts() {
        let Some((b, r)) = load("piprobe_v13") else { return };
        let infos = classify(&b, &r);
        assert_eq!(level_of(&b, &infos, "+1V1"), Level::Power);
        assert_eq!(level_of(&b, &infos, "QSPI_SD0"), Level::Data);
        // Through R4 to /PWR_LED: the name of that net starts like a rail, and it's an LED.
        assert_eq!(level_of(&b, &infos, "GPIO2"), Level::Other);
        assert_eq!(level_of(&b, &infos, "XIN"), Level::Crystal);
    }

    /// Fine pitch from the pads, against the reference's parts: 0.4 and 0.5 mm QFNs are, a 2 mm module and a
    /// 1.27 mm SOIC aren't.
    #[test]
    fn fine_pitch_comes_from_the_pad_gap() {
        let fine = |b: &Board, r: &Rules, name: &str| is_fine_pitch(b, r, b.footprints.iter().position(|f| f.reference == name).unwrap());
        let Some((b, r)) = load("piprobe_v13") else { return };
        assert!(fine(&b, &r, "U1"));
        assert!(fine(&b, &r, "U2"));
        assert!(!fine(&b, &r, "C4"));
        let Some((b, r)) = load("benchb_v210") else { return };
        assert!(fine(&b, &r, "U3"));
        assert!(!fine(&b, &r, "U2"));
        assert!(!fine(&b, &r, "Q1"));
        let Some((b, r)) = load("benchc_v10") else { return };
        assert!(fine(&b, &r, "U6"));
        assert!(fine(&b, &r, "U11"));
    }
}