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.
main
John Lauer
Publish 0.2.24
5b1c5bf
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
//! Issue #107: with a same-input Fields solve, the solved rise at each net's narrowest neck judges the
//! whole-net IPC-2221 screen.
//!
//! The whole-net screen takes the narrowest track over 0.2 mm anywhere on a loaded net and judges it at
//! the full load. Side strips, short branches and necks into small pins never carry the full load, so
//! a careful design can fail that approximation. adom-fields (0.9+) reports `review.checks.ampacity`:
//! per loaded net, the neck where the solved current density peaks, its width and layer, and the
//! solved temperature rise there with every heat source on the board. When that solve has already
//! passed the same-input field screen (exact board and spec hashes, converged at the full declared
//! current, density and barrel limits), its solved rise replaces the approximation for the nets whose
//! only failure was the IPC width (or a pour that does not cover the narrow copper). The heuristic
//! stays the verdict without a solve.
use serde_json::{json, Value};
/// Fields rounds the solved rise to 0.1 C; reserve half a display step.
const ROUNDING_C: f64 = 0.05;
/// Rewrite the failing whole-net width results that the solve can judge. Call only after the field
/// screen of the same report passed. Returns one line per net it judged.
pub fn accept_solve(results: &mut [Value], spec: &Value, fields: &Value) -> Vec<String> {
let rows = fields["review"]["checks"]["ampacity"].as_array().cloned().unwrap_or_default();
let max_rise_default = spec["maxRiseC"].as_f64().unwrap_or(10.0);
let mut notes = Vec::new();
for r in results.iter_mut() {
if r["status"] != "fail" || !matches!(r["failure"].as_str(), Some("bottleneck" | "pour-coverage")) { continue; }
let net = r["net"].as_str().unwrap_or("").to_string();
let load = &spec["loads"][&net];
let amps = load["amps"].as_f64().unwrap_or(0.0);
let limit = load["maxRiseC"].as_f64().unwrap_or(max_rise_default);
let Some(row) = rows.iter().find(|x| x["net"].as_str() == Some(net.as_str())) else {
r["hint"] = json!(format!("{}; the Fields report has no review.checks.ampacity row for {net} (adom-fields 0.9 or later reports the solved rise at its narrowest neck)", r["hint"].as_str().unwrap_or("")));
continue;
};
let row_amps = row["amps"].as_f64().unwrap_or(f64::NAN);
let rise = row["solvedRiseC"].as_f64().filter(|v| v.is_finite() && *v >= 0.0);
if !row_amps.is_finite() || (row_amps - amps).abs() > 1e-8 * amps.max(1.0) || rise.is_none() {
r["hint"] = json!(format!("{}; the Fields ampacity row for {net} is not a finite solved rise at the full {amps} A", r["hint"].as_str().unwrap_or("")));
continue;
}
let rise = rise.unwrap();
let neck = format!("{:.2} mm on {} at ({:.2}, {:.2})", row["neckMm"].as_f64().unwrap_or(f64::NAN), row["layer"].as_str().unwrap_or("?"), row["at"][0].as_f64().unwrap_or(f64::NAN), row["at"][1].as_f64().unwrap_or(f64::NAN));
let fields_judgement = json!({"solvedRiseC": rise, "limitC": limit, "neck": row.clone()});
if rise + ROUNDING_C <= limit {
let ipc = r.clone();
r["status"] = json!("pass");
r["judgedBy"] = json!("fields-solve");
r["fieldsAmpacity"] = fields_judgement;
r["ipcWholeNetScreen"] = ipc;
r["how"] = json!(format!("judged by the Fields solve: {rise:.1} C solved rise at the narrowest neck (about {neck}) at {amps:.1} A, within {limit:.0} C; the whole-net IPC-2221 approximation alone failed it"));
if let Some(o) = r.as_object_mut() { o.remove("hint"); }
notes.push(format!("{net}: judged by the Fields solve ({rise:.1} C at its narrowest neck, limit {limit:.0} C)"));
} else {
r["fieldsAmpacity"] = fields_judgement;
r["hint"] = json!(format!("{}; the same-input Fields solve agrees: {rise:.1} C at its narrowest neck (about {neck}), limit {limit:.0} C", r["hint"].as_str().unwrap_or("")));
}
}
notes
}
#[cfg(test)]
mod tests {
use super::*;
fn spec() -> Value { json!({"maxRiseC": 10, "loads": {"VBUS": {"amps": 1.5}, "VSYS": {"amps": 1.2, "maxRiseC": 5}}}) }
fn fields(rise: Value) -> Value {
json!({"review": {"checks": {"ampacity": [
{"net": "VBUS", "amps": 1.5, "neckMm": 0.31, "layer": "F.Cu", "copperUm": 35, "ipc2221_10C_A": 1.1, "solvedRiseC": rise, "at": [12.0, 4.0]},
{"net": "VSYS", "amps": 1.2, "neckMm": 0.25, "layer": "F.Cu", "copperUm": 35, "ipc2221_10C_A": 0.9, "solvedRiseC": 6.0, "at": [3.0, 4.0]}]}}})
}
fn failing() -> Vec<Value> {
vec![json!({"net": "VBUS", "amps": 1.5, "status": "fail", "failure": "bottleneck", "hint": "VBUS carries 1.5 A on a 0.30 mm bottleneck"}),
json!({"net": "VSYS", "amps": 1.2, "status": "fail", "failure": "bottleneck", "hint": "VSYS ..."})]
}
#[test]
fn a_solved_rise_within_the_limit_judges_the_net() {
let mut r = failing();
let notes = accept_solve(&mut r, &spec(), &fields(json!(3.5)));
assert_eq!(r[0]["status"], "pass");
assert_eq!(r[0]["judgedBy"], "fields-solve");
assert!(r[0]["how"].as_str().unwrap().contains("judged by the Fields solve"));
assert_eq!(r[0]["ipcWholeNetScreen"]["status"], "fail");
// VSYS: 6.0 C solved against its own 5 C limit still fails, and says so
assert_eq!(r[1]["status"], "fail");
assert!(r[1]["hint"].as_str().unwrap().contains("6.0 C"));
assert_eq!(notes.len(), 1);
}
#[test]
fn rounding_allowance_missing_rows_and_other_failures_stay_failed() {
let mut r = failing();
accept_solve(&mut r, &spec(), &fields(json!(9.96)));
assert_eq!(r[0]["status"], "fail");
for rise in [Value::Null, json!(-1.0), json!("3")] {
let mut r = failing();
accept_solve(&mut r, &spec(), &fields(rise));
assert_eq!(r[0]["status"], "fail");
}
let mut r = failing();
accept_solve(&mut r, &spec(), &json!({"review": {}}));
assert!(r[0]["hint"].as_str().unwrap().contains("no review.checks.ampacity row"));
// a solve at another current does not judge the declared load
let mut f = fields(json!(3.5)); f["review"]["checks"]["ampacity"][0]["amps"] = json!(1.0);
let mut r = failing();
accept_solve(&mut r, &spec(), &f);
assert_eq!(r[0]["status"], "fail");
// via stitching and missing copper are not the width approximation
let mut r = vec![json!({"net": "VBUS", "status": "fail", "failure": "via-stitching", "hint": "x"})];
accept_solve(&mut r, &spec(), &fields(json!(1.0)));
assert_eq!(r[0]["status"], "fail");
}
}