main
Caleb Publish 0.2.0 cbe04a0 11h ago
//! The placement rules routing depends on, measured on the copper pads (4.1). They grade the layouts
//! The designer rejected: a router can't rescue a crystal or a decoupling cap in the wrong place, and his
//! rule (R1, 4.1) is that a layout which looks fine by distances alone is not fine.

use crate::board::*;
use crate::check::Finding;
use crate::classify::*;
use crate::geom::*;
use crate::rules::Rules;

fn at_mm(p: P) -> (f64, f64) {
    (to_mm(p.0), to_mm(p.1))
}

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

fn d_edge(board: &Board, a: usize, b: usize) -> f64 {
    board.pads[a].shape.dist_shape(&board.pads[b].shape)
}

/// An IC on this board. A molecule plugged in as a part (benchmark B's U2, Molecule_RP2350A_v1)
/// carries its own decoupling on its own board, so its contacts are not supply pins here.
fn is_ic(board: &Board, fp: usize) -> bool {
    let f = &board.footprints[fp];
    matches!(ref_prefix(&f.reference), "U" | "IC") && f.pads.len() > 4 && !f.lib.to_ascii_lowercase().contains("molecule")
}

/// Distance of `p` from the line a-b, and how far along it (0 at a, 1 at b).
fn along(p: V, a: V, b: V) -> (f64, f64) {
    let ab = sub(b, a);
    let l2 = dot(ab, ab).max(1.0);
    let t = dot(sub(p, a), ab) / l2;
    let q = V(a.0 + ab.0 * t, a.1 + ab.1 * t);
    (hypot(sub(p, q)), t)
}

/// Crystal laid out as a circuit (4.1): the XOUT series resistor inline at the MCU pin, each load
/// cap's signal pad on the line from the pin to the crystal pad. The crystal's distance from the
/// MCU is not the measure; the line is.
pub fn crystal_rules(board: &Board, f: &mut Vec<Finding>) {
    for (fi, x) in board.footprints.iter().enumerate() {
        if !is_crystal(board, fi) {
            continue;
        }
        for &xp in &x.pads {
            let net = board.pads[xp].net;
            let name = board.net_name(net);
            if net == NO_NET || name.starts_with("unconnected-") || looks_ground(name) {
                continue;
            }
            let on_net: Vec<usize> = (0..board.pads.len()).filter(|&i| board.pads[i].net == net && i != xp).collect();
            // The pin is on this net, or one series resistor away.
            let mut pin = on_net.iter().copied().find(|&i| is_ic(board, board.pads[i].fp));
            let mut series: Option<(usize, usize)> = None;
            if pin.is_none() {
                for &i in &on_net {
                    let fp = &board.footprints[board.pads[i].fp];
                    if ref_prefix(&fp.reference) != "R" || fp.pads.len() != 2 {
                        continue;
                    }
                    let Some(&other) = fp.pads.iter().find(|&&q| q != i) else { continue };
                    let on = board.pads[other].net;
                    if on == NO_NET {
                        continue;
                    }
                    if let Some(u) = (0..board.pads.len()).find(|&q| board.pads[q].net == on && is_ic(board, board.pads[q].fp)) {
                        pin = Some(u);
                        series = Some((other, i));
                        break;
                    }
                }
            }
            let Some(pin) = pin else { continue };
            let (pv, xv): (V, V) = (board.pads[pin].center.into(), board.pads[xp].center.into());
            if let Some((near, far)) = series {
                let straight = d_center(board, pin, xp);
                let path = d_center(board, pin, near) + d_center(board, near, far) + d_center(board, far, xp);
                let off = along(board.pads[near].center.into(), pv, xv).0.max(along(board.pads[far].center.into(), pv, xv).0);
                let gap = d_edge(board, pin, near);
                if path > 1.10 * straight || off > 1.0 * NM || gap > 2.5 * NM {
                    let r = &board.footprints[board.pads[near].fp].reference;
                    f.push(Finding { rule: "xtal-series-line", severity: "error", at: at_mm(board.pads[near].center), text: format!("{r} on {name}: path {:.2} mm against {:.2} mm straight (limit 1.10 x), {:.2} mm off the line (limit 1.0), {:.2} mm from {} (limit 2.5) (4.1)", path / NM, straight / NM, off / NM, gap / NM, board.pad_name(pin)) });
                }
            }
            for &i in &on_net {
                let fp = &board.footprints[board.pads[i].fp];
                if ref_prefix(&fp.reference) != "C" || fp.pads.len() != 2 {
                    continue;
                }
                let (off, t) = along(board.pads[i].center.into(), pv, xv);
                // The pad's edge may touch the line, so the 0.4 mm is measured from the pad's copper.
                let shape_off = (off - board.pads[i].size.0.min(board.pads[i].size.1) / 2.0).max(0.0);
                if shape_off > 0.4 * NM || !(0.0..=1.0).contains(&t) {
                    f.push(Finding { rule: "xtal-load-cap-line", severity: "error", at: at_mm(board.pads[i].center), text: format!("{} on {name}: {:.2} mm off the line from {} to {} (limit 0.4), {:.2} of the way along (limit 0 to 1) (4.1)", board.pad_name(i), shape_off / NM, board.pad_name(pin), board.pad_name(xp), t) });
                }
            }
        }
    }
}

/// A net the spec says is filled under a part (4.7: the supply rings) has a fill there. Net names
/// are compared without case: the spec says +3v3 and his v1.3 says +3V3 (R1).
pub fn inner_fill_rule(board: &Board, rules: &Rules, f: &mut Vec<Finding>) {
    for (r, n) in &rules.inner_fills {
        let Some(fp) = board.footprints.iter().find(|x| &x.reference == r) else { continue };
        let mut bb = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
        for &p in &fp.pads {
            let s = board.pads[p].shape.bbox();
            bb = (bb.0.min(s.0), bb.1.min(s.1), bb.2.max(s.2), bb.3.max(s.3));
        }
        let under = board.zones.iter().filter(|z| board.net_name(z.net).eq_ignore_ascii_case(n)).any(|z| {
            z.outline.iter().any(|v| v.0 >= bb.0 && v.0 <= bb.2 && v.1 >= bb.1 && v.1 <= bb.3)
        });
        if !under {
            f.push(Finding { rule: "inner-tie-no-fill", severity: "error", at: at_mm(fp.at), text: format!("{r}: the spec fills {n} under the part and no zone of that net is there (4.7)") });
        }
    }
}

/// Each supply pin has its own cap at the pin (4.1; 4.7: a tie never replaces it). No distance limit:
/// his document gives none, and a limit read off the reference boards is what P23 and R1 warn about (3.5 mm sat
/// between v1.3's 3.05 and the rejected 3.66). 4.1 says "on the same side", and that alone grades
/// the fixture set correctly: v1.3's far caps are on their pin's side, the rejected board's C8 and C9
/// are across the package. Distances go in the score, for reading. Pins next to each
/// other on the package count as one (v1.3's 43/44, 48/49; benchmark C's U9.1-3), and no cap serves two
/// groups (P25: C16 serving pins 45 and 50). A supply pin is what the schematic says it is: an
/// Benchmark C U5 feedback pin on +5V and an INA228 sense input on +3V3_EXT are not.
/// Whether a cap's pad sits on the side of the package its pin is on (4.1: "at its own supply pin,
/// on the same side"). The side is the package edge nearest the pin; the cap is on it when it lies
/// beyond the pin's inner end on that side's outward direction.
pub fn same_side(board: &Board, pin: usize, cap: usize) -> bool {
    let fp = &board.footprints[board.pads[pin].fp];
    let mut bb = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
    for &q in &fp.pads {
        let s = board.pads[q].shape.bbox();
        bb = (bb.0.min(s.0), bb.1.min(s.1), bb.2.max(s.2), bb.3.max(s.3));
    }
    let b = board.pads[pin].shape.bbox();
    let c = board.pads[pin].shape.center();
    // Distance from the pin to each package edge, and that edge's outward direction.
    let edges = [(c.0 - bb.0, -1.0, 0.0, b.2), (bb.2 - c.0, 1.0, 0.0, b.0), (c.1 - bb.1, 0.0, -1.0, b.3), (bb.3 - c.1, 0.0, 1.0, b.1)];
    // A pin within its own length of a corner belongs to both sides.
    let reach = (b.2 - b.0).max(b.3 - b.1);
    let near = edges.iter().map(|e| e.0).fold(f64::INFINITY, f64::min) + reach;
    let cc = board.pads[cap].shape.center();
    edges.iter().filter(|e| e.0 <= near).any(|e| {
        let inner = if e.1 != 0.0 { (cc.0 - e.3) * e.1 } else { (cc.1 - e.3) * e.2 };
        inner >= 0.0
    })
}

/// A supply pin by what the schematic says: typed power_in, or named VCC or VDD whatever its type
/// (v1.3's buffers have VCC typed power_out, a symbol quirk). A feedback or sense input on a rail is
/// not one (benchmark C U5.9 FB, U11.10 Vin+).
pub fn supply_typed(p: &Pad) -> bool {
    p.pintype.is_empty() || p.pintype.starts_with("power_in") || p.pinfunction.starts_with("VCC") || p.pinfunction.starts_with("VDD")
}

/// A supply pin of an IC.
pub fn is_supply_pin(board: &Board, i: usize) -> bool {
    let p = &board.pads[i];
    let name = board.net_name(p.net);
    is_ic(board, p.fp) && p.net != NO_NET && !p.layers.is_empty() && looks_power(name) && supply_typed(p)
}

pub fn decoupling_rules(board: &Board, _rules: &Rules, f: &mut Vec<Finding>, farthest: &mut Vec<(f64, String)>) {
    let is_supply = |i: usize| {
        let p = &board.pads[i];
        let name = board.net_name(p.net);
        p.net != NO_NET && !p.layers.is_empty() && looks_power(name) && supply_typed(p)
    };
    let mut by_net: std::collections::BTreeMap<NetId, Vec<Vec<usize>>> = Default::default();
    for (fi, u) in board.footprints.iter().enumerate() {
        if !is_ic(board, fi) || u.dnp {
            continue;
        }
        let mut pins: Vec<usize> = u.pads.iter().copied().filter(|&p| is_supply(p)).collect();
        pins.sort_by_key(|&p| board.pads[p].number.parse::<i64>().unwrap_or(i64::MAX));
        let mut groups: Vec<Vec<usize>> = Vec::new();
        for pin in pins {
            let joins = groups.last().and_then(|g| g.last()).map(|&q| {
                let adjacent = board.pads[pin].number.parse::<i64>().ok().zip(board.pads[q].number.parse::<i64>().ok()).map(|(a, b)| a - b <= 1).unwrap_or(false);
                board.pads[q].net == board.pads[pin].net && adjacent && d_edge(board, q, pin) <= 0.5 * NM
            });
            if joins == Some(true) {
                groups.last_mut().unwrap().push(pin);
            } else {
                groups.push(vec![pin]);
            }
        }
        for g in groups {
            by_net.entry(board.pads[g[0]].net).or_default().push(g);
        }
    }
    for (net, groups) in by_net {
        let caps: Vec<usize> = (0..board.pads.len()).filter(|&i| board.pads[i].net == net && ref_prefix(&board.footprints[board.pads[i].fp].reference) == "C" && !board.footprints[board.pads[i].fp].dnp).collect();
        if caps.is_empty() {
            continue;
        }
        // Every (group, cap) by distance, nearest first; each takes the closest still free.
        let mut options: Vec<(f64, usize, usize)> = Vec::new();
        for (gi, g) in groups.iter().enumerate() {
            for &c in &caps {
                let d = g.iter().map(|&p| d_edge(board, p, c)).fold(f64::INFINITY, f64::min);
                options.push((d, gi, c));
            }
        }
        options.sort_by(|a, b| a.0.total_cmp(&b.0));
        let mut own: Vec<Option<(usize, f64)>> = vec![None; groups.len()];
        let mut taken: Vec<usize> = Vec::new();
        for (d, gi, c) in options {
            if own[gi].is_none() && !taken.contains(&c) {
                own[gi] = Some((c, d));
                taken.push(c);
            }
        }
        for (gi, g) in groups.iter().enumerate() {
            let pin = g[0];
            let name = board.net_name(net);
            match own[gi] {
                Some((c, _)) if g.iter().any(|&q| same_side(board, q, c)) => {}
                Some((c, d)) => f.push(Finding { rule: "decoupling-pin", severity: "error", at: at_mm(board.pads[pin].center), text: format!("{} ({name}): its own cap {} is {:.2} mm away on another side of the package (4.1: at its own pin, on the same side)", board.pad_name(pin), board.pad_name(c), d / NM) }),
                None => f.push(Finding { rule: "decoupling-pin", severity: "error", at: at_mm(board.pads[pin].center), text: format!("{} ({name}): no cap of its own; each one already serves another pin (4.1)", board.pad_name(pin)) }),
            }
            if let Some((_, d)) = own[gi] {
                farthest.push((d / NM, board.pad_name(pin)));
            }
        }
    }
}