← Commit history

Resolve repeated physical pads in branch-current screening; preserve explicit currents and limits

John Lauer ·40e205e6dd · ·parent e407ac4
9 files changed +94−31
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.83"+version = "0.1.84" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.83"+version = "0.1.84" dependencies = [  "aiflow-board",  "serde",@@ -34,7 +34,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.83"+version = "0.1.84" dependencies = [  "roxmltree",  "serde",@@ -43,7 +43,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.83"+version = "0.1.84" dependencies = [  "aiflow-board",  "serde",@@ -52,7 +52,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.83"+version = "0.1.84" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -62,7 +62,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.83"+version = "0.1.84" dependencies = [  "aiflow-board",  "serde",@@ -71,7 +71,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.83"+version = "0.1.84" dependencies = [  "aiflow-board",  "serde",@@ -80,7 +80,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.83"+version = "0.1.84" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -90,7 +90,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.83"+version = "0.1.84" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -100,7 +100,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.83"+version = "0.1.84" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.83"+version = "0.1.84" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+1−1
@@ -392,4 +392,4 @@ Derive continuous heat cases from the actual voltage/current/power envelope and  ### Repeated physical pads in explicit current boundaries -When multiple physical lands share one logical pin number, use the reviewed Fields physical-pad selector (`at: [x_mm, y_mm]`, adom/adom-fields PR10 and dependencies). Declare each branch current and its assumption; never silently divide the load to obtain a pass. AIFlow matches each reported physical boundary once, including its coordinates, and refuses missing/changed selectors. Recheck coordinates after moving a footprint. Preserve owner review and shared runtimes; source support is not a deployed release or package-resistance model.+When multiple physical lands share one logical pin number, use the reviewed Fields physical-pad selector (`at: [x_mm, y_mm]`, adom/adom-fields PR10 and dependencies). Declare each branch current and its assumption; never silently divide the load to obtain a pass. AIFlow resolves and injects current at each declared physical pad, then matches each reported boundary once, including its coordinates. Missing, ambiguous, duplicate or changed selectors refuse; integer and decimal coordinate spellings are equivalent. Recheck coordinates after moving a footprint. Preserve owner review and shared runtimes; source support is not a deployed release or package-resistance model.
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/aiflow-analyze/src/branches.rs+63−14
@@ -182,27 +182,39 @@ pub fn screen(board: &Board, net: &str, load: &Value, spec: &Value) -> Result<Va             .as_f64()             .filter(|v| v.is_finite())             .ok_or("terminal amps must be finite")?;-        if declarations.insert((reference, pad), a).is_some() {-            return Err("duplicate terminal assignment".into());-        }-        if pads+        let at = if row["at"].is_null() {+            None+        } else {+            let xy = row["at"].as_array().filter(|xy| xy.len() == 2)+                .ok_or("terminal at must be two finite board coordinates")?;+            Some((xy[0].as_f64().filter(|v| v.is_finite())+                .ok_or("terminal at must be two finite board coordinates")?,+                xy[1].as_f64().filter(|v| v.is_finite())+                .ok_or("terminal at must be two finite board coordinates")?))+        };+        let matches: Vec<_> = pads             .iter()-            .filter(|p| p.reference == reference && p.name == pad)-            .count()-            != 1-        {+            .enumerate()+            .filter(|(_, p)| p.reference == reference && p.name == pad+                && 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 {             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());+        }         sources += a.max(0.);         sinks += (-a).max(0.);     }     if (sources - amps).abs() > 1e-8 * amps.max(1.) || (sinks - amps).abs() > 1e-8 * amps.max(1.) {         return Err("sources and sinks must each equal load amps".into());     }-    for p in &pads {-        if !declarations.contains_key(&(p.reference.as_str(), p.name.as_str())) {+    for (i, p) in pads.iter().enumerate() {+        if !declarations.contains_key(&i) {             return Err(format!(                 "declare current for {}.{} (zero for an unloaded pad)",                 p.reference, p.name@@ -334,9 +346,10 @@ pub fn screen(board: &Board, net: &str, load: &Value, spec: &Value) -> Result<Va     }     let active: Vec<_> = pads         .iter()-        .filter(|p| declarations[&(p.reference.as_str(), p.name.as_str())] != 0.)+        .enumerate()+        .filter(|(i, _)| declarations[i] != 0.)         .collect();-    let through = active.iter().filter(|p| p.thru).count();+    let through = active.iter().filter(|(_, p)| p.thru).count();     if through > 4 {         return Err(             "more than four active through-pad terminals exceeds exhaustive layer cases".into(),@@ -348,7 +361,7 @@ pub fn screen(board: &Board, net: &str, load: &Value, spec: &Value) -> Result<Va     for mask in 0..(1usize << through) {         let mut injection = BTreeMap::new();         let mut j = 0;-        for p in &active {+        for (i, p) in &active {             let l = if p.thru {                 let l = (mask >> j) & 1;                 j += 1;@@ -360,7 +373,7 @@ pub fn screen(board: &Board, net: &str, load: &Value, spec: &Value) -> Result<Va             };             *injection                 .entry(root(key(l, (p.x, p.y)), &parents))-                .or_insert(0.) += declarations[&(p.reference.as_str(), p.name.as_str())];+                .or_insert(0.) += declarations[i];         }         let currents = solve(&edges, &injection)?;         let mut conductors = Vec::new();@@ -462,4 +475,40 @@ mod geometry_tests {         s["branchCurrentModel"] = json!({});         assert!(screen(&board(2.), "V", &s["loads"]["V"], &s).is_err());     }+    fn repeated_pad_case() -> (Board, Value) {+        let mut text = board(2.).text.replace("(width 0.25)", "(width 2)");+        let old = r#"(property "Reference" "F1") (pad "1" smd circle (at 0 0) (size 1 1) (layers "F.Cu") (net 1 "V"))"#;+        let new = format!("{old} {}", r#"(pad "1" smd circle (at 0 10) (size 1 1) (layers "F.Cu") (net 1 "V"))"#);+        assert!(text.contains(old));+        text = text.replace(old, &new);+        text.pop();+        text.push_str(r#"(segment (start 2 12) (end 12 12) (width 2) (layer "F.Cu") (net 1)))"#);+        let mut s = spec();+        s["loads"]["V"]["terminals"][1] = json!({"reference":"F1","pad":"1","amps":-1.5,"at":[12,2]});+        s["loads"]["V"]["terminals"].as_array_mut().unwrap()+            .push(json!({"reference":"F1","pad":"1","amps":-1.5,"at":[12,12]}));+        (Board::from_text("repeated.kicad_pcb", text).unwrap(), s)+    }+    #[test]+    fn repeated_physical_pads_accept_explicit_centers_and_preserve_balance() {+        let (b, mut s) = repeated_pad_case();+        assert_eq!(screen(&b, "V", &s["loads"]["V"], &s).unwrap()["status"], "pass");+        s["loads"]["V"]["terminals"].as_array_mut().unwrap().reverse();+        assert_eq!(screen(&b, "V", &s["loads"]["V"], &s).unwrap()["status"], "pass");+        s["loads"]["V"]["terminals"][0]["amps"] = json!(-1.4);+        assert!(screen(&b, "V", &s["loads"]["V"], &s).unwrap_err().contains("sources and sinks"));+    }+    #[test]+    fn repeated_physical_pads_refuse_missing_duplicate_or_invalid_selectors() {+        let (b, s) = repeated_pad_case();+        for at in [Value::Null, json!([12,12]), json!([12,3]), json!(["x",2]), json!([12])] {+            let mut bad = s.clone();+            bad["loads"]["V"]["terminals"][1]["at"] = at;+            assert!(screen(&b, "V", &bad["loads"]["V"], &bad).is_err());+        }+        let mut missing = s.clone();+        missing["loads"]["V"]["terminals"].as_array_mut().unwrap().pop();+        missing["loads"]["V"]["terminals"][1]["amps"] = json!(-3);+        assert!(screen(&b, "V", &missing["loads"]["V"], &missing).unwrap_err().contains("declare current"));+    } }
crates/aiflow-analyze/src/lib.rs+12−3
@@ -320,14 +320,21 @@ pub fn field_screen(fields: &Value, spec: &Value, board_bytes: &[u8], spec_bytes                 if terminals["mode"] != "explicit" { return Err(format!("Fields {name} used heuristic terminals; solve the declared explicit pins")); }                 let wanted = value["terminals"].as_array().ok_or("branch model has no terminal array")?;                 let actual = terminals["pins"].as_array().ok_or("Fields has no explicit terminal pins")?;+                let same_at = |a: &Value, b: &Value| {+                    if a.is_null() && b.is_null() { return true; }+                    match (a.as_array(), b.as_array()) {+                        (Some(a), Some(b)) if a.len() == 2 && b.len() == 2 =>+                            a.iter().zip(b).all(|(x, y)| x.as_f64().is_some_and(|x|+                                x.is_finite() && y.as_f64() == Some(x))),+                        _ => false,+                    }+                };                 let mut matched = vec![false; actual.len()];                 let same_boundary = wanted.len() == actual.len() && wanted.iter().all(|w| {                     let found = actual.iter().enumerate().position(|(i, a)| !matched[i]                         && a["reference"] == w["reference"] && a["pad"] == w["pad"]                         && a["amps"].as_f64() == w["amps"].as_f64()-                        && a["at"] == w["at"]-                        && (w["at"].is_null() || w["at"].as_array().is_some_and(|xy|-                            xy.len() == 2 && xy.iter().all(|v| v.as_f64().is_some_and(f64::is_finite)))));+                        && same_at(&a["at"], &w["at"]));                     if let Some(i) = found { matched[i] = true; true } else { false }                 });                 if !same_boundary {@@ -447,6 +454,8 @@ mod field_screen_tests {         field_screen(&f,&spec,b"board",b"spec","current").unwrap();         f["terminalAssignments"]["VCC"]["pins"].as_array_mut().unwrap().reverse();         field_screen(&f,&spec,b"board",b"spec","current").unwrap();+        f["terminalAssignments"]["VCC"]["pins"][1]["at"]=json!([1,2]);+        field_screen(&f,&spec,b"board",b"spec","current").unwrap();         for at in [Value::Null,json!([1.0,4.0]),json!([1.0,2.1])] {             let mut bad=f.clone();bad["terminalAssignments"]["VCC"]["pins"][1]["at"]=at;             assert!(field_screen(&bad,&spec,b"board",b"spec","current").is_err());
docs/release-0.1.84.mdadded+5
@@ -0,0 +1,5 @@+# 0.1.84: repeated physical pads in branch-current screening++The resistive branch calculator now resolves optional board-coordinate selectors and injects each physical terminal once. This completes the repeated-pad path introduced in 0.1.83: a two-anode PowerDI5 package can be screened without omitting a terminal or bypassing a gate. Missing, ambiguous, repeated or malformed selectors refuse; all physical pads still require a declared current, and signed balance is unchanged. Field-report matching treats integer and decimal spellings of the same coordinates equally.++No current or thermal thresholds changed. Branch-current sharing is an explicit input assumption, not a package resistance model.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.83",+  "version": "0.1.84",   "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 today; Altium, Fusion and 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.",
skills/adom-aiflow/SKILL.md+1−1
@@ -358,4 +358,4 @@ Derive continuous heat cases from the actual voltage/current/power envelope and  ### Repeated physical pads in explicit current boundaries -When multiple physical lands share one logical pin number, use the reviewed Fields physical-pad selector (`at: [x_mm, y_mm]`, adom/adom-fields PR10 and dependencies). Declare each branch current and its assumption; never silently divide the load to obtain a pass. AIFlow matches each reported physical boundary once, including its coordinates, and refuses missing/changed selectors. Recheck coordinates after moving a footprint. Preserve owner review and shared runtimes; source support is not a deployed release or package-resistance model.+When multiple physical lands share one logical pin number, use the reviewed Fields physical-pad selector (`at: [x_mm, y_mm]`, adom/adom-fields PR10 and dependencies). Declare each branch current and its assumption; never silently divide the load to obtain a pass. AIFlow resolves and injects current at each declared physical pad, then matches each reported boundary once, including its coordinates. Missing, ambiguous, duplicate or changed selectors refuse; integer and decimal coordinate spellings are equivalent. Recheck coordinates after moving a footprint. Preserve owner review and shared runtimes; source support is not a deployed release or package-resistance model.