← Commit history

Recognize actual rectangular pad contact in explicit-current branch screening; retain disconnected-net refusal

John Lauer ·96ef829d00 · ·parent 44e8e3d
2 files changed +41−21
crates/aiflow-analyze/examples/branch_screen.rs+10−19
@@ -1,22 +1,13 @@-use serde_json::{json, Value};+//! Read-only diagnostic for the optional explicit-terminal branch model.+use aiflow_board::Board;+use serde_json::Value; fn main() {-    let a: Vec<_> = std::env::args().collect();-    let b = aiflow_board::Board::load(&a[1]).unwrap();-    let spec: Value = serde_json::from_slice(&std::fs::read(&a[2]).unwrap()).unwrap();-    let mut result = Vec::new();-    for (net, load) in spec["loads"].as_object().unwrap() {-        if load["currentModel"] == "resistive-branches" {-            match aiflow_analyze::branches::screen(&b, net, load, &spec) {-                Ok(r) => result.push(r),-                Err(e) => {-                    eprintln!("{net}: {e}");-                    std::process::exit(1);-                }-            }-        }+    let args: Vec<String> = std::env::args().collect();+    assert_eq!(args.len(), 4, "usage: branch_screen BOARD SPEC NET");+    let board = Board::from_text(&args[1], std::fs::read_to_string(&args[1]).unwrap()).unwrap();+    let spec: Value = serde_json::from_str(&std::fs::read_to_string(&args[2]).unwrap()).unwrap();+    match aiflow_analyze::branches::screen(&board, &args[3], &spec["loads"][&args[3]], &spec) {+        Ok(value) => println!("{}", serde_json::to_string_pretty(&value).unwrap()),+        Err(error) => { eprintln!("{error}"); std::process::exit(2); }     }-    println!(-        "{}",-        serde_json::to_string_pretty(&json!({"results":result})).unwrap()-    ); }
crates/aiflow-analyze/src/branches.rs+31−2
@@ -304,7 +304,16 @@ pub fn screen(board: &Board, net: &str, load: &Value, spec: &Value) -> Result<Va                 let center = key(l, (p.x, p.y));                 for k in &candidates {                     if k.0 == l-                        && distance(pt(*k), (p.x, p.y)) <= p.size.0.min(p.size.1) / 2. + 1e-7+                        && if p.shape == "rect" {+                            p.polys.iter().any(|poly| {+                                let (x, y) = pt(*k);+                                aiflow_board::geom::point_poly_distance(x, y, poly) <= 1e-7+                            })+                        } else {+                            // Rounded pads retain the conservative central-circle model.+                            // The board parser currently represents roundrects by bounding boxes.+                            distance(pt(*k), (p.x, p.y)) <= p.size.0.min(p.size.1) / 2. + 1e-7+                        }                     {                         unite(center, *k, &mut parents);                     }@@ -397,7 +406,7 @@ pub fn screen(board: &Board, net: &str, load: &Value, spec: &Value) -> Result<Va         cases.push(json!({"throughPadLayerCase":mask,"conductors":conductors}));     }     Ok(-        json!({"net":net,"status":if worst<=max_rise && peak_barrel<=barrel_limit{"pass"}else{"fail"},"method":"explicit-pin resistive centerlines plus mandatory same-input Fields","peakTrackRiseC":worst,"peakBarrelDensityAperMm2":peak_barrel,"barrelDensityLimitAperMm2":barrel_limit,"maxRiseC":max_rise,"cases":cases,"how":format!("branch currents give {worst:.2} C maximum track estimate against {max_rise:.2} C; barrel density {peak_barrel:.2} against {barrel_limit:.2} A/mm2; all through-terminal layer extremes checked"),"limits":"Nominal straight-track and barrel resistances, ideal pad central regions; no pad/contact thermal qualification. Filled-copper Fields and physical stack validation remain mandatory."}),+        json!({"net":net,"status":if worst<=max_rise && peak_barrel<=barrel_limit{"pass"}else{"fail"},"method":"explicit-pin resistive centerlines plus mandatory same-input Fields","peakTrackRiseC":worst,"peakBarrelDensityAperMm2":peak_barrel,"barrelDensityLimitAperMm2":barrel_limit,"maxRiseC":max_rise,"cases":cases,"how":format!("branch currents give {worst:.2} C maximum track estimate against {max_rise:.2} C; barrel density {peak_barrel:.2} against {barrel_limit:.2} A/mm2; all through-terminal layer extremes checked"),"limits":"Nominal straight-track and barrel resistances, ideal rectangular pad outlines and conservative rounded-pad central regions; no pad/contact thermal qualification. Filled-copper Fields and physical stack validation remain mandatory."}),     ) } @@ -467,6 +476,26 @@ mod geometry_tests {         assert_eq!(r["status"], "fail");     }     #[test]+    fn rectangular_corner_contact_and_rotated_contact_are_connected_but_gaps_refuse() {+        for rotation in [0., 90., 37.] {+            let base = board(2.).text;+            let (dx, dy) = aiflow_board::geom::rot_kicad(0.45, 0.9, rotation);+            let shape = format!("(at 0 0 {rotation}) (size 1 2)");+            let text = base.replace(+                r#"(property "Reference" "F1") (pad "1" smd circle (at 0 0) (size 1 1)"#,+                &format!(r#"(property "Reference" "F1") (pad "1" smd rect {shape}"#),+            );+            for (offset, connected) in [(0., true), (2., false)] {+                let endpoint = format!("(end {:.6} {:.6})", 12. + dx + offset, 2. + dy);+                let text = text.replace("(end 12 2)", &endpoint);+                let b = Board::from_text("corner.kicad_pcb", text).unwrap();+                let s = spec();+                let result = screen(&b, "V", &s["loads"]["V"], &s);+                assert_eq!(result.is_ok(), connected, "rotation={rotation}, offset={offset}: {result:?}");+            }+        }+    }+    #[test]     fn missing_pin_and_implicit_material_assumptions_refuse() {         let mut s = spec();         s["loads"]["V"]["terminals"].as_array_mut().unwrap().pop();