main
John Lauer 0.10.0: lean by default; optional modules with board-specific hints; grouped findings; lean fields.json + detail.json;… 4bbb86d
//! The IR-drop probe (issue 14): the DC voltage difference between two pads of a solved net, or from a pad to the far end
//! of its net, read from the solve's voltage map (flow.bin) without re-solving.
use serde_json::{json, Value};
use std::path::Path;

fn r(x: f64, k: f64) -> f64 { (x * k).round() / k }

/// `from` and `to` are REF.PAD (J1.1); `to` may instead be a net name, meaning the point of that net farthest in voltage
/// from `from`.
pub fn compute(dir: &Path, from: &str, to: &str) -> Result<Value, String> {
    let f: Value = crate::slim::load(dir).ok_or_else(|| format!("{}: no readable fields.json", dir.join("fields.json").display()))?;
    let bin = std::fs::read(dir.join("flow.bin")).map_err(|_| "no flow.bin next to fields.json: re-run `adom-fields analyze` with 0.7.20 or later".to_string())?;
    let fl = &f["flow"];
    let (w, h) = (fl["cols"].as_u64().unwrap_or(0) as usize, fl["rows"].as_u64().unwrap_or(0) as usize);
    let (d, x0, y0) = (fl["cellMm"].as_f64().unwrap_or(0.2), fl["x0"].as_f64().unwrap_or(0.0), fl["y0"].as_f64().unwrap_or(0.0));
    let layers: Vec<String> = fl["layers"].as_array().cloned().unwrap_or_default().iter().filter_map(|l| l.as_str().map(String::from)).collect();
    let names: Vec<String> = fl["nets"].as_array().cloned().unwrap_or_default().iter().map(|m| m["name"].as_str().unwrap_or("").to_string()).collect();
    let n = w * h;
    if n == 0 || bin.len() < layers.len() * 4 * n * 4 { return Err("flow.bin does not match fields.json's grid".into()); }
    let at = |k: usize, arr: usize, i: usize| -> f32 { let o = ((k * 4 + arr) * n + i) * 4; f32::from_le_bytes([bin[o], bin[o + 1], bin[o + 2], bin[o + 3]]) };
    let volt = |k: usize, i: usize| at(k, 2, i); let owner = |k: usize, i: usize| at(k, 3, i);
    let pads = f["pads"].as_array().cloned().unwrap_or_default();
    let pad = |s: &str| -> Result<Value, String> {
        let (rf, pn) = s.rsplit_once('.').ok_or_else(|| format!("{s}: give a pad as REF.PAD, e.g. J1.1"))?;
        pads.iter().find(|p| p["ref"] == rf && p["pad"] == pn).cloned().ok_or_else(|| format!("{s}: no such pad on the board"))
    };
    // the pad's voltage: the mean over its net's solved copper within 0.6 mm, on every layer
    let pad_v = |p: &Value| -> Result<(f64, String), String> {
        let net = p["net"].as_str().unwrap_or("").to_string();
        let id = names.iter().position(|x| *x == net).ok_or_else(|| format!("{}.{}: its net {net} is not on the grid", p["ref"], p["pad"]))? as f32;
        let (x, y) = (p["x"].as_f64().unwrap_or(0.0), p["y"].as_f64().unwrap_or(0.0));
        for rad in [0.6, 1.2] {
            let (mut s, mut c) = (0.0, 0);
            let (c0, r0) = (((x - x0) / d) as i64, ((y - y0) / d) as i64); let k_r = (rad / d).ceil() as i64;
            for k in 0..layers.len() { for dr in -k_r..=k_r { for dc in -k_r..=k_r { let (rr, cc) = (r0 + dr, c0 + dc); if rr < 0 || cc < 0 || rr >= h as i64 || cc >= w as i64 { continue; } if ((dr as f64) * d).hypot(dc as f64 * d) > rad { continue; } let i = rr as usize * w + cc as usize; let v = volt(k, i); if owner(k, i) == id && v.is_finite() { s += v as f64; c += 1; } } } }
            if c > 0 { return Ok((s / c as f64, net)); }
        }
        Err(format!("{}.{}: no copper of {net} under it on the grid", p["ref"], p["pad"]))
    };
    let pa = pad(from)?; let (va, net) = pad_v(&pa)?;
    let meta = fl["nets"].as_array().and_then(|a| a.iter().find(|m| m["name"] == net.as_str())).cloned().unwrap_or(Value::Null);
    if meta["loaded"] != true { return Err(format!("{net} carries no solved current: its drop is zero by construction; add it to the spec's loads")); }
    let (vb, to_at, to_name) = if to.contains('.') && pads.iter().any(|p| format!("{}.{}", p["ref"].as_str().unwrap_or(""), p["pad"].as_str().unwrap_or("")) == to) {
        let pb = pad(to)?; let (vb, nb) = pad_v(&pb)?;
        if nb != net { return Err(format!("{from} is on {net} and {to} is on {nb}: the probe reads the drop along one net")); }
        (vb, [pb["x"].as_f64().unwrap_or(0.0), pb["y"].as_f64().unwrap_or(0.0)], to.to_string())
    } else {
        if to != net { return Err(format!("{to}: give a pad (REF.PAD) on {net}, or the net name {net} for its farthest point")); }
        let id = names.iter().position(|x| *x == net).unwrap() as f32;
        let (mut best, mut bi) = (va, (0usize, 0usize));
        for k in 0..layers.len() { for i in 0..n { let v = volt(k, i); if owner(k, i) == id && v.is_finite() && (v as f64 - va).abs() > (best - va).abs() { best = v as f64; bi = (k, i); } } }
        (best, [x0 + ((bi.1 % w) as f64 + 0.5) * d, y0 + ((bi.1 / w) as f64 + 0.5) * d], format!("{net} (farthest point, {})", layers[bi.0]))
    };
    let worst = f["nets"].as_array().and_then(|a| a.iter().find(|x| x["net"] == net.as_str())).map(|x| json!({"at": x["peakAt"], "layer": x["peakLayer"], "peakJ": x["peakJ"], "note": "the net's highest current density: where its copper narrows most and the drop builds fastest"})).unwrap_or(Value::Null);
    Ok(json!({"net": net, "from": from, "to": to_name, "fromAt": [pa["x"], pa["y"]], "toAt": [r(to_at[0], 100.0), r(to_at[1], 100.0)], "fromV": r(va, 1e6), "toV": r(vb, 1e6), "dropMv": r((va - vb) * 1e3, 1e3), "netDropMv": r(meta["dropV"].as_f64().unwrap_or(0.0) * 1e3, 1e3), "amps": meta["amps"], "worstCell": worst}))
}