← Commit history

0.9.8: hot parts read at their exposed pad, footprint thermal vias counted (issue 20)

John Lauer ·cdd995c100 · ·parent 2fd6f09
8 files changed +35−14
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-fields"-version = "0.9.6"+version = "0.9.8" dependencies = [  "clap",  "png",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-fields"-version = "0.9.6"+version = "0.9.8" edition = "2021" license = "MIT" description = "Adom Fields: where the current and the heat go on your board. Current density, copper heat flow and board temperature from the board file itself, in 3D, for the AI to drive and the human to explore."
SKILL.md+4
@@ -247,6 +247,10 @@ The items covered: - **On the ESC's SWD clock** (73 mm, 449 ps of 58 Ω line, a 0.5 ns edge from 20 Ω): 40 % overshoot, fixed by a 38 Ω series resistor. The DShot input (2 ns edge) is electrically short. - `impedance` segments' `psPerMm` is now in ps/mm (it read a million times too high before 0.9.6). +## Hot parts read at their tab (0.9.8)++A hot part with a tab net is read at its tab: the pad named in `hot.<ref>.tabPad` (the exposed pad's number, e.g. `"29"`), else the largest tab-net pad, so an exposed pad wins over corner pins that share its net. `vias` counts the tab net's vias near every tab-net pad, including a footprint's own thermal-via holes. The watts are still spread over all tab-net pads by area.+ ## The ground view (0.7.6)  `ui view --quantity ground`: the ground copper lifted by its solved millivolts (rings every step), each part's ground pin tagged with its offset, dashed arcs for the possible loops through the ground connectors' cables, floating ground islands in red, and the stitching fence: a ring per ground via (λ/40 reach) with the uncovered stretches of the edge in red.
docs/release-0.9.8.mdadded+6
@@ -0,0 +1,6 @@+# adom-fields 0.9.8++- **Hot parts read at their tab (issue 20).**+  - **The bug:** a part whose tab net has several pads (an exposed pad plus corner pins on GND) was read at its first pad in board order. Its rise, via count and thermal-via issue came from a corner pin, not the die pad.+  - **The fix:** it is now read at the pad named by `hot.<ref>.tabPad`, else the largest tab-net pad. Vias are counted near every tab-net pad, and a footprint's own thermal-via holes count.+  - **On the AM/FM receiver's QFN-48:** with 0.5 W in it, the exposed pad reads +15.2 °C (it read +10.4 °C at pin 7), with its 9 footprint thermal vias counted (it read 0).
package.json+2−2
@@ -1,8 +1,8 @@ {   "slug": "adom-fields",   "type": "app",-  "version": "0.9.7",-  "changelog": "0.9.7: skill cleanup: shorter skill description, and the two SKILL.md copies are the same again.",+  "version": "0.9.8",+  "changelog": "0.9.8: a hot part is read at its exposed pad (hot.<ref>.tabPad, else the largest tab-net pad), and its thermal vias include the footprint's own (issue 20).",   "title": "Adom Fields",   "brief": "Adom Fields: where the current and the heat go on your board.",   "summary": "Current density on the copper, heat flow through the pours and the vias, and the temperature over the board, solved from the board file itself and shown on the board in 3D. The AI does the analysis and drives the view; the human orbits, zooms, hovers for the number under the cursor, and reads the nets by what they carry and the issues by what to fix. A step in Adom's AI Flow: the walkthrough goes into the video.",
page.json+2−2
@@ -1,8 +1,8 @@ {   "slug": "adom-fields",   "type": "app",-  "version": "0.9.7",-  "changelog": "0.9.7: skill cleanup: shorter skill description, and the two SKILL.md copies are the same again.",+  "version": "0.9.8",+  "changelog": "0.9.8: a hot part is read at its exposed pad (hot.<ref>.tabPad, else the largest tab-net pad), and its thermal vias include the footprint's own (issue 20).",   "title": "Adom Fields",   "brief": "Adom Fields: where the current and the heat go on your board.",   "summary": "Current density on the copper, heat flow through the pours and the vias, and the temperature over the board, solved from the board file itself and shown on the board in 3D. The AI does the analysis and drives the view; the human orbits, zooms, hovers for the number under the cursor, and reads the nets by what they carry and the issues by what to fix. A step in Adom's AI Flow: the walkthrough goes into the video.",
skills/adom-fields/SKILL.md+4
@@ -247,6 +247,10 @@ The items covered: - **On the ESC's SWD clock** (73 mm, 449 ps of 58 Ω line, a 0.5 ns edge from 20 Ω): 40 % overshoot, fixed by a 38 Ω series resistor. The DShot input (2 ns edge) is electrically short. - `impedance` segments' `psPerMm` is now in ps/mm (it read a million times too high before 0.9.6). +## Hot parts read at their tab (0.9.8)++A hot part with a tab net is read at its tab: the pad named in `hot.<ref>.tabPad` (the exposed pad's number, e.g. `"29"`), else the largest tab-net pad, so an exposed pad wins over corner pins that share its net. `vias` counts the tab net's vias near every tab-net pad, including a footprint's own thermal-via holes. The watts are still spread over all tab-net pads by area.+ ## The ground view (0.7.6)  `ui view --quantity ground`: the ground copper lifted by its solved millivolts (rings every step), each part's ground pin tagged with its offset, dashed arcs for the possible loops through the ground connectors' cables, floating ground islands in red, and the stitching fence: a ring per ground via (λ/40 reach) with the uncovered stretches of the edge in red.
src/fields.rs+15−8
@@ -97,7 +97,7 @@ fn d60() -> f64 { 60.0 } #[derive(Deserialize, Clone)] pub struct Load { pub amps: f64, #[serde(default)] pub maxRiseC: Option<f64>, /** the IR drop this net may have from its source to its farthest sink, mV (issue 14) */ #[serde(default)] pub maxDropMv: Option<f64>, /** each sink part's own draw on this net, amps, e.g. {"U3": 0.3, "J2": 4.0} (issue 17); the rest of the net's amps splits over the other sinks by pad area */ #[serde(default)] pub draws: BTreeMap<String, f64>, #[serde(default)] pub terminals: Option<Vec<crate::terminals::Terminal>> } /// a part's dissipation: through its exposed tab's net (tabNet), or, for a leaded part with no tab, through all its leads, /// shared by pad area (issue 8)-#[derive(Deserialize, Clone)] pub struct Hot { pub watts: f64, #[serde(default)] pub tabNet: String, /** this part's own rise limit, °C; spec hotMaxRiseC when not given (issue 16) */ #[serde(default)] pub maxRiseC: Option<f64>, /** absolute maximum junction temperature, °C (125 when not given) */ #[serde(default)] pub tjMaxC: Option<f64>, /** junction-to-case (tab), K/W, added on top of the solved tab rise */ #[serde(default)] pub thetaJcKPerW: Option<f64> }+#[derive(Deserialize, Clone)] pub struct Hot { pub watts: f64, #[serde(default)] pub tabNet: String, /** this part's own rise limit, °C; spec hotMaxRiseC when not given (issue 16) */ #[serde(default)] pub maxRiseC: Option<f64>, /** absolute maximum junction temperature, °C (125 when not given) */ #[serde(default)] pub tjMaxC: Option<f64>, /** junction-to-case (tab), K/W, added on top of the solved tab rise */ #[serde(default)] pub thetaJcKPerW: Option<f64>, /** the pad the part is read at (its exposed pad's number, e.g. "29"); the largest tab-net pad when not given */ #[serde(default)] pub tabPad: Option<String> }  #[derive(Serialize, Clone)] pub struct NetResult { /** the current on every terminal pin the solve used: + into the copper, - out (issue 17) */ pub terminalCurrents: Vec<Value>, /** heuristic or explicit, and how the sources and sinks were chosen */ pub terminalBasis: String, pub residual: f64, pub converged: bool, pub platedPads: usize, pub platedConnections: Vec<Value>, pub net: String, pub amps: f64, pub layers: Vec<String>, pub peakJ: f64, pub peakAt: [f64; 2], pub peakLayer: String, pub copperMm2: f64, pub jouleW: f64, pub vias: usize, pub iterations: usize }@@ -439,7 +439,7 @@ pub fn analyze(b: &Board, spec: &Spec, pitch: f64, neck: f64, log: &mut dyn FnMu     };     let mut q_rhs = vec![0.0; nl * n];     for k in 0..nl { for i in 0..n {q_rhs[k*n+i] += p_joule[k].v[i] as f64;} }-    let mut chip_pos: Vec<(String, f64, f64, f64, String)> = Vec::new();+    let mut chip_pos: Vec<(String, f64, f64, f64, String, Vec<(f64, f64)>)> = Vec::new();     for (reference, hot) in &spec.hot {         let leads = hot.tabNet.is_empty();         let pads: Vec<&crate::board::Pad> = b.pads.iter().filter(|p| p.reference == *reference && (leads || p.net == hot.tabNet)).collect();@@ -454,7 +454,11 @@ pub fn analyze(b: &Board, spec: &Spec, pitch: f64, neck: f64, log: &mut dyn FnMu             for dr in -rad..=rad { for dc in -rad..=rad { let (r, c) = (r0 as isize + dr, c0 as isize + dc); if r >= 0 && c >= 0 && (r as usize) < h && (c as usize) < w && dr * dr + dc * dc <= rad * rad { spots.push((r as usize, c as usize)); } } }             for (r, c) in &spots { q_rhs[r * w + c] += per / spots.len() as f64; }         }-        chip_pos.push((reference.clone(), cells[0].0, cells[0].1, hot.watts, if leads { "leads".to_string() } else { hot.tabNet.clone() }));+        // the part is read at its tab: the named pad (hot.<ref>.tabPad), else the largest tab-net pad (an exposed pad+        // beats the corner pins that share its net, issue 20); its vias are counted under every tab-net pad+        let at = hot.tabPad.as_ref().and_then(|tp| pads.iter().position(|p| p.pad == *tp)).unwrap_or_else(|| (0..pads.len()).max_by(|a, z| pads[*a].area.partial_cmp(&pads[*z].area).unwrap()).unwrap_or(0));+        if let Some(tp) = &hot.tabPad { if !pads.iter().any(|p| p.pad == *tp) { log(&format!("{reference}: tabPad {tp} is not a pad on {}; read at the largest one", hot.tabNet)); } }+        chip_pos.push((reference.clone(), cells[at].0, cells[at].1, hot.watts, if leads { "leads".to_string() } else { hot.tabNet.clone() }, if leads { vec![cells[at]] } else { cells.clone() }));     }     let tab_total: f64 = spec.hot.values().map(|h| h.watts).sum();     let (sys_still, sink_meshes, packages_still, solid_still) = build(0.0);@@ -474,7 +478,7 @@ pub fn analyze(b: &Board, spec: &Spec, pitch: f64, neck: f64, log: &mut dyn FnMu         let t_end = tr.durationS.max(1e-3); let t0 = (t_end / 1e4).max(1e-4); let ratio = (t_end / t0).powf(1.0 / (steps as f64 - 1.0));         let mut temp = if tr.fromSteady == Some(true) { tv.clone() } else { vec![0.0; sys_still.n()] };         let (mut tprev, mut rows) = (0.0f64, Vec::new());-        let chip_cells: Vec<(String, usize)> = chip_pos.iter().filter_map(|(rf, x, y, _, _)| cell(*x, *y).map(|(r, c)| (rf.clone(), r * w + c))).collect();+        let chip_cells: Vec<(String, usize)> = chip_pos.iter().filter_map(|(rf, x, y, _, _, _)| cell(*x, *y).map(|(r, c)| (rf.clone(), r * w + c))).collect();         for s in 0..steps {             let tnow = t0 * ratio.powi(s as i32); let dt = (tnow - tprev).max(1e-6); tprev = tnow;             let mut sy = sys_still.clone();@@ -547,7 +551,7 @@ pub fn analyze(b: &Board, spec: &Spec, pitch: f64, neck: f64, log: &mut dyn FnMu         let sol_ref = match (&sol_l, lv) { (Some(a), _) => Some(a), (None, x) if x == 0.0 => solid_still.as_ref(), (None, _) => solid_moving.as_ref() };         let (sv, sc) = match (&spec.solids, sol_ref) { (Some(sd), Some(a)) => a.result(sd, &tl), _ => (Value::Null, Value::Null) };         let pk_ref = match &pk_l { Some(p) => p, None => if lv == 0.0 { &packages_still } else { &packages_moving } };-        let chips_l: Vec<(String, f64)> = chip_pos.iter().filter_map(|(rf, x, y, _, _)| { let (r0, c0) = cell(*x, *y)?; let mut v = tl[r0 * w + c0];+        let chips_l: Vec<(String, f64)> = chip_pos.iter().filter_map(|(rf, x, y, _, _, _)| { let (r0, c0) = cell(*x, *y)?; let mut v = tl[r0 * w + c0];             for (p, nd) in spec.packageCooling.iter().zip(pk_ref.iter()) { if p.reference == *rf { v = v.max(tl[nd.junction]).max(tl[nd.case]); } }             if let (Some(sd), Some(a)) = (&spec.solids, sol_ref) { for c in &a.cells { if sd.bodies[c.1].heat_of.as_deref() == Some(rf.as_str()) { v = v.max(tl[c.0]); } } }             Some((rf.clone(), (v * 10.0).round() / 10.0)) }).collect();@@ -557,7 +561,7 @@ pub fn analyze(b: &Board, spec: &Spec, pitch: f64, neck: f64, log: &mut dyn FnMu     let r_chip = 3.0;     let via_wanted = |reference: &str| -> usize { spec.thermalVias.iter().filter(|v| v["pad"].as_str().map(|p| p.split('.').next() == Some(reference)).unwrap_or(false)).map(|v| v["count"].as_u64().unwrap_or(0) as usize).sum() };     let mut chips: Vec<ChipResult> = Vec::new();-    for (reference, x, y, watts, tab) in &chip_pos {+    for (reference, x, y, watts, tab, tab_pads) in &chip_pos {         let Some((r0, c0)) = cell(*x, *y) else { continue };         let inside = |r: usize, c: usize| -> bool { let (dr, dc) = (r as f64 - r0 as f64, c as f64 - c0 as f64); (dr * dr + dc * dc) * d * d <= r_chip * r_chip };         let mut plane = 0.0;@@ -568,8 +572,11 @@ pub fn analyze(b: &Board, spec: &Spec, pitch: f64, neck: f64, log: &mut dyn FnMu         let (mut via_w, mut other_w, mut diel_w, mut air_w, mut nv, mut no) = (0.0, 0.0, 0.0, 0.0, 0usize, 0usize);         let g_diel = K_FR4_Z * d * d / t_diel[0];         let mut own_cells: std::collections::HashSet<usize> = std::collections::HashSet::new();-        for v in b.vias.iter().filter(|v| v.net == *tab) { if let Some((r, c)) = cell(v.x, v.y) { if inside(r, c) { own_cells.insert(r * w + c); nv += 1; } } }-        for v in b.vias.iter().filter(|v| v.net != *tab) { if let Some((r, c)) = cell(v.x, v.y) { if inside(r, c) { no += 1; } } }+        let near_tab = |vx: f64, vy: f64| tab_pads.iter().any(|(px, py)| (vx - px).hypot(vy - py) <= r_chip);+        for v in b.vias.iter().filter(|v| v.net == *tab) { if near_tab(v.x, v.y) { nv += 1; if let Some((r, c)) = cell(v.x, v.y) { if inside(r, c) { own_cells.insert(r * w + c); } } } }+        for v in b.vias.iter().filter(|v| v.net != *tab) { if near_tab(v.x, v.y) { no += 1; } }+        // a footprint's own thermal vias (plated holes in the exposed pad) count as the tab's vias too+        for v in b.plated_holes.iter().filter(|v| v.net == *tab && v.drill <= 0.5) { if near_tab(v.x, v.y) { nv += 1; if let Some((r, c)) = cell(v.x, v.y) { if inside(r, c) { own_cells.insert(r * w + c); } } } }         for r in 0..h { for c in 0..w { if !inside(r, c) { continue; } let i = r * w + c; let dt = tv[i] - tv[n + i]; let gv = sys_still.gz[0][i] - g_diel; if gv > 0.0 { if own_cells.contains(&i) { via_w += gv * dt; } else { other_w += gv * dt; } } diel_w += g_diel * dt; air_w += sys_still.leak[0][i] * tv[i]; } }         chips.push(ChipResult { reference: reference.clone(), watts: *watts, tabNet: tab.clone(), x: *x, y: *y, riseC: (tv[r0 * w + c0] * 10.0).round() / 10.0, riseMovingC: (tm[r0 * w + c0] * 10.0).round() / 10.0, planeW: (plane * 1000.0).round() / 1000.0, viaW: (via_w * 1000.0).round() / 1000.0, vias: nv, otherViaW: (other_w * 1000.0).round() / 1000.0, otherVias: no, dielW: (diel_w * 1000.0).round() / 1000.0, airW: (air_w * 1000.0).round() / 1000.0, viasWanted: via_wanted(reference) });     }