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.33
d057c93
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
//! Issue #116: a hot part with no exposed tab, cooled through its leads (LEDs, inductors, small
//! leaded ICs). `spec.hot.<REF>: {"watts": W, "cooling": "leads"}` with no tabNet. adom-fields
//! models such a part with its watts through all of its pads by area and reports it as tabNet
//! "leads". There is no tab pour to estimate, so the part passes only on that same-input Fields
//! solve, never on the copper estimate, and it gets no heatsink-via target.
use aiflow_board::Board;
use serde_json::{json, Value};
/// The tabNet adom-fields reports for a part it modelled through its leads.
pub const LEADS: &str = "leads";
/// Whether `spec.hot.<reference>` is lead-cooled; Err for a contradictory declaration. A part with
/// no tabNet is lead-cooled, exactly as adom-fields models it (Matt's rule in #116: no extra key to
/// remember); `"cooling": "leads"` says the same thing explicitly.
pub fn declared(reference: &str, h: &Value) -> Result<bool, String> {
let no_tab = h["tabNet"].is_null() || h["tabNet"] == "";
let explicit = match &h["cooling"] {
Value::Null => false,
Value::String(s) if s == LEADS => true,
other => return Err(format!("{reference}: cooling must be \"leads\" (a part with no exposed tab), not {other}")),
};
if !no_tab {
return if explicit { Err(format!("{reference}: cooling \"leads\" is for a part with no exposed tab, but it also names tabNet {}: keep the tabNet if the part has an exposed pad, else remove it", h["tabNet"])) } else { Ok(false) };
}
if explicit && !h["tabPad"].is_null() { return Err(format!("{reference}: cooling \"leads\" has no tab, so tabPad does not apply")); }
if !h["tjMaxC"].is_null() {
let theta = h["thetaJcKPerW"].as_f64().filter(|v| v.is_finite() && *v > 0.0);
let source = h["thetaSource"].as_str().filter(|s| !s.trim().is_empty());
if theta.is_none() || source.is_none() || !h["tjMaxC"].as_f64().is_some_and(|v| v.is_finite()) {
return Err(format!("{reference}: a junction limit (tjMaxC) on a lead-cooled part needs thetaJcKPerW, the datasheet's junction-to-board figure for this package, and thetaSource naming where it came from"));
}
}
Ok(true)
}
/// The copper-estimate row for a declared lead-cooled part: pending until the Fields solve judges it.
pub fn preliminary(reference: &str, h: &Value) -> Value {
let watts = h["watts"].as_f64().unwrap_or(0.0);
let mut r = json!({"ref": reference, "watts": watts, "cooling": LEADS, "tabNet": LEADS});
if watts <= 0.0 {
r["status"] = json!("pass");
r["how"] = json!(format!("lead-cooled, {watts} W declared: nothing to dissipate"));
} else {
r["status"] = json!("pending-fields");
r["hint"] = json!(format!("{reference} is lead-cooled ({watts} W through its leads, no exposed tab): no tab pour applies, so it passes only on the same-input Fields solve (adom-fields analyze on this saved board and spec)"));
}
r
}
/// Refuse lead cooling for a part that has an exposed pad: its heat goes into that pad's net.
/// Two-terminal parts are their leads; with three or more pads, one pad at least three times the
/// median of the others is a tab.
pub fn check_footprint(b: &Board, reference: &str) -> Result<(), String> {
let pads: Vec<_> = b.pads.iter().filter(|p| p.reference == reference).collect();
if pads.is_empty() { return Err(format!("{reference} (cooling \"leads\") has no pads on the board")); }
if pads.len() < 3 { return Ok(()); }
let area = |p: &aiflow_board::Pad| p.size.0 * p.size.1;
let big = pads.iter().copied().max_by(|a, z| area(a).total_cmp(&area(z))).unwrap();
let mut rest: Vec<f64> = pads.iter().filter(|p| !std::ptr::eq(**p, big)).map(|p| area(p)).collect();
rest.sort_by(f64::total_cmp);
let median = rest[rest.len() / 2];
if area(big) >= 3.0 * median {
return Err(format!("{reference} has an exposed pad ({} on {}, {:.2} mm2 against {:.2} mm2 for its leads): declare tabNet {} instead of cooling \"leads\"", big.name, if big.net.is_empty() { "no net" } else { big.net.as_str() }, area(big), median, if big.net.is_empty() { "<its net>" } else { big.net.as_str() }));
}
Ok(())
}
/// After the same-input field screen passed: each pending lead-cooled row takes its solved rise.
pub fn accept_solve(results: &mut [Value], fields: &Value) -> Vec<String> {
let mut notes = Vec::new();
for r in results.iter_mut().filter(|r| r["cooling"] == LEADS && r["status"] == "pending-fields") {
let reference = r["ref"].as_str().unwrap_or("").to_string();
let row = fields["chips"].as_array().into_iter().flatten().find(|c| c["reference"] == reference.as_str() && c["tabNet"] == LEADS);
match row {
Some(c) => {
r["status"] = json!("pass");
r["judgedBy"] = json!("fields-solve");
r["riseC"] = c["riseC"].clone();
r["riseMovingC"] = c["riseMovingC"].clone();
r["how"] = json!(format!("lead-cooled: the same-input Fields solve puts it at +{} C still air, +{} C moving air, within its budget", c["riseC"], c["riseMovingC"]));
r.as_object_mut().unwrap().remove("hint");
notes.push(format!("{reference}: lead-cooled, judged by the Fields solve"));
}
None => {
r["status"] = json!("fail");
r["hint"] = json!(format!("{reference}: the Fields report has no lead-cooled row for it; solve again on this saved board and spec"));
}
}
}
notes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn declaration_is_explicit_and_consistent() {
assert_eq!(declared("D1", &json!({"watts": 0.06})), Ok(true), "no tabNet is lead-cooled, as Fields models it");
assert_eq!(declared("Q1", &json!({"watts": 2, "tabNet": "VCC"})), Ok(false));
assert_eq!(declared("D1", &json!({"watts": 0.06, "cooling": "leads"})), Ok(true));
assert!(declared("D1", &json!({"watts": 0.06, "cooling": "leads", "tabNet": "GND"})).is_err());
assert!(declared("D1", &json!({"watts": 0.06, "cooling": "leads", "tabPad": "3"})).is_err());
assert!(declared("D1", &json!({"watts": 0.06, "cooling": "tab"})).is_err());
assert!(declared("U1", &json!({"watts": 0.3, "cooling": "leads", "tjMaxC": 125})).is_err());
assert!(declared("U1", &json!({"watts": 0.3, "cooling": "leads", "tjMaxC": 125, "thetaJcKPerW": 60})).is_err());
assert_eq!(declared("U1", &json!({"watts": 0.3, "cooling": "leads", "tjMaxC": 125, "thetaJcKPerW": 60, "thetaSource": "datasheet p.4 psiJB"})), Ok(true));
}
#[test]
fn pending_until_the_solve_then_judged_by_it() {
let mut rows = vec![preliminary("D10", &json!({"watts": 0.07, "cooling": "leads"}))];
assert_eq!(rows[0]["status"], "pending-fields");
assert_eq!(preliminary("D11", &json!({"watts": 0, "cooling": "leads"}))["status"], "pass");
let wrong = json!({"chips": [{"reference": "D10", "tabNet": "GND", "riseC": 3.0, "riseMovingC": 2.0}]});
let mut failed = rows.clone();
accept_solve(&mut failed, &wrong);
assert_eq!(failed[0]["status"], "fail");
let fields = json!({"chips": [{"reference": "D10", "tabNet": "leads", "riseC": 3.1, "riseMovingC": 2.2}]});
assert_eq!(accept_solve(&mut rows, &fields).len(), 1);
assert_eq!(rows[0]["status"], "pass");
assert_eq!(rows[0]["judgedBy"], "fields-solve");
}
fn board(pads: &str) -> Board {
Board::from_text("t", format!("(kicad_pcb (version 20241229) (layers (0 \"F.Cu\" signal) (2 \"B.Cu\" signal) (25 \"Edge.Cuts\" user)) (net 0 \"\") (net 1 \"GND\") (net 2 \"A\") (gr_rect (start 0 0) (end 20 20) (layer \"Edge.Cuts\")) (footprint \"T:P\" (layer \"F.Cu\") (at 10 10) (property \"Reference\" \"U1\") {pads}))")).unwrap()
}
#[test]
fn an_exposed_pad_refuses_lead_cooling() {
let leads = "(pad \"1\" smd rect (at -1 0) (size 0.6 1.0) (layers \"F.Cu\") (net 2 \"A\")) (pad \"2\" smd rect (at 0 0) (size 0.6 1.0) (layers \"F.Cu\") (net 1 \"GND\")) (pad \"3\" smd rect (at 1 0) (size 0.6 1.0) (layers \"F.Cu\") (net 2 \"A\"))";
assert!(check_footprint(&board(leads), "U1").is_ok());
let tab = format!("{leads} (pad \"4\" smd rect (at 0 3) (size 3.2 2.0) (layers \"F.Cu\") (net 1 \"GND\"))");
let e = check_footprint(&board(&tab), "U1").unwrap_err();
assert!(e.contains("exposed pad") && e.contains("GND"), "{e}");
assert!(check_footprint(&board(leads), "U9").is_err());
}
}