← Commit history

AI Flow 0.2.76: offline pours count in analyze (#202)

Colby Knox ·50263890aa · ·parent 33babc2
Co-Authored-By: Claude Opus 5.5 <[email protected]>
8 files changed +77−14
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.2.75"+version = "0.2.76" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.2.75"+version = "0.2.76" dependencies = [  "aiflow-board",  "serde",@@ -35,7 +35,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.2.75"+version = "0.2.76" dependencies = [  "roxmltree",  "serde",@@ -44,7 +44,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.2.75"+version = "0.2.76" dependencies = [  "aiflow-board",  "serde",@@ -53,7 +53,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.2.75"+version = "0.2.76" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.2.75"+version = "0.2.76" dependencies = [  "aiflow-board",  "serde",@@ -72,7 +72,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.2.75"+version = "0.2.76" dependencies = [  "aiflow-board",  "serde",@@ -81,7 +81,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.2.75"+version = "0.2.76" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.2.75"+version = "0.2.76" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.2.75"+version = "0.2.76" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.2.75"+version = "0.2.76" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+6−1
@@ -5557,7 +5557,12 @@ fn main() {             let plan_v = plan_with_board_copper(&read_json(&planp), &board_now);             let spec = spec_of(&r);             let zsp = dir.join("zone-state.json");-            let zs = if zsp.is_file() { Some(read_json(&zsp.display().to_string())) } else { None };+            let zs = if zsp.is_file() { Some(read_json(&zsp.display().to_string())) } else {+                // issue #202 (Noah): no live measurement on this run (offline pours, adopted): read the pours from the saved board's fills+                let z = aiflow_analyze::pour_cover::zone_state_from_board(&board_now);+                let n = z["zones"].as_array().map(|a| a.len()).unwrap_or(0);+                if n > 0 { eprintln!("no live copper measurement on this run (no zone-state.json): the pours are read from the saved board's own fills ({n} filled zone(s))"); Some(z) } else { None }+            };             let name = format!("analyze-{what}");             r.stage_start(&name, Some("binary"), None);             let (mut heuristic_passed, mut res) = match what.as_str() { "current" => aiflow_analyze::analyze_current_on_board(&plan_v, &spec, zs.as_ref(), Some(&pad_layers_of(&board_now)), &board_now), "thermal" => aiflow_analyze::analyze_thermal_on_board(&plan_v, &spec, zs.as_ref(), &board_now), other => err(&format!("analyze {other}: use current, thermal or impedance"), &[]) };
crates/aiflow-analyze/src/pour_cover.rs+49
@@ -73,6 +73,34 @@ fn net_fills(board: &Board, net: &str) -> Vec<Fill> {     out } +/// Issue #202 (Noah): offline runs (pours written with pcbnew in a container and adopted with `verify-pours+/// --from-board`, no live editor): the zone state `measure` reads from the editor, built from the saved board's+/// own filled polygons instead, so `analyze current|thermal` see the pours the board really carries.+/// Only zones with saved fills count (an unfilled zone gives nothing); keepouts are skipped.+pub fn zone_state_from_board(board: &Board) -> serde_json::Value {+    let mut zones = Vec::new();+    for z in board.root.find_all("zone") {+        if z.find("keepout").is_some() { continue; }+        let net = board.item_net(z);+        if net.is_empty() { continue; }+        let mut fills = Vec::new();+        let mut layers: Vec<String> = Vec::new();+        for f in z.find_all("filled_polygon") {+            let Some(layer) = f.find("layer").and_then(|l| l.value()) else { continue };+            let poly = pts_of(f);+            if poly.len() < 3 { continue; }+            let mut a = 0.0;+            for i in 0..poly.len() { let (p, q) = (poly[i], poly[(i + 1) % poly.len()]); a += p.0 * q.1 - q.0 * p.1; }+            fills.push(serde_json::json!({"layer": layer, "areaMm2": ((a / 2.0).abs() * 10.0).round() / 10.0}));+            if !layers.iter().any(|l| l == layer) { layers.push(layer.to_string()); }+        }+        if fills.is_empty() { continue; }+        let name = z.find("name").and_then(|n| n.value()).unwrap_or("").to_string();+        zones.push(serde_json::json!({"type": "copper", "net": net, "name": name, "layers": layers, "fills": fills}));+    }+    serde_json::json!({"source": "saved-board", "zones": zones})+}+ fn in_box(p: Pt, b: (f64, f64, f64, f64), m: f64) -> bool { p.0 >= b.0 - m && p.0 <= b.2 + m && p.1 >= b.1 - m && p.1 <= b.3 + m }  fn pad_on(p: &aiflow_board::Pad, layer: &str) -> bool {@@ -382,4 +410,25 @@ mod tests {         let how = check(&Board::from_text("t.kicad_pcb", t).unwrap(), "V", &serde_json::Value::Null, 1.5, 10.0, 35.0).unwrap();         assert!(how.contains("B.Cu+F.Cu pour reaches all 2"), "{how}");     }+    /// Issue #202: an offline run's pours come from the saved board's fills; an unfilled zone and a keepout give nothing.+    #[test]+    fn the_zone_state_of_an_offline_run_comes_from_the_saved_fills() {+        let b = Board::from_text("t.kicad_pcb", r#"(kicad_pcb (version 20240108) (generator "t")+  (layers (0 "F.Cu" signal) (31 "B.Cu" signal) (44 "Edge.Cuts" user))+  (net 0 "") (net 1 "+5V") (net 2 "GND")+  (gr_rect (start 0 0) (end 20 20) (layer "Edge.Cuts") (stroke (width 0.1) (type default)))+  (zone (net 1) (net_name "+5V") (layer "F.Cu") (name "P5") (polygon (pts (xy 0 0) (xy 10 0) (xy 10 10) (xy 0 10)))+    (filled_polygon (layer "F.Cu") (pts (xy 0 0) (xy 10 0) (xy 10 10) (xy 0 10))))+  (zone (net 2) (net_name "GND") (layer "B.Cu") (polygon (pts (xy 0 0) (xy 20 0) (xy 20 20) (xy 0 20))))+  (zone (net 2) (net_name "GND") (layer "F.Cu") (keepout (tracks not_allowed)) (polygon (pts (xy 0 0) (xy 1 0) (xy 1 1)))+    (filled_polygon (layer "F.Cu") (pts (xy 0 0) (xy 1 0) (xy 1 1))))+)"#.to_string()).unwrap();+        let z = zone_state_from_board(&b);+        let zones = z["zones"].as_array().unwrap();+        assert_eq!(zones.len(), 1, "the unfilled GND zone and the keepout give nothing: {z}");+        assert_eq!(zones[0]["net"], "+5V");+        assert_eq!(zones[0]["fills"][0]["areaMm2"], 100.0);+        assert_eq!(zones[0]["layers"], serde_json::json!(["F.Cu"]));+    }+ }
docs/release-0.2.76.mdadded+9
@@ -0,0 +1,9 @@+# AI Flow 0.2.76++- **#202 (Noah): an offline run's pours count in `analyze current` and `analyze thermal`.** The analyses read+  the pours only from `zone-state.json`, which only `measure` writes, and `measure` needs the live editor. A run+  whose pours were written offline (pcbnew in a container) and adopted (`verify-pours --from-board --adopt`) had+  none, so every poured net read as "no copper" or "no pour", and `finish-guard` could never complete. With no+  live measurement, the zone state is now built from the saved board's own filled zones (net, layers, area per+  fill; keepouts and unfilled zones skipped), and the analysis says so. A live measurement still wins when there+  is one. Patch by Noah, with a unit test added.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.75",+  "version": "0.2.76",   "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.75",+  "version": "0.2.76",   "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.",