← All Pull Requests

AI Flow 0.2.55: the branch screen takes a pad drawn as coincident lands #70

Merged Released in 0.2.55 on 5 Nov 2026 opened by Colby Knox
Merges aiflow-lane-0.2.55 → main

The branch screen takes a pad drawn as several lands at one centre, such as KiCad's DGN0008B exposed pad 9. This matches adom-fields 0.10.7 (adom-fields#26): the largest land takes the current, and the others count as declared. A pad number whose lands are in different places still needs at.

Tested: 601 Rust tests and the Python tests pass. Branch screen on a board built with the real KiCad footprint: 0.2.54 refused U101.9, this build passes it.

🤖 Generated with Claude Code

Diff Skip to comments

Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.2.54"+version = "0.2.55" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.2.54"+version = "0.2.55" dependencies = [  "aiflow-board",  "serde",@@ -35,7 +35,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.2.54"+version = "0.2.55" dependencies = [  "roxmltree",  "serde",@@ -44,7 +44,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.2.54"+version = "0.2.55" dependencies = [  "aiflow-board",  "serde",@@ -53,7 +53,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.2.54"+version = "0.2.55" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.2.54"+version = "0.2.55" dependencies = [  "aiflow-board",  "serde",@@ -72,7 +72,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.2.54"+version = "0.2.55" dependencies = [  "aiflow-board",  "serde",@@ -81,7 +81,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.2.54"+version = "0.2.55" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.2.54"+version = "0.2.55" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.2.54"+version = "0.2.55" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.2.54"+version = "0.2.55" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
crates/aiflow-analyze/src/branches.rs+37−3
@@ -274,13 +274,22 @@ pub fn screen(board: &Board, net: &str, load: &Value, spec: &Value) -> Result<Va                 && at.is_none_or(|(x, y)| (p.x-x).abs() <= 1e-6 && (p.y-y).abs() <= 1e-6))             .map(|(i, _)| i)             .collect();-        if matches.len() != 1 {+        // adom-fields#26: one pad number on several lands at one centre (a KiCad exposed pad drawn as a+        // 2 x 3 mm and a 1.6 x 2.94 mm land) is one piece of copper. The largest land takes the current,+        // as in Fields; the others count as declared, with no current of their own.+        let mut matches = matches;+        let coincident = matches.len() > 1 && matches.iter().all(|&i| (pads[i].x - pads[matches[0]].x).abs() <= 1e-6 && (pads[i].y - pads[matches[0]].y).abs() <= 1e-6);+        if coincident {+            matches.sort_by(|&i, &j| (pads[j].size.0 * pads[j].size.1).partial_cmp(&(pads[i].size.0 * pads[i].size.1)).unwrap_or(std::cmp::Ordering::Equal));+        } else if matches.len() != 1 {             return Err(format!(                 "{reference}.{pad}: terminal is missing, ambiguous or on another net"             ));         }-        if declarations.insert(matches[0], a).is_some() {-            return Err("duplicate physical terminal assignment".into());+        for (k, &i) in matches.iter().enumerate() {+            if declarations.insert(i, if k == 0 { a } else { 0. }).is_some() {+                return Err("duplicate physical terminal assignment".into());+            }         }         sources += a.max(0.);         sinks += (-a).max(0.);@@ -577,6 +586,31 @@ mod geometry_tests {     fn spec() -> Value {         json!({"copperUm":35,"branchCurrentModel":{"boardThicknessMm":1.6,"barrelPlatingUm":25,"maxBarrelDensityAperMm2":60},"loads":{"V":{"amps":3.001,"maxRiseC":10,"terminals":[{"reference":"J1","pad":"1","amps":3.001},{"reference":"F1","pad":"1","amps":-3},{"reference":"R1","pad":"1","amps":-0.001}]}}})     }+    /// adom-fields#26 (Noah, TI DGN0008B on am32-esc-v1): pad 9 is two lands at one centre plus via pads+    /// around it. Named with `at`, the coincident lands are one terminal; a pad number on lands at different+    /// places still needs `at` to say which.+    #[test]+    fn coincident_lands_of_one_pad_are_one_terminal() {+        let text = r#"(kicad_pcb (version 20260101) (layers (0 "F.Cu" signal) (31 "B.Cu" signal)) (net 1 "V")+  (gr_rect (start 0 0) (end 20 20) (layer "Edge.Cuts") (width 0.05))+  (footprint "A" (layer "F.Cu") (at 2 2) (property "Reference" "J1") (pad "1" smd circle (at 0 0) (size 1 1) (layers "F.Cu") (net 1 "V")))+  (footprint "U" (layer "F.Cu") (at 12 2) (property "Reference" "U1")+    (pad "9" smd rect (at 0 0) (size 2 3) (layers "F.Cu") (net 1 "V"))+    (pad "9" smd rect (at 0 0) (size 1.6 2.94) (layers "F.Cu") (net 1 "V"))+    (pad "9" smd circle (at 0.5 1) (size 0.5 0.5) (layers "F.Cu") (net 1 "V")))+  (segment (start 2 2) (end 12 2) (width 2) (layer "F.Cu") (net 1)))"#;+        let b = Board::from_text("test.kicad_pcb", text.to_string()).unwrap();+        let mut s = spec();+        s["loads"]["V"] = json!({"amps": 3, "maxRiseC": 10, "terminals": [+            {"reference": "J1", "pad": "1", "amps": 3},+            {"reference": "U1", "pad": "9", "at": [12.0, 2.0], "amps": -3},+            {"reference": "U1", "pad": "9", "at": [12.5, 3.0], "amps": 0}]});+        let r = screen(&b, "V", &s["loads"]["V"], &s).unwrap();+        assert_eq!(r["status"], "pass", "{r}");+        // without `at`, pad 9's lands are in two places: still ambiguous+        s["loads"]["V"]["terminals"][1] = json!({"reference": "U1", "pad": "9", "amps": -3});+        assert!(screen(&b, "V", &s["loads"]["V"], &s).unwrap_err().contains("ambiguous"));+    }     #[test]     fn sense_branch_does_not_take_main_load_but_narrow_main_path_still_fails() {         let s = spec();
docs/release-0.2.55.mdadded+7
@@ -0,0 +1,7 @@+# AI Flow 0.2.55 (2026-11-05)++**The branch screen takes a pad drawn as several lands at one centre (adom-fields#26).** KiCad's stock `Texas_DGN0008B` footprint draws its exposed pad 9 as a 2 x 3 mm and a 1.6 x 2.94 mm land at the centre, plus six via pads. A load terminal naming pad 9 with `at` on the centre was refused as "missing, ambiguous or on another net", so the pad had to be left out with a spec amendment. Coincident lands of one pad now count as one terminal: the largest takes the current, as in adom-fields 0.10.7, and the others count as declared. A pad number whose lands are in different places still needs `at` to say which.++**Checked:** the branch screen on a test board built with that real KiCad footprint (pad 9 at the centre, its six via pads at 0 A). 0.2.54 refused `U101.9`; this build passes it. A unit test covers both cases.++**adom-fields 0.10.7's `ground` spec key** needs nothing from AI Flow, which hands its spec to Fields unchanged. Boards with more than one ground net, or none named GND, can get different Fields results with 0.10.7.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.54",+  "version": "0.2.55",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.54",+  "version": "0.2.55",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",

Comments

No comments yet.

Log in to comment.