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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
//! Fast tests cut from the reference boards: a few nets of a reference board routed alone, in seconds, with
//! pass/fail checks on the result. The whole-board benchmark takes 13 minutes on benchmark C and says
//! only how many joins were made; a fixture says whether one behaviour is right (the feed to U10
//! holds its width, the trunk does not run wide to a part that draws 3 mA) and says it at once.
//!
//! A fixture strips the board, keeps every pad and pour as it is, and routes only the nets it
//! names. The other nets stay unrouted, so it tests a behaviour, not congestion: the whole-board
//! benchmark is still what says the board routes.
//!
//! `known_fail` marks a fixture that records a target the router does not meet yet. It is reported
//! and does not fail the run; when it starts passing the run says so, and the mark comes off.
use crate::board::*;
use crate::geom::*;
use serde_json::Value;
use std::path::Path;
struct Outcome {
pass: bool,
lines: Vec<String>,
}
/// Every fixture at once, each in its own process (each has its own work clock, so running side by
/// side changes no result), reported in the file's order. The set takes as long as its slowest one.
pub fn run_all(file: &str, only: Option<&str>, keep: Option<&str>) -> bool {
let text = std::fs::read_to_string(file).unwrap_or_else(|e| crate::fail(format!("{file}: {e}"), "Pass the fixtures JSON, e.g. bench/fixtures.json."));
let v: Value = serde_json::from_str(&text).unwrap_or_else(|e| crate::fail(format!("{file}: {e}"), ""));
let names: Vec<String> = v["fixtures"].as_array().map(|a| a.iter().filter_map(|x| x["name"].as_str().map(String::from)).collect()).unwrap_or_default();
let names: Vec<String> = names.into_iter().filter(|n| only.map(|o| n.contains(o)).unwrap_or(true)).collect();
let exe = std::env::current_exe().unwrap_or_else(|e| crate::fail(e, ""));
let t0 = std::time::Instant::now();
let kids: Vec<(String, std::process::Child)> = names.iter().map(|n| {
let mut c = std::process::Command::new(&exe);
c.args(["fixtures", file, "--one", n]);
if let Some(k) = keep {
c.args(["--keep", k]);
}
(n.clone(), c.stdout(std::process::Stdio::piped()).spawn().unwrap_or_else(|e| crate::fail(e, "")))
}).collect();
let (mut ok, mut skipped) = (true, 0usize);
for (n, kid) in kids {
let out = kid.wait_with_output().unwrap_or_else(|e| crate::fail(e, ""));
let text = String::from_utf8_lossy(&out.stdout);
for l in text.lines().filter(|l| !l.starts_with("OK: ") && !l.starts_with("ERROR: ")) {
println!("{l}");
}
if text.contains("SKIP ") {
skipped += 1;
}
if !out.status.success() {
ok = false;
if text.trim().is_empty() {
println!("FAIL {n}: it stopped without a result");
}
}
}
println!("OK: {} fixtures run in {:.0} s, {skipped} skipped{}", names.len() - skipped, t0.elapsed().as_secs_f64(), if ok { "." } else { "; at least one FAIL above." });
ok
}
pub fn run(file: &str, only: Option<&str>, keep: Option<&str>) -> bool {
let text = std::fs::read_to_string(file).unwrap_or_else(|e| crate::fail(format!("{file}: {e}"), "Pass the fixtures JSON, e.g. bench/fixtures.json."));
let v: Value = serde_json::from_str(&text).unwrap_or_else(|e| crate::fail(format!("{file}: {e}"), ""));
let base = Path::new(file).parent().unwrap_or(Path::new("."));
let work = keep.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("adom-route-fixtures"));
std::fs::create_dir_all(&work).unwrap_or_else(|e| crate::fail(e, ""));
let (mut ok, mut ran, mut skipped) = (true, 0usize, 0usize);
for fx in v["fixtures"].as_array().cloned().unwrap_or_default() {
let name = fx["name"].as_str().unwrap_or("?").to_string();
if only.map(|o| name != o).unwrap_or(false) {
continue;
}
let board = base.join(fx["board"].as_str().unwrap_or(""));
if !board.exists() {
println!("SKIP {name}: its board is not here ({})", board.display());
skipped += 1;
continue;
}
let t0 = std::time::Instant::now();
let out = one(&fx, &name, &board, base, &work);
let known = fx["known_fail"].as_bool().unwrap_or(false);
let secs = t0.elapsed().as_secs_f64();
ran += 1;
match (out.pass, known) {
(true, false) => println!("PASS {name} ({secs:.1} s)"),
(true, true) => println!("FIXED {name} ({secs:.1} s): it passes now, take its known_fail mark off"),
(false, true) => println!("KNOWN FAIL {name} ({secs:.1} s): a target the router does not meet yet"),
(false, false) => {
println!("FAIL {name} ({secs:.1} s)");
ok = false;
}
}
for l in &out.lines {
println!(" {l}");
}
}
println!("{} {ran} fixtures run, {skipped} skipped.", if ok { "OK:" } else { "ERROR:" });
ok
}
fn one(fx: &Value, name: &str, board: &Path, base: &Path, work: &Path) -> Outcome {
let mut lines: Vec<String> = Vec::new();
let spec: Option<String> = fx["spec"].as_str().map(|s| base.join(s).to_string_lossy().to_string());
let nets: Vec<String> = fx["nets"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect()).unwrap_or_default();
// Strip, with the project beside it so the same rules bind.
let src = Board::read(&board.to_string_lossy()).unwrap_or_else(|e| crate::fail(e, ""));
let stripped = work.join(format!("{name}.stripped.kicad_pcb"));
let routed = work.join(format!("{name}.routed.kicad_pcb"));
std::fs::write(&stripped, crate::write::strip(&src)).unwrap_or_else(|e| crate::fail(e, ""));
let pro = board.with_extension("kicad_pro");
if pro.exists() {
for p in [&stripped, &routed] {
let _ = std::fs::copy(&pro, p.with_extension("kicad_pro"));
}
}
let (b, mut r) = crate::load(&stripped.to_string_lossy(), &spec);
let missing: Vec<&String> = nets.iter().filter(|n| !b.net_ids.contains_key(n.as_str())).collect();
if !missing.is_empty() {
return Outcome { pass: false, lines: vec![format!("not on the board: {missing:?}")] };
}
let mut plan = crate::router::make_plan(&b, &r);
let wanted = |n: NetId| nets.iter().any(|x| x == b.net_name(n));
plan.tasks.retain(|t| wanted(t.net));
plan.pairs.retain(|p| wanted(p.0) && wanted(p.1));
r.time_limit = fx["limit"].as_f64().unwrap_or(60.0);
let mut rt = crate::router::Router::new(&b, r.clone());
rt.only = Some(nets.iter().filter_map(|n| b.net_ids.get(n.as_str()).copied()).collect());
let o = rt.run(&plan);
let mut routes: Vec<&crate::router::Route> = rt.routes.values().collect();
routes.sort_by_key(|x| x.id);
std::fs::write(&routed, crate::write::render(&b, &routes, &r)).unwrap_or_else(|e| crate::fail(e, ""));
let _ = crate::project::write_dru(&stripped.to_string_lossy(), &routed.to_string_lossy(), &rt.rules.pad_rules);
let _ = o;
let _ = std::fs::write(work.join(format!("{name}.log")), rt.log.join("\n") + "\n" + &rt.notes.join("\n"));
lines.push(format!("board kept at {}", routed.display()));
// Judge the written board, as `check` would.
let (rb, rr) = crate::load(&routed.to_string_lossy(), &spec);
let (findings, _) = crate::check::check(&rb, &rr);
let mine = |text: &str| nets.iter().any(|n| text.starts_with(&format!("{n}:")));
let mut pass = true;
let mut say = |ok: bool, what: String, lines: &mut Vec<String>| {
lines.push(format!("{} {what}", if ok { "ok " } else { "FAIL" }));
if !ok {
pass = false;
}
};
for e in fx["expect"].as_array().cloned().unwrap_or_default() {
if e.get("joined").is_some() {
let open: Vec<&str> = findings.iter().filter(|f| f.rule == "unconnected" && mine(&f.text)).map(|f| f.text.as_str()).collect();
say(open.is_empty(), format!("joined: every named net is one piece{}", if open.is_empty() { String::new() } else { format!(" ({})", open.join("; ")) }), &mut lines);
}
if let Some(rules) = e.get("clean").and_then(|x| x.as_array()) {
for rule in rules.iter().filter_map(|x| x.as_str()) {
let hits: Vec<&str> = findings.iter().filter(|f| f.rule == rule && mine(&f.text)).map(|f| f.text.as_str()).collect();
say(hits.is_empty(), format!("clean {rule}{}", if hits.is_empty() { String::new() } else { format!(": {}", hits.join("; ")) }), &mut lines);
}
}
if let Some(w) = e.get("width_near") {
let (net, part) = (w["net"].as_str().unwrap_or(""), w["part"].as_str().unwrap_or(""));
let radius = w["radius"].as_f64().unwrap_or(3.0) * NM;
let id = rb.net_ids.get(net).copied();
let pads: Vec<V> = rb.pads.iter().filter(|p| Some(p.net) == id && rb.footprints[p.fp].reference == part).map(|p| V::from(p.center)).collect();
// Runs, not links: a track shorter than `min_len` (2 mm unless given) is a tie between a
// pad and its via or capacitor, not the run to the part that this check is about.
let min_len = w["min_len"].as_f64().unwrap_or(2.0) * NM;
let near: Vec<i64> = rb.tracks.iter().filter(|t| Some(t.net) == id && hypot(sub(t.a.into(), t.b.into())) >= min_len && pads.iter().any(|c| pt_seg(*c, t.a.into(), t.b.into()) <= radius)).map(|t| t.width).collect();
if near.is_empty() {
say(false, format!("width near {part} on {net}: no track of the net within {:.1} mm of it", radius / NM), &mut lines);
continue;
}
let (lo, hi) = (to_mm(*near.iter().min().unwrap()), to_mm(*near.iter().max().unwrap()));
if let Some(m) = w["max"].as_f64() {
say(hi <= m + 1e-6, format!("width near {part} on {net}: widest {hi:.2} mm, at most {m:.2} mm allowed"), &mut lines);
}
if let Some(m) = w["min"].as_f64() {
say(lo >= m - 1e-6, format!("width near {part} on {net}: narrowest {lo:.2} mm, at least {m:.2} mm wanted"), &mut lines);
}
}
if let Some(d) = e.get("drop_max") {
let (net, load, pct) = (d["net"].as_str().unwrap_or(""), d["load"].as_str().unwrap_or(""), d["pct"].as_f64().unwrap_or(0.25));
let found = rr.rails.iter().find(|(k, _)| k.eq_ignore_ascii_case(net)).and_then(|(_, rail)| {
let id = rb.net_ids.iter().find(|(k, _)| k.eq_ignore_ascii_case(net)).map(|(_, v)| *v)?;
let v = rail.volts?;
crate::current::rail_drops(&rb, &rr, id, rail).into_iter().find(|x| x.load == load).map(|x| (x.volts, v))
});
match found {
Some((volts, v)) => say(volts / v * 100.0 <= pct + 1e-6, format!("drop on {net} to {load}: {:.1} mV, {:.2}% of {v} V, at most {pct:.2}% allowed", volts * 1000.0, volts / v * 100.0), &mut lines),
None => say(false, format!("drop on {net} to {load}: not found (no rail, voltage or path)"), &mut lines),
}
}
if let Some(m) = e.get("max_vias").and_then(|x| x.as_u64()) {
let n = rb.vias.iter().filter(|v| nets.iter().any(|x| x == rb.net_name(v.net))).count();
say(n as u64 <= m, format!("vias on the named nets: {n}, at most {m} allowed"), &mut lines);
}
if let Some(p) = e.get("pair_coupled") {
let (want, min) = (p["pair"].as_str().unwrap_or(""), p["min_pct"].as_f64().unwrap_or(80.0));
match crate::compare::pairs(&rb, &rr).into_iter().find(|x| x.0 == want) {
Some((_, lp, _, c, _)) => {
let pct = if lp > 0.0 { 100.0 * c / lp } else { 0.0 };
say(pct >= min, format!("pair {want}: {pct:.0}% coupled, at least {min:.0}% wanted"), &mut lines);
}
None => say(false, format!("pair {want}: not found on the routed board"), &mut lines),
}
}
}
Outcome { pass, lines }
}