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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
//! `grade`: runs the checker over the designer's boards and holds it to an expected-results file. His rule
//! (AI_Mistakes_Log R1): a check gates only after it passes the reference boards and fails the layouts he
//! rejected. Findings his own finished boards carry stay listed as known, with their coordinates,
//! for him to decide; a new one fails the grade, and so does a known one that has gone away.
use crate::board::Board;
use crate::check::{self, Finding, GEOMETRY_RULES};
use crate::classify;
use crate::project;
use crate::rules::Rules;
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::path::Path;
const NEAR_MM: f64 = 0.02;
pub struct Outcome {
pub ok: bool,
pub lines: Vec<String>,
pub json: Value,
}
fn strs(v: Option<&Value>) -> Vec<String> {
v.and_then(|x| x.as_array()).map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect()).unwrap_or_default()
}
pub fn run(file: &str) -> Result<Outcome, String> {
let text = std::fs::read_to_string(file).map_err(|e| format!("{file}: {e}"))?;
let v: Value = serde_json::from_str(&text).map_err(|e| format!("{file}: {e}"))?;
let base = Path::new(file).parent().unwrap_or(Path::new("."));
let boards = v.get("boards").and_then(|b| b.as_array()).ok_or("no boards in the grade file")?;
let mut ok = true;
let mut lines = Vec::new();
let mut out = Vec::new();
for e in boards {
let name = e.get("name").and_then(|x| x.as_str()).unwrap_or("?");
let rel = e.get("board").and_then(|x| x.as_str()).ok_or("board entry without a path")?;
let path = base.join(rel);
// A synthetic fixture is the reference board with copper added, in memory: `insert` is appended to the
// file's text, so every check is proven to fire on a fault placed in a board it passes.
let inserts = strs(e.get("insert"));
let board = if inserts.is_empty() {
Board::read(path.to_str().unwrap_or(rel))?
} else {
let t = std::fs::read_to_string(&path).map_err(|e| format!("{rel}: {e}"))?;
let end = t.rfind(')').ok_or("board file has no closing bracket")?;
Board::parse(format!("{}\n{}\n)\n", &t[..end], inserts.join("\n")))?
};
let mut rules = match e.get("spec").and_then(|x| x.as_str()) {
Some(s) => Rules::load(base.join(s).to_str().unwrap_or(s))?,
None => Rules::default(),
};
let pro = e.get("project").and_then(|x| x.as_str()).map(|p| base.join(p)).unwrap_or_else(|| path.with_extension("kicad_pro"));
project::apply_files(&pro, &path.with_extension("kicad_dru"), &mut rules);
for k in strs(e.get("kelvin")) {
rules.kelvin.push(k);
}
rules.bind(&board);
classify::find_pairs(&board, &mut rules);
let (f, s) = check::check(&board, &rules);
let errors: Vec<&Finding> = f.iter().filter(|x| x.severity == "error").collect();
let count = |rule: &str| errors.iter().filter(|x| x.rule == rule).count();
let mut fails: Vec<String> = Vec::new();
let known_at = e.get("knownAt").and_then(|x| x.as_array()).cloned().unwrap_or_default();
let listed: Vec<(String, f64, f64)> = known_at
.iter()
.filter_map(|k| Some((k.get("rule")?.as_str()?.to_string(), k.pointer("/at/0")?.as_f64()?, k.pointer("/at/1")?.as_f64()?)))
.collect();
let known_count: BTreeMap<String, usize> = e.get("knownCount").and_then(|x| x.as_object()).map(|o| o.iter().map(|(k, c)| (k.clone(), c.as_u64().unwrap_or(0) as usize)).collect()).unwrap_or_default();
let is_known = |x: &Finding| known_count.contains_key(x.rule) || listed.iter().any(|(r, a, b)| r == x.rule && (a - x.at.0).abs() <= NEAR_MM && (b - x.at.1).abs() <= NEAR_MM);
// A finished board: connected, and every geometry rule clean apart from what is listed.
if e.get("finished").and_then(|x| x.as_bool()).unwrap_or(false) {
if s.nets_open != 0 {
fails.push(format!("{} nets open; KiCad and the reference board say 0", s.nets_open));
}
for x in errors.iter().filter(|x| GEOMETRY_RULES.contains(&x.rule) && !is_known(x)) {
fails.push(format!("new finding {} at ({:.2}, {:.2}): {}", x.rule, x.at.0, x.at.1, x.text));
}
for (r, a, b) in &listed {
if !f.iter().any(|x| x.rule == r && (a - x.at.0).abs() <= NEAR_MM && (b - x.at.1).abs() <= NEAR_MM) {
fails.push(format!("known {r} at ({a:.2}, {b:.2}) is no longer found: the list is stale or the check changed"));
}
}
for (r, c) in &known_count {
let got = f.iter().filter(|x| x.rule == r).count();
if got != *c {
fails.push(format!("known {r}: expected {c}, found {got}"));
}
}
}
for r in strs(e.get("mustFail")) {
if count(&r) == 0 {
fails.push(format!("{r} must fail on this layout and did not"));
}
}
for r in strs(e.get("mustPass")) {
if count(&r) != 0 {
fails.push(format!("{r} must pass on this layout and found {}", count(&r)));
}
}
for r in strs(e.get("mustWarn")) {
if !f.iter().any(|x| x.rule == r && x.severity != "error") {
fails.push(format!("{r} must warn on this layout and did not"));
}
}
// A fixture must add what it says and nothing else: the check fires on the fault it was
// built for, and the board's other findings don't move.
if let Some(add) = e.get("adds").and_then(|x| x.as_object()) {
let plain = Board::read(path.to_str().unwrap_or(rel))?;
let (f0, _) = check::check(&plain, &rules);
let tally = |v: &[Finding]| {
let mut m: BTreeMap<String, i64> = BTreeMap::new();
for x in v.iter().filter(|x| x.severity == "error") {
*m.entry(x.rule.to_string()).or_insert(0) += 1;
}
m
};
let (t0, t1) = (tally(&f0), tally(&f));
let mut rules_seen: Vec<&String> = t0.keys().chain(t1.keys()).collect();
rules_seen.sort();
rules_seen.dedup();
for r in rules_seen {
let d = t1.get(r).copied().unwrap_or(0) - t0.get(r).copied().unwrap_or(0);
let want = add.get(r).and_then(|x| x.as_i64()).unwrap_or(0);
if d != want {
fails.push(format!("the edit moved {r} by {d}, expected {want}"));
}
}
for (r, w) in add {
if !t1.contains_key(r.as_str()) && w.as_i64().unwrap_or(0) != 0 {
fails.push(format!("{r} never fired on the edit"));
}
}
}
let mut per: BTreeMap<&str, usize> = BTreeMap::new();
for x in &f {
*per.entry(x.rule).or_insert(0) += 1;
}
let known_n = errors.iter().filter(|x| is_known(x)).count();
let pass = fails.is_empty();
ok &= pass;
lines.push(format!("{} {name}: {} errors ({} known), {} warnings, {} nets open. {:?}", if pass { "PASS" } else { "FAIL" }, errors.len(), known_n, f.len() - errors.len(), s.nets_open, per));
for x in &fails {
lines.push(format!(" {x}"));
}
if let Some(k) = e.get("kicad") {
lines.push(format!(" KiCad DRC, last run by hand: {k}"));
}
out.push(json!({"name": name, "pass": pass, "errors": errors.len(), "known": known_n, "warnings": f.len() - errors.len(), "netsOpen": s.nets_open, "perRule": per, "fails": fails}));
}
Ok(Outcome { ok, lines, json: json!({"ok": ok, "boards": out}) })
}