app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
← Commit history
Resolve repeated physical pads in branch-current screening; preserve explicit currents and limits
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.